前面我们讲到了宏任务和微任务,我们先来回顾一下:
JavaScript 异步编程
由于 JavaScript 是单线程执行模型,因此必须支持异步编程才能提高运行效率。异步编程的语法目标是让异步过程写起来像同步过程。
运行机制
- 在执行栈中执行一个宏任务。
- 执行过程中遇到微任务,将微任务添加到微任务队列中。
- 当前宏任务执行完毕,立即执行微任务队列中的任务。
- 当前微任务队列中的任务执行完毕,检查渲染,GUI线程接管渲染。
- 渲染完毕后,js线程接管,开启下一次事件循环,执行下一次宏任务(事件队列中取)。
我们常常采用Promise来将回调函数改成链式调用,如果不转换,可能多个回调函数嵌套最终形成回调地狱,值得强调的是,即使采用了Promsie,但最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆 then,原来的语义变得很不清楚。
const fs = require('fs')
const readFileWithPromise = file => {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) {
reject(err)
} else {
resolve(data)
}
})
})
}
readFileWithPromise('/etc/passwd')
.then(data => {
console.log(data.toString())
return readFileWithPromise('/etc/profile')
})
.then(data => {
console.log(data.toString())
})
.catch(err => {
console.log(err)
})
这个时候为了解决Promise的问题,ES7中async和await就被提出来了。
async、await
先说一下async的用法,它作为一个关键字放在函数前面,用于表示函数是一个异步函数,因为async就是异步的意思,异步函数意味着该函数的执行不会阻塞后面代码的运行。
const fs = require('fs')
async function readFile() {
try {
var f1 = await readFileWithPromise('/etc/passwd')
console.log(f1.toString())
var f2 = await readFileWithPromise('/etc/profile')
console.log(f2.toString())
} catch (err) {
console.log(err)
}
}
async和await 函数写起来跟同步函数一样,条件是需要接收 Promise 或原始类型的值。异步编程的最终目标是转换成最容易理解的形式。
分析 async、await 实现原理之前,先介绍下知识:
Generator
Generator是ES6标准引入的新的数据类型。Generator可以理解为一个状态机,内部封装了很多状态,同时返回一个迭代器Iterator对象。可以通过这个迭代器遍历相关的值及状态。
Generator的显著特点是可以多次返回,每次的返回值作为迭代器的一部分保存下来,可以被我们显式调用。
Generator 的基本用法:
function* foo(x) {
yield x + 1;
yield x + 2;
return x + 3;
}
const result = foo(0) // foo {<suspended>}
result.next(); // {value: 1, done: false}
result.next(); // {value: 2, done: false}
result.next(); // {value: 3, done: true}
result.next(); //{value: undefined, done: true}
在这个例子里,我们可以看到,在执行foo函数后返回了一个 Generator函数的实例。它具有状态值suspended和closed,suspended代表暂停,closed则为结束。但是这个状态是无法捕获的,我们只能通过Generator函数的提供的方法获取当前的状态。 在执行next方法后,顺序执行了yield的返回值。返回值有value和done两个状态。value为返回值,可以是任意类型。done的状态为false和true,true即为执行完毕。在执行完毕后再次调用返回{value: undefined, done: true}
注意:在遇到return的时候,所有剩下的yield不再执行,直接返回{ value: undefined, done: true }
Generator函数的方法
Generator函数提供了3个方法:next / return / throw
- next方式是按步执行,每次返回一个值,同时也可以每次传入新的值作为计算
- return则直接跳过所有步骤,直接返回 {value: undefined, done: true}
- throw则根据函数中书写try catch返回catch中的内容,如果没有写try,则直接抛出异常。
run函数
有了前面关于 Generator 知识的复习,我们实现本文的核心函数;
它接收一个 Generator 函数 fn 作为参数,它的功能就是模拟 async/await 机制运行该 fn 函数。
function run(fn) {
const gen = fn()
function _next(data){
const { value, done } = gen.next(data)
if(done) return value
if(value.then){
value.then(data => _next(data))
}else{
_next(value)
}
}
_next()
}
看了这个示例代码,应该就能明白 run() 的作用了,实际上通过这个函数,我们已经模拟实现了 async/await 机制;
我们做个不严谨的类比来说明:
- function * work() { } 中间的 * 号就相当于 async 关键字的作用;
- yield 关键字就相当于 await 关键字;
实际上 async/await 关键字也可以认为是 ES7 的语法糖,我们已经通过 run() 函数来模拟了其工作原理,所以这个时候它的基本原理也显而易见了。
async&await实现原理
-
async(异步)
- 自动将常规函数转换成Promise,返回值也是一个Promise对象,Promise对象的状态值是resolved(成功的)
-
await(等待)
- 只能放在async函数里,await 强制后面的代码等待,直到Promise对象resolve,得到resolve的值作为await表达式的运算结果 。
-
使用async/await的时候,是无法捕获错误的,任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行,如果想捕获错误,需要使用es5中的try&catch,来进行错误的捕获 。
-
async 会将其后的函数(函数表达式或 Lambda)的返回值封装成一个 Promise 对象,而 await 会等待这个 Promise 完成,并将其 resolve 的结果返回出来。
使用async&await实现异步请求
想要Generator函数执行下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式或return语句。由此可见,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。