在日常开发中,相信async/await函数对于很多前端开发者而言并不陌生,它是一种语法糖,提供开发者一种以同步代码风格编写异步代码的方式,让编写的代码可读性更强,但是有没有想过在原生不支持这种async环境下是如何实现的呢?下面讲解async/await原理以及在ES5中的实现。
什么是async函数,其实现原理?
MDN上对async函数有以下两个描述:
上面说async函数并不是一个全新的东西,而是能够以更简洁的方式(这种简洁的方式是指以同步代码的形式编写异步代码)写出Promise异步行为,而不是以Promise的链式调用方式实现。以下例子:
function awaitTime(time) {
return new Promise(res => {
setTimeout(res, time);
});
}
// 实现1s后打印 1 second, 再过1s后打印 2 second
// Promise实现
awaitTime(1000).then(() => {
console.log('1 second');
awaitTime(1000).then(() => {
console.log('2 second');
});
})
// async await 实现
async function resolve() {
await awaitTime(1000);
console.log('1 second');
await awaitTime(1000);
console.log('2 second');
}
相比于使用Promise,async会以更直观的方式编写基于Promise的异步行为,那为什么可以以简洁的方式编写异步代码呢? 请看下面这段描述:
上面描述指出async一般情况下(即存在await的情况下),执行到await语法的时候,中断本函数执行,出让进程控制权。等到Promise被resolve或者reject的时候才会恢复进程。
这里有一个关键点:函数执行过程中会保存现场并出让程序控制权。而能够在函数执行过程中保存现场并出让进程控制权的同样有Generator函数,那它们之间有什么关系呢?
在阮一峰的《ES6标准入门》中,是这么介绍async:它是Generator函数的语法糖,实现原理是将Generator函数和自动执行器包装在一个函数里。其中自动执行器的作用是监听Promise状态改变后,触发Generator继续执行。
那么讲解async await的ES5实现可以转为讲解Generator和自动执行器ES5实现。
Generator和自动执行器
Generator
Generator函数的特点是:开发者可以在内部通过yield语句设计多个流程卡点,当函数执行到yield语句的时候,函数会保留现场并中断执行,将进程控制权交由外部。等到外部通过迭代器(该迭代器由函数返回)通知函数继续执行。
function* gen() {
yield 1;
console.log('log 1');
yield 2;
console.log('log 2');
}
const iter = gen();
iter.next();
// log 1;
iter.next();
// log 2;
Generator函数内部有一个指针,指向当前程序运行到哪个yield语句,因为该指针只能从一个yield语句跳到下一个yield语句,所以可以将指针看做是内部状态,每次通过迭代器通知执行的时候,会跳到下一个状态。与之相类似的有状态机,那么是否用状态机来描述Generator呢?
我们看看状态机四个要素Generator是否都具有:
- 状态:状态机内部具有当前状态机所处的状态。而Generator函数内部也会有一个当前内部指针指向当前函数执行到哪里,可以将这个指针看作是一个状态。
- 事件:引起状态机执行某个操作的条件或者口令。Generator函数返回一个迭代器,外部可以通过迭代器向函数通知事件,引起内部执行某个操作。
- 动作:事件发生后执行的一系列处理动作。当外部通知Generator函数内部的时候,函数会执行一段代码,也就是相应的动作。
- 变换:从一个状态变为另外一个状态。当外部向Generator函数发出事件、内部处理完动作后,内部状态指针都会发生变化。
Generator符合状态机的四个要求,那么我们可以用状态机来描述Generator函数内容。下面举个例子:
function* gen(){
console.log("state1");
let state1 = yield "state1";
console.log("state2");
let state2 = yield "state2";
console.log("end");
}
该函数对应的状态机如下:
上图中根据yield语句将gen函数拆分成三个状态,每次外部事件产生(没有表示出来,这部分由自动执行器触发执行)都会触发状态机发生状态,状态变更的时候会执行相应的执行完动作会切换到下一个状态直至到达状态终点。
自动执行器
Generator函数交出进程控制权的时候会携带信息数据,当yield后面的异步任务是基于Promise的时候,可以通过注册then方法使得generator执行下一步任务,那么状态机加上自动执行后流程图如下:
自动执行器实现原理是先拿到generator的迭代器,并且拿到状态机每次切换状态后输出出来的Promise对象,通过注册then方法自动执行下一步,具体的代码实现如下:
function run(gen) {
const iter = gen();
const next = (value) => {
const { value, done } = iter.next(value);
// 当Promise状态改变的时候执行下一步,从而实现自动执行
value.then(res => {
next(res);
})
}
next();
}
从这里看出,ES5实现自动执行器只需要加入一个Promise的Polyfill实现即可。那么现在ES5实现async await这个问题就转变为ES5实现Generator
Generator的ES5实现
ES5想要实现Generator,首先要解决的问题是:如何实现Generator中遇到yield语句的时候,函数内部会停止执行,并且将进程控制权抛出外面。
但ES5实现中,JS进程执行一个函数会同步执行到底,并不能够控制JS进程停止工作,那么并不能通过模拟Generator的保留现场并抛出JS线程控制权。
前面有讲到Generator可以用一个状态机来表示,那么我们是否可以使用状态机的思想来模拟Generator的实现呢?
先找出实现状态机的重要点:
- 状态(对应着Generator内部指针):使用类似于全局变量存储着状态机的状态,这个变量与执行函数无关。
- 暴露事件控制(对应Generator函数返回迭代器):暴露一个与Generator函数执行结果的迭代器
- 动作(对应Generator函数内部yield语句之间的代码):将Generator函数根据yield进行划分成多个代码段,一个代码段代表一个动作。并且使用swich语句将状态与对应的动作联系起来。
- 状态变换(对应着Generator内部指针移动):执行动作后,需要将状态进行切换。
除了上面状态机的四要素,要需要实现一个重要内容:
执行环境上下文:模拟状态机需要多次执行函数,js函数中局部变量的变量存储在栈上,当函数执行完毕后销毁。而Generator却可以保留执行环境,那么模拟执行环境上下文的话,需要将局部变量和状态一样存储到全局变量中,每次执行都会将上下文放到执行函数中。
所以想要实现generator函数的话需要实现以上五点内容。
下面是一段使用babel转换后的代码:
可以看到右侧两个函数是自动执行函数,这个很简单,接下来看看Generator部分是如何转变的:
可以看到箭头标记的内容,从上面我们可以得到以下信息:
- 本地变量采用闭包形式存储,这样能够避免函数执行完毕后局部变量丢失问题,这也就解决了最后一个问题。
- 状态:使用context记录状态;
- 动作:将代码根据await语句切分。
- 变换:每次执行的时候都会根据状态执行对应代码段,并且会切换状态。
但是还少了一个信息,内部的callee函数在什么时候执行,会执行多少次?
通过阅读regenerator的源代码,得出结论是在调用迭代器的next、throw、return方法的时候会调用(这里主要next方法)。这么一来状态机的运转流程就很明显了:
- 根据yield语句将代码分割并生成switch语句,switch语句通过判断状态执行相应的动作,并且会修改状态。
- 采用context存储状态机状态,每次执行迭代器的next方法的时候执行状态机函数。
- 采用闭包方式保存执行上环境下文。
总结
本文内容是讲解async的ES5实现。而async函数底层是generator和自动执行器实现。但是ES5是无法直接实现Generator函数特点,需要借用状态机思想实现。那么最后Async函数的ES5实现图如下:
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!