Bootstrap

JavaScript异步编程

璃安猫: 观看学习视频总结的一些异步编程的相关知识点,还有许多不完善的地方,之后会逐渐补充,若有不对之处烦请指正。

JS工作模式

JS是单线程的,但浏览器不是单线程的 即API不是单线程

JavaScript采用单线程模式工作的原因

JS是在浏览器端的脚本语言,目的是为了实现页面的动态交互(通过DOM操作),因此采用单线程避免出现复杂的线程同步问题,成为特性之一

单线程:JS执行环境中负责执行代码的线程只有一个

JS将任务执行模式分成了两种 同步模式(Synchrorous) / 异步模式(Asynchronous)

这里所说的同步/异步模式指的是运行环境提供的API是以同步/异步模式的方式工作 而不是JS的运行模式

同步模式

执行代码方式与编写代码一致,并不是同时执行,而是排队执行

//首先将以下代码添加到一个匿名函数内  然后逐行执行

//压入调用栈 执行完毕后出栈
console.log('global begin')


//函数 / 变量的声明不会产生调用
function bar () {
    //打印结果 执行完毕出栈
	console.log('bar task')
}

function foo() {
    // 打印结果
	console.log('foo task')
    // bar函数调用 压入调用栈  执行完毕后出栈
	bar()
}

//foo函数调用 压入调用栈  
foo()

//最后压入调用栈 执行完毕后清空调用栈
//此时为纯同步的任务执行情况   
//存在问题: 
//存在阻塞——若中间某一任务执行时间过长,则后续任务则会被延迟执行
console.log('global end')

使用异步模式用于解决耗时任务的问题 如Ajax node.js 的大文件读写

异步模式

不会去等待这个任务的结束才开始下一个任务

任务开启过后就会执行下一个任务 后续逻辑一般会通过回调函数的方式定义

因为单线程的Js无法同时处理大量耗时任务

问题 代码执行顺序混乱

console.log('global begin');

//将计数器放入API计时后继续执行
setTimeout( function timer1() {
	console.log('timer1 inxole')
}, 1800)

//将计数器放入API计时后继续执行
setTimeout(function timer2() {
	console.log('timer1 invoke')
	
	setTimeout(function inner() {
		console.log('inner invoke')
	}, 1000)
}, 1000)

console.log('global end')

//事件回调
//API计时器结束则进入事件回调 
//以上例子 先结束timer2() 然后将inner的计数器放入API
//接下来timer1()结束 执行结束后出栈
//inner计数器结束后  执行结束后出栈

总结:
同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

JS异步编程

回调函数

回调函数是所有异步编程方案的根基,由调用者定义并交给执行者执行的函数

ES5后为异步而生的的语法

function foo (callback) {
	setTimeout(function () {
		callback()
	}, 3000)
}

foo(function () {
	console.log('回调函数')
	console.log('调用者定义,执行者执行')
	console.log('调用者告诉执行者异步任务结束后应该做什么')
})

Promise

promise——一种更优的异步编程统一方案,为了解决回调函数嵌套导致的回调地狱问题

其本质为对象,表示异步任务最终结果是成功还是失败,因此Promise有三种状态:pending(待定)、fulfilled(成功)、Rejected(失败)

Promise用法

//Promise 基本示例

//其中接收两个参数 resolve 和 reject 都是函数
const promise = new Promise( function (resolve, reject) {
    //这里用于“兑现”承诺
    //只能调用以下两者的其中一个

    //resolve 将 promise状态修改为fulfilled  将resolve的结果传递出去
    resolve(100)    //承诺达成

    //将peomise状态修改为rejected失败  返回失败理由
    // reject(new Error('promise rejected'))
})

//promise的then方法去指定其onfulfilled 和 onrejected 的回调函数
//第一个参数为成功的回调函数  第二个参数为失败的回调函数
//即便是承诺内无内容 then方法也需要进入队列,等待同步操作执行完了 才会执行
promise.then(function (value) {
    //打印得到的惨呼
    console.log('resolved', value);
}, function ( error ) {
    console.log('rejected', error);
})

//先打印该字符串  才会执行then方法内的内容
console.log('end');

Promise使用案例

//Promise 方式的 AJAX

function ajax (url) {
    return new Promise(function (resolve, reject) {
        //AJAX请求
        var xhr = new XMLHttpRequest()
        //请求方式和地址
        xhr.open('GET', url)
        //请求类型
        //获取结果为json对象而不是字符串
        xhr.responseType = 'json'
        //请求完成过后执行
        xhr.onload = function () {
            if (this.status === 200) {
                //请求成功 返回请求的结果
                resolve(this.response)
            } else {
                //请求失败 返回错误信息 即当前状态文本
                reject(new Error(this.statusText))
            }
        }
        //开始执行异步请求
        xhr.send()
    }) 
}

//执行回调函数
ajax('/api/users.json').then(function (res) {
    console.log(res);
}, function (error) {
    console.log(error);
})
//返回成功

//当url改成不存在的文件时 返回Error not found

Promise常见误区

promise本质上是使用回调函数定义的异步任务结束后所需要执行的任务,回调函数是通过then方法传递进去的,并且将回调函数分为了两种 :onfulfilled 和 onrejected

错误用法:若执行串联promise的任务,仍会出现回调函数嵌套的问题

//Promise 方式的 AJAX

function ajax (url) {
    return new Promise(function (resolve, reject) {
        //AJAX请求
        var xhr = new XMLHttpRequest()
        //请求方式和地址
        xhr.open('GET', url)
        //请求类型
        //获取结果为json对象而不是字符串
        xhr.responseType = 'json'
        //请求完成过后执行
        xhr.onload = function () {
            if (this.status === 200) {
                //请求成功 返回请求的结果
                resolve(this.response)
            } else {
                //请求失败 返回错误信息 即当前状态文本
                reject(new Error(this.statusText))
            }
        }
        //开始执行异步请求
        xhr.send()
    }) 
}

//执行回调函数  多个连续的请求仍然会形成回调地狱
//嵌套使用是使用promise最常见的误区
//正确做法是借助Promise then 方法链式调用的特点去保证异步任务的扁平化
ajax('/api/users.json').then(function (urls) {
    ajax(urls.users).then(function (users){
        ajax(urls.users).then(function (users){
            ajax(urls.users).then(function (users){
        
            })
        })
    })
})

Promise链式调用

//Promise 的链式调用

function ajax (url) {
    return new Promise(function (resolve, reject) {
        //AJAX请求
        var xhr = new XMLHttpRequest()
        //请求方式和地址
        xhr.open('GET', url)
        //请求类型
        //获取结果为json对象而不是字符串
        xhr.responseType = 'json'
        //请求完成过后执行
        xhr.onload = function () {
            if (this.status === 200) {
                //请求成功 返回请求的结果
                resolve(this.response)
            } else {
                //请求失败 返回错误信息 即当前状态文本
                reject(new Error(this.statusText))
            }
        }
        //开始执行异步请求
        xhr.send()
    }) 
}

var promise = ajax('/api/users.json')

//执行回调函数  返回结果也是一个promise对象
var promise2 = promise.then(
    //成功后的回调
    function onFulfilled (value) {
        console.log('onFulfilled', value);
    },
    //失败后的回调
    function onRejected (error) {
        console.log('onRejected', error);
    }
)

console.log(promise2);       //打印一个Promise对象
console.log(promise === promise2);      //并不是一个对象
// //因此并不是返回的this方法的链式调用

//执行完一个回调任务后,再去返回一个新的承诺,相互之前无影响
//链式then方法是依次执行  可以在then当中手动回调一个promise对象
ajax('/api/users.json')
    .then(function (value) {
        console.log(1111);
        return ajax('/api/users.json')
    }) // => promise添加状态名并回调
    //每个then方法实际上是为了上一个then方法返回的promise对象添加状态名过后的回调
    //此时的then执行的则是上一个return的ajax的回调任务  可以避免回调的嵌套
    .then(function (value) {
        console.log(2222);
        //则是返回的promise的返回的值  下一个then方法的获取值则是该值
        //若没有返回任何值 则默认返回undefined
        return 'foo'        
    })
    .then(function (value) {
        console.log(value);
    })
    .then(function (value) {
        console.log(4444);
    })
    .then(function (value) {
        console.log(5555);
    })
  • Promise 对象的then方法会返回一个全新的Promise对象
  • 后面的then方法就是在为上衣个then返回的Promise注册回调
  • 前面then方法中回调函数的返回值会作为后面then方法回调的参数
  • 如果回调中返回的是Promise,那后面then方法的回调会等待它的结束

Promise 异常处理

//Promise 异常处理

function ajax (url) {
    return new Promise(function (resolve, reject) {
        // foo()        执行onRejected
        // throw new Error()      执行onRejected
        //AJAX请求
        var xhr = new XMLHttpRequest()
        //请求方式和地址
        xhr.open('GET', url)
        //请求类型
        //获取结果为json对象而不是字符串
        xhr.responseType = 'json'
        //请求完成过后执行
        xhr.onload = function () {
            if (this.status === 200) {
                //请求成功 返回请求的结果
                resolve(this.response)
            } else {
                //请求失败 返回错误信息 即当前状态文本
                //为promise当中的异常做一些处理
                reject(new Error(this.statusText))
            }
        }
        //开始执行异步请求
        xhr.send()
    }) 
}

//文件路径不存在则会执行请求失败的回调函数
//若promise执行异常 或手动抛出一个异常 onRejected的回调也会被执行
ajax('/api/users.json')
    .then(function onFulfilled (value) {
        console.log('onFulfilled', value);
        return ajax('/error-url')   //不存在一定失败 但并没有被捕获到
    }, function onRejected (error) {
        //promise失败或是异常都会被执行   
        console.log('onRejected', error);
    })

//可以用catch方法来注册promise失败/异常时的回调
ajax('/api/users.json')
    .then(function onFulfilled (value) {
        console.log('onFulfilled', value);
        return ajax('/error-url')   //该异常被捕获到
    }) // => promise {} 
    // catch更为常用 利于链式调用 
    //是给上一个then返回的promise做回调 而不是第一个promise
    //能够捕获到后续then操作中的promise的异常  而上方的例子无法捕获其他then返回的promise异常
    .catch(function onRejected (error) {
        //promise失败或是异常都会被执行 
        console.log('onRejected', error);
    }) 
//不推荐使用      最好是在代码中明确捕获每一个可能的异常
//可以在全局上注册一个unhandledrejection事件 处理代码中没有被手动捕获的promise异常
window.addEventListener('unhandledrejection', event => {
    const { reason, promise } = event
    console.log(reason, promise);

    //reason => Promise 失败原因, 一般是一个错误对象
    // promsie => 出现异常的Promise对象

    event.preventDefault()
},false)


//node当中 process当中注册以下事件
process.on('unhandledRejection', (reason, promise) => {
    console.log(reason, promise);
    //reason => Promise 失败原因, 一般是一个错误对象
    // promsie => 出现异常的Promise对象
})

Promise静态方法

Promise.resolve() - 快速把一个值转换成promise对象

//常用 Promise 静态方法

function ajax (url) {
    return new Promise(function (resolve, reject) {
        //AJAX请求
        var xhr = new XMLHttpRequest()
        //请求方式和地址
        xhr.open('GET', url)
        //请求类型
        //获取结果为json对象而不是字符串
        xhr.responseType = 'json'
        //请求完成过后执行
        xhr.onload = function () {
            if (this.status === 200) {
                //请求成功 返回请求的结果
                resolve(this.response)
            } else {
                //请求失败 返回错误信息 即当前状态文本
                reject(new Error(this.statusText))
            }
        }
        //开始执行异步请求
        xhr.send()
    }) 
}

// 返回状态为fulfilled的Promise对象 作为promise对象的返回值
Promise.resolve('foo')
    .then(function (value) {
        console.log(value);     //'foo'
    })

// //等价于下面的例子
new Promise(function (resolve, reject) {
    resolve('foo')
})

//接收到的为promise对象 则会原样返回该promise对象
var promise  = ajax('/api/users.json')
var promise2 = Promise.resolve(promise)
//返回true 说明Promise.resolve原样返回promise
console.log(promise === promise2);

//传入对象也可以作为一个promise对象被执行
//对象中的then提供了一个接口 可以被then的对象
//利于第三方的promise转换成原生的promise对象
Promise.resolve({
    then: function (onFulfilled, onRejected) {
        onFulfilled('foo')
    }
})
.then(function (value) {
    console.log(value);
})

//用于创建一定是失败的promise对象  传入的参数会作为promise失败的理由
Promise.reject(new Error('rejected'))
    .catch(function (error) {
        console.log(error);
    })

Promise并行执行

Promise也可以提供多个任务并行执行任务

有两个方法可以对promise异步任务并行执行

Promise.all() - 等待所有任务结束后才会结束(若有一个失败,则认定为失败并返回第一个失败的结果)

Promise.race() - 只会等待第一个结束的任务一起结束

ES2020新增Promise.allSettled() - 返回一个在所有给定的promise都已经fulfilled或rejected后的promise,并带有一个对象表格,每个对象表示对应的promise结果。

//Promise  并行执行

function ajax (url) {
    return new Promise(function (resolve, reject) {
        //AJAX请求
        var xhr = new XMLHttpRequest()
        //请求方式和地址
        xhr.open('GET', url)
        //请求类型
        //获取结果为json对象而不是字符串
        xhr.responseType = 'json'
        //请求完成过后执行
        xhr.onload = function () {
            if (this.status === 200) {
                //请求成功 返回请求的结果
                resolve(this.response)
            } else {
                //请求失败 返回错误信息 即当前状态文本
                reject(new Error(this.statusText))
            }
        }
        //开始执行异步请求
        xhr.send()
    }) 
}

//Ajax请求之间相互没有依赖 则可以使用并行执行同时请求

//Promise.all()
//对于多个同步任务的执行 可以使用all方法将多个promise合并为一个promise统一管理
//all方法会返回一个全新的promise对象 只有当all内的promise全部完成 其返回的promise才会完成

var promise = Promise.all([
    ajax('/api/users.json'),
    ajax('/api/users.json'),
    ajax('/api/users.json'),
    ajax('/api/users.json'),
    ajax('/api/users.json'),
])

//promise完成后会返回一个数组,包含all内每个promise执行的结果
//执行中的promise有一个失败了 执行完毕后其也会因失败而结束
promise.then(function (values) {
    console.log(values)
}).catch(function (error ) {
    console.log(error);
})

ajax('/api/urls.json')
    .then(value => {
        console.log(value);
        //将文件内的json对象中可枚举属性值转换成数组
        const urls = Object.values(value)
        //将urls数组内元素逐个执行ajax请求函数并整合成数组
        const tasks = urls.map(url => ajax(url))
        //使用promise并行执行
        return Promise.all(tasks)
    })
    //promise.all并行执行任务的结果
    .then(values => {
        console.log(values);
    })


//Promise.race() 也可以将多个promise对象组合成一个promise对象
//Promise.race() 只会等待第一个结束的任务然后结束
//paromise.all 是等待所有任务结束后才会结束

const request = ajax('/api/posts.json')
const timeout = new Promise((resolve, reject) => {
    //500毫秒过后请求失败
    setTimeout(() => reject(new Error('timeout')), 500)
})

//500毫秒内执行完第一个请求任务可以返回结果  500毫秒过后则未执行完第一个请求任务 返回错误
Promise.race([
    request,
    timeout
])
.then( value => {
    console.log(value);
})
.catch(error => {
    console.log(error);
})

Promise.all() VS Promise.allSettled()

const arr = [Promise.resolve('1'), Promise.reject('2'), Promise.resolve('3')];

Promise.all(arr).then(value => console.log(value)).catch(err=>console.log(err))
// =>  2

Promise.allSettled(arr).then(value => console.log(value)).catch(err=>console.log(err))
/**
 *  [
      { status: "fulfilled", value: "1" },
      { status: "rejected", reason: "2" },
      { status: "fulfilled", value: "3" },
​   ]
*/

Promise执行时序

浏览理解:
宏任务指的是回调队列中的任务,宏任务当中有额外的任务(如绝大部分的异步调用)则会作为一个新的宏任务进到队列中排队

微任务是直接在当前任务结束过后立即执行

// 微任务

//回调队列中的任务称为[宏任务] 宏任务执行的过程中可能会临时加一些额外的需求 可选择作为一个新的宏任务进到队列中排队
//目前绝大部分异步调用都是作为宏任务执行
//微任务 : Promise / MutationObserver /process.nextTick

console.log('global start');

//此处setTimeout为宏任务 会再次回到队列中排队
setTimeout(() => {
    console.log('setTimeout');
}, 0)

//作为当前任务的微任务 直接在当前任务结束过后立即执行  提高整体的响应能力
//Promise的回调还会作为微任务执行
Promise.resolve()
    .then(() => {
        console.log('promise');
    })
    .then(() => {
        console.log('promise 2');
    })
    .then(() => {
        console.log('promise 3');
    })

console.log('global end');

手写模拟Promise

详见本人文章 手写模拟Promise

Generator

Promise虽然拥有链式调用实现串联执行 但代码可读性较低 因此学习更优的异步编程调用的写法

Generator 除用于异步编程外,还用于构造迭代器

Generator 用法

//生成器函数仅在普通函数前多一个 *
function * foo () {
    console.log('start');

    try{
        //可以使用 yield 关键词向外返回一个值
        //返回对象当中还有一个done属性 表示是否全部执行结束 
        //并且不会立即结束该函数foo的执行 而是暂定该函数foo的执行 直到下一个next()操作 才会继续往下执行
        const res = yield 'foo'
        console.log(res);
    } catch (e) {
        console.log(e);
    }
}

//调用生成器函数并不会立即执行 而是得到一个生成器对象
const generator = foo ()

//当调用生成器函数的 next() 方法才会开始执行
const result = generator.next()
console.log(result);

//传入参数时 则会作为 yield 的返回值
// generator.next('bar')

//对生成器内部抛出一个异常 内部再继续往下执行就会得到该异常
generator.throw(new Error('Generator error'))

Generator函数异步方案

// Generator 配合 Promise 的异步方案

const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = require("constants")

function ajax (url) {
    return new Promise(function (resolve, reject) {
        //AJAX请求
        var xhr = new XMLHttpRequest()
        //请求方式和地址
        xhr.open('GET', url)
        //请求类型
        //获取结果为json对象而不是字符串
        xhr.responseType = 'json'
        //请求完成过后执行
        xhr.onload = function () {
            if (this.status === 200) {
                //请求成功 返回请求的结果
                resolve(this.response)
            } else {
                //请求失败 返回错误信息 即当前状态文本
                reject(new Error(this.statusText))
            }
        }
        //开始执行异步请求
        xhr.send()
    }) 
}

function * main () {
    const users = yield ajax('/api/users.json')
    console.log(users);

    const posts = yield ajax('api/posts.json')
    console.log(posts);
}

const g = main()

//此时 yield 返回对象的value就是返回的promise对象
const result = g.next()

//此时执行返回的promise的回调函数 可以拿到promise的执行结果
//使用递归的方式不断迭代 直到生成器函数执行结束过后结束递归
// 这样会无限调用造成地狱回调问题
result.value.then(data => {
    //将得到的结果传给生成器函数 该函数则会继续往下执行 并且传入的数据会作为yield的返回值
    const result2 = g.next(data);

    //查看生成器函数内是否执行完毕 执行完毕则返回不再继续
    if (result2.done) return 

    result2.value.then(data => {
        const result3 = g.next(data);

        //查看生成器函数内是否执行完毕 执行完毕则返回不再继续
        if (result3.done) return 

        result3.value.then(data => {
            g.next(data)
        })
    })
})

Generator异步递归调用优化

// Generator 配合 Promise 的异步方案

const { SSL_OP_NO_SESSION_RESUMPTION_ON_RENEGOTIATION } = require("constants")

function ajax (url) {
    return new Promise(function (resolve, reject) {
        //AJAX请求
        var xhr = new XMLHttpRequest()
        //请求方式和地址
        xhr.open('GET', url)
        //请求类型
        //获取结果为json对象而不是字符串
        xhr.responseType = 'json'
        //请求完成过后执行
        xhr.onload = function () {
            if (this.status === 200) {
                //请求成功 返回请求的结果
                resolve(this.response)
            } else {
                //请求失败 返回错误信息 即当前状态文本
                reject(new Error(this.statusText))
            }
        }
        //开始执行异步请求
        xhr.send()
    }) 
}

function * main () {
    try {
        const users = yield ajax('/api/users.json')
        console.log(users);

        const posts = yield ajax('api/posts.json')
        console.log(posts);
    } catch (e) {
        //捕获执行的异常
        console.log(e);
    }
    
}

// const g = main()

// //递归函数 封装generator
// function handleResult (result) {
//     if (result.done) return  //生成器函数结束

//     //未结束则执行回调函数 将结果传给生成器函数 使其继续执行
//     result.value.then(data => {
//         handleResult(g.next(data))
//         //处理失败的回调错误
//     }, error => {
//         //在生成器内生成一个异常
//         g.throw(error)
//     })
// }

// //生成器函数第一次调用结果
// handleResult(g.next())

//方便复用 可以封装成一个公共的函数 可以用于任一个生成器函数
function co (generator) {
    const g = generator()

    //递归函数
    function handleResult (result) {
        if (result.done) return  //生成器函数结束

        //未结束则执行回调函数 将结果传给生成器函数 使其继续执行
        result.value.then(data => {
            handleResult(g.next(data))
            //处理失败的回调错误
        }, error => {
            //在生成器内生成一个异常
            g.throw(error)
        })
    }

    //生成器函数第一次调用结果
    handleResult(g.next())
}

//有更为完善的co库
co(main)

在异步调用开发中还是常选择async和await的方式

Async/await语法糖

语言层面的异步编程标准 提供扁平化的异步编程函数

await 后的值会包装成promise 对象 而接下来的代码则会放到微任务中执行

// Async / Await 语法糖

function ajax (url) {
    return new Promise(function (resolve, reject) {
        //AJAX请求
        var xhr = new XMLHttpRequest()
        //请求方式和地址
        xhr.open('GET', url)
        //请求类型
        //获取结果为json对象而不是字符串
        xhr.responseType = 'json'
        //请求完成过后执行
        xhr.onload = function () {
            if (this.status === 200) {
                //请求成功 返回请求的结果
                resolve(this.response)
            } else {
                //请求失败 返回错误信息 即当前状态文本
                reject(new Error(this.statusText))
            }
        }
        //开始执行异步请求
        xhr.send()
    }) 
}

//标准的async函数
//await关键词只能出现再async函数内部 不能在外层使用
async function main () {
    try {
        const users = await ajax('/api/users.json')
        console.log(users);

        const posts = await ajax('api/posts.json')
        console.log(posts);
    } catch (e) {
        //捕获执行的异常
        console.log(e);
    }
    
}

//可直接在外面调用该函数
//不需要再配合co这样的执行器 因为是语言层面的异步编程标准
//其次会返回一个promise对象  利于对整体代码进行控制
const promise = main()

promise.then(() => {
    console.log('all comeleted');
})
;