AI智能
改变未来

深入理解JS的事件循环

本文由 dellyoung 独家授权发布,如果觉得文章有帮助,欢迎点击阅读原文给作者点个赞~

前言

「 本文共 8606 字,预计阅读全文需要 28 分钟 」”
本文将从万物初始讲起JS世界的运转规则,也就是事件循环,在这个过程中你就能明白为什么需要这些规则。有了规则JS世界才能稳稳的运转起来,所以这些规则非常重要,但是你真的了解它们了吗?

阅读本文前可以思考下面几个问题:

  • 你理解中的事件循环是怎样的?
  • 有宏任务了,为什么还要有微任务,它们又有什么关系?
  • promise非常重要,你可以手撕promise/A+规范了吗?
  • async/await底层实现原理是什么?
    本文将会由浅入深的解答这些问题

深入理解JS系列

第一节:深入理解JS的深拷贝
第二节:深入理解JS的原型和原型链
第三节:深入理解JS的事件循环

万物初始

本文基于chromium内核讲解”
刚开始让万物运转是件挺容易的事情,毕竟刚开始嘛,也没什么复杂事,比如有如下一系列任务:

  • 任务1:1 + 2
  • 任务2:3 / 4
  • 任务3:打印出 任务1 和 任务2 结果
    把任务转换成JS代码长这样:
function MainThread() {let a = 1 + 2;let b = 3 / 4;console.log(a + b)}

JS世界拿到这个任务一看很简单啊:首先建一条流水线(一个单线程),然后依次处理这三个任务,最后执行完后撤掉流水线(线程退出)就行了。


现在咱们的事件循环系统很容易就能处理这几个任务了,可以得出:

  • 单线程解决了处理任务的问题:如果有一些确定好的任务,可以使用一个单线程来按照顺序处理这些任务。
    但是有一些问题:

  • 但并不是所有的任务都是在执行之前统一安排好的,很多时候,新的任务是在线程运行过程中产生的
  • 在线程执行过程中,想加入一个新任务,但是现在这个线程执行完当前记录的任务就直接退出了

    世界循环运转

    要想解决上面的问题,就需要引入循环机制,让线程持续运转,再来任务就能执行啦

转换成代码就像这样

function MainThread() {while(true){······}}


现在的JS的事件循环系统就能持续运转起来啦:

  • 循环机制解决了不能循环执行的问题:引入了循环机制,通过一个 while 循环语句,线程会一直循环执行
    不过又有其他问题出现了:

  • 别的线程要交给我这个主线程任务,并且还可能短时间内交给很多的任务。这时候该如何优化来处理这种情况呢?

    任务放入队列

    交给主线程的这些任务,肯定得按一定顺序执行,并且还要得主线程空闲才能做这些任务,所以就需要先将这些任务按顺序存起来,等着主线程有空后一个个执行。

但是如何按顺序存储这些任务呢?

很容易想到用队列,因为这种情况符合队列“先进先出”的特点,也就是说 要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。


有了队列之后,主线程就可以从消息队列中读取一个任务,然后执行该任务,主线程就这样一直循环往下执行,因此只要消息队列中有任务,主线程就会去执行。

我们要注意的是:

  • JavaScript V8引擎是在渲染进程的主线程上工作的
    结果如下图所示:


其实渲染进程会有一个IO线程:IO线程负责和其它进程IPC通信,接收其他进程传进来的消息,如图所示:


咱们现在知道页面主线程是如何接收外部任务了:

  • 如果其他进程想要发送任务给页面主线程,那么先通过 IPC 把任务发送给渲染进程的 IO 线程,IO 线程再把任务发送给页面主线程

到现在,其实已经完成chromium内核基本的事件循环系统了:

  • JavaScript V8引擎在渲染进程的主线程上工作
  • 主线程有循环机制,能在线程运行过程中,能接收并执行新的任务
  • 交给主线程执行的任务会先放入任务队列中,等待主线程空闲后依次调用
  • 渲染进程会有一个IO线程:IO线程负责和其它进程IPC通信,接收其他进程传进来的消息

    完善运转规则

    现在已经知道:页面线程所有执行的任务都来自于任务队列。任务队列是“先进先出”的,也就是说放入队列中的任务,需要等待前面的任务被执行完,才会被执行。

这就导致两个问题了:

  • 如何处理高优先级的任务?
  • 如何处理执行时间长的任务?
    如何解决这两个问题呢?

处理高优先级的任务-微任务
以监听dom变化为例,如果dom变化则触发任务回调,但是如果将这个任务回调放到队列尾部,等到轮到它出队列,可能已经过去一段时间了,影响了监听的实时性。并且如果变化很频繁的话,往队列中插入了这么多的任务,必然也降低了效率。

所以需要一种既能兼顾实时性,又能兼顾效率的方法。

解决方案V8引擎已经给出了:在每个任务内部,开辟一个属于该任务的队列,把需要兼顾实时性和效率的任务,先放到这个任务内部的队列中等待执行,等到当前任务快执行完准备退出前,执行该任务内部的队列。咱们把放入到这个特殊队列中的任务称为微任务。

这样既不会影响当前的任务又不会降低多少实时性。

如图所示以任务1放为例:


可以总结一下:

  • 任务队列中的任务都是宏观任务
  • 每个宏观任务都有一个自己的微观任务队列
  • 微任务在当前宏任务中的JavaScript快执行完成时,也就在V8引擎准备退出全局执行上下文并清空调用栈的时候,V8引擎会检查全局执行上下文中的微任务队列,然后按照顺序执行队列中的微任务。
  • V8引擎一直循环执行微任务队列中的任务,直到队列为空才算执行结束。也就是说在执行微任务过程中产生的新的微任务并不会推迟到下个宏任务中执行,而是在当前的宏任务中继续执行。
    我们来看看微任务怎么产生?在现代浏览器里面,产生微任务只有两种方式。

  • 第一种方式是使用 MutationObserver监控某个DOM节点,然后再通过JavaScript来修改这个节点,或者为这个节点添加、删除部分子节点,当 DOM 节点发生变化时,就会产生 DOM 变化记录的微任务。
  • 第二种方式是使用 Promise,当调用 Promise.resolve()或者 Promise.reject() 的时候,也会产生微任务。
    而常见的宏任务又有哪些呢?

  • 定时器类:setTimeout、setInterval、setImmediate
  • I/O操作:比如读写文件
  • 消息通道:MessageChannel
    并且我们要知道:

  • 宿主(如浏览器)发起的任务称为宏观任务
  • JavaScript 引擎发起的任务称为微观任务
    处理执行时间长的任务-回调
    要知道排版引擎 Blink和JavaScript引擎 V8都工作在渲染进程的主线程上并且是互斥的。”
    在单线程中,每次只能执行一个任务,而其他任务就都处于等待状态。如果其中一个任务执行时间过久,那么下一个任务就要等待很长时间。

如果页面上有动画,当有一个JavaScript任务运行时间较长的时候(比如大于16.7ms),主线程无法交给排版引擎 Blink来工作,动画也就无法渲染来,造成卡顿的效果。这当然是非常糟糕的用户体验。想要避免这种问题,就需要用到回调来解决。

从底层看setTimeout实现

到现在已经知道了,JS世界是由事件循环和任务队列来驱动的。

setTimeout大家都很熟悉,它是一个定时器,用来指定某个函数在多少毫秒后执行。那浏览器是怎么实现setTimeout的呢?

要搞清楚浏览器是怎么实现setTimeout就先要弄明白两个问题:

  • setTimeout任务存到哪了?
  • setTimeout到时间后怎么触发?
  • 取消setTimeout是如何实现的?
    setTimeout任务存到哪了
    首先要清楚,任务队列不止有一个,Chrome还维护着一个延迟任务队列,这个队列维护了需要延迟执行的任务,所以当你通过Javascript调用setTimeout时,渲染进程会将该定时器的回调任务添加到延迟任务队列中。

回调任务的信息包含:回调函数、当前发起时间、延迟执行时间

具体我画了个图:


setTimeout到时间后怎么触发
当主线程执行完任务队列中的一个任务之后,主线程会对延迟任务队列中的任务,通过当前发起时间和延迟执行时间计算出已经到期的任务,然后依次的执行这些到期的任务,等到期的任务全部执行完后,主线程就进入到下一次循环中。具体呢我也画了个图:


ps:为了讲清楚,画配图真的好累哦,点个赞吧!”
到这就清楚setTimeout是如何实现的了:

  • setTimeout存储到延迟任务队列中
  • 当主线程执行完任务队列中的一个任务后,计算延迟任务队列中到期到任务,并执行所有到期任务
  • 执行完所有到期任务后,让出主线程,进行下一次事件循环

    手撕promise

    promise非常重要,新加入的原生api和前端框架都大量使用了promise,promise已然成为前端的“水”和“电”。

promise解决了什么问题呢?promise解决的是异步编码风格的问题。

咱们来看,以前我们的异步代码长这样:

let fs = require(\'fs\');fs.readFile(\'./dellyoung.json\',function(err,data){fs.readFile(data,function(err,data){fs.readFile(data,function(err,data){console.log(data)})})})

层层嵌套,环环相扣,想拿到回调结果已经够费劲了,如果还想进行错误处理。。。那简直太难受了。

而promise出现后,这些问题迎刃而解:

let fs = require(\'fs\');function getFile(url){return new Promise((resolve,reject)=>{fs.readFile(url,function(error,data){if(error){reject(error)}resolve(data)})})}getFile(\'./dellyoung.json\').then(data=>{return getFile(data)}).then(data=>{return getFile(data)}).then(data=>{console.log(data)}).catch(err=>{// 统一错误处理console.log(err)})

简直好用了太多。

可以发现,使用promise解决了异步回调的嵌套调用和错误处理的问题。

大家已经知道promise非常重要了,但是如何完全学会promise呢?手撕一遍promise自然就贯通啦,咱们开始撕,在过程中抽丝剥茧。

promise/A+规范

我们现在想写一个promise,但是谁来告诉怎样才算一个合格的promise,不用担心,业界是通过一个规则指标来实现promise的,这就是Promise / A+,还有一篇翻译可供参考【翻译】Promises / A+规范。

接下来就开始逐步实现吧!

同步的promise

先从一个最简单的promise实现开始

构造函数

先实现promise的地基:初始化用的构造函数

class ObjPromise {constructor(executor) {// promise状态this.status = \'pending\';// resolve回调成功,resolve方法里的参数值this.successVal = null;// reject回调成功,reject方法里的参数值this.failVal = null;// 定义resolve函数const resolve = (successVal) => {if (this.status !== \'pending\') {return;}this.status = \'resolve\';this.successVal = successVal;};// 定义rejectconst reject = (failVal) => {if (this.status !== \'pending\') {return;}this.status = \'reject\';this.failVal = failVal;};try {// 将resolve函数给使用者executor(resolve, reject)} catch (e) {// 执行抛出异常时reject(e)}}}

咱们先写一个constructor用来初始化promise。

接下来分析一下:

  • 调用ObjPromise传入一个函数命名为executor,executor函数接受两个参数resolve、reject,可以理解为分别代表成功时的调用和失败时的调用。executor函数一般长这样(resolve,reject)=>{…}
  • status代表当前promise的状态,有三种\’pending\’、\’resolve\’、\’reject\’(注:从状态机考虑的话还有一个额外的初始状态,表示promise还未执行)
  • successVal和failVal分别代表resolve回调和reject回调携带的参数值
  • 函数resolve:初始化的时候通过作为executor的参数传递给使用者,用来让使用者需要的时候调用,将status状态从\’pending\’改成\’resolve\’
  • 函数reject:初始化的时候通过作为executor的参数传递给使用者,将status状态从\’pending\’改成\’reject\’
  • 你可能还发现了函数resolve和函数reject 里面都有if (this.status !== \’pending\’) {return;},这是因为resolve或reject只能调用一次,也就是status状态只能改变一次。

    then方法

    then方法作用:拿到promise中的resolve或者reject的值。

1.基础版then方法

在class里面放上如下then方法:

then(onResolved, onRejected) {switch (this.status) {case \"resolve\":onResolved(this.successVal);break;case \"reject\":onRejected(this.failVal);break;}}

来分析一下:

  • then方法可以传入两个参数,两个参数都是函数,俩函数就像这样(val)=>{…}
  • 当status状态为\’resolve\’则调用第一个传入的函数,传入的val为successVal
  • 当status状态为\’reject\’则调用第二个传入的函数,传入的val为failVal
    但是then方法还需要支持链式调用的,也就是说可以这样:
new Promise((resolve,reject)=>{resolve(1);}).then((resp)=>{console.log(resp); // 1}).then(()=>{...})

2.使then方法支持链式调用

其实支持链式核心就是then方法要返回一个新的promise,咱们来改造一下实现支持链式调用。

then(onResolved, onRejected) {// 要返回一个promise对象let resPromise;switch (this.status) {case \"resolve\":resPromise = new ObjPromise((resolve, reject) => {try{// 传入的第一个函数onResolved(this.successVal);resolve();}catch (e) {reject(e);}});break;case \"reject\":resPromise = new ObjPromise((resolve, reject) => {try{// 传入的第二个函数onRejected(this.failVal);resolve();}catch (e) {reject(e);}});break;}return resPromise;}

再分析一下:

  • 当status为\’resolve\’时,将promise成功resolve的结果successVal,传递给第一个方法onResolved(),然后执行onResolved8000(this.successVal)函数
  • 当status为\’reject\’时,过程一直,就不多说啦
  • 重点看它们都会把新创建的promise赋值给then方法,执行完后then方法会返回这个新的promise,这样就能实现then的链式调用了
    3.使then方法的链式调用可以传参

但是你没有发现一个问题,我then方法内的第一个参数,也就是onResolved()函数,函数内部的返回值应该是要能够传递给下面接着进行链式调用的then方法的,如下所示:

new Promise((resolve,reject)=>{resolve(1);}).then((resp)=>{console.log(resp); // 1return 2; // <<< 关注这行}).then((resp)=>{console.log(resp); // 2 接受到了参数2})

这该如何实现呢?

其实很简单:

then(onResolved, onRejected) {// 定义这个变量保存要返回的promise对象let resPromise;switch (this.status) {case \"resolve\":resPromise = new ObjPromise((resolve, reject) => {try{// 传入的第一个函数let data = onResolved(this.successVal);resolve(data);}catch (e) {reject(e);}});break;case \"reject\":resPromise = new ObjPromise((resolve, reject) => {try{// 传入的第二个函数let data = onRejected(this.failVal);resolve(data);}catch (e) {reject(e);}});break;}return resPromise;}

很简单:

  • 先保存函数执行的结果,也就是函数的返回值
  • 然后,将返回值传递给新的用来返回的promise的resolve(),就可以将返回值保存到新的promise的successVal
  • 执行出错的话,当然要将错误传递给新的用来返回的promise的reject(),将错误保存到新的promise的failVal
    4.then传入参数处理

再看看这段常见的代码:

new Promise((resolve,reject)=>{resolve(1);}).then((resp)=>{console.log(resp); // 1return 2;}).then((resp)=>{console.log(resp); // 2})

可以看到,then方法的参数可以只传一个,继续来改造:

then(onResolved, onRejected) {const isFunction = (fn) => {return Object.prototype.toString.call(fn) === \"[object Function]\"};onResolved = isFunction(onResolved) ? onResolved : (e) => e;onRejected = isFunction(onRejected) ? onRejected : err => {throw err};······}

分析一下:

  • 判断传入参数的类型是不是函数
  • 传入类型是函数的话,那没毛病,直接用就行
  • 传入类型不是函数的话,那糟糕啦,咱们得分别用(e) => e和(err)=>{throw err}来替换
    到现在promise已经能正常运转啦,代码如下:
class ObjPromise {constructor(executor) {// promise状态this.status = \'pending\';// resolve回调成功,resolve方法里的参数值this.successVal = null;// reject回调成功,reject方法里的参数值this.failVal = null;// 定义resolve函数const resolve = (successVal) => {if (this.status !== \'pending\') {return;}this.status = \'resolve\';this.successVal = successVal;};// 定义rejectconst reject = (failVal) => {if (this.status !== \'pending\') {return;}this.status = \'reject\';this.failVal = failVal;};try {// 将resolve函数给使用者executor(resolve, reject)} catch (e) {// 执行抛出异常时reject(e)}}then(onResolved, onRejected) {const isFunction = (fn) => {return Object.prototype.toString.call(fn) === \"[object Function]\"};onResolved = isFunction(onResolved) ? onResolved : (e) => e;onRejected = isFunction(onRejected) ? onRejected : err => {throw err};// 定义这个变量保存要返回的promise对象let resPromise;switch (this.status) {case \"resolve\":resPromise = new ObjPromise((resolve, reject) => {try{// 传入的第一个函数let data = onResolved(this.successVal);resolve(data);}catch (e) {reject(e);}});break;case \"reject\":resPromise = new ObjPromise((resolve, reject) => {try{// 传入的第二个函数let data = onRejected(this.failVal);resolve(data);}catch (e) {reject(e);}});break;}return resPromise;}}

你可以在控制台运行下面这个测试代码:

new ObjPromise((resolve,reject)=>{resolve(1);}).then((resp)=>{console.log(resp); // 1return 2;}).then((resp)=>{console.log(resp); // 2})

控制台会依次打印出 1 2。

5.then返回值处理

到现在同步promise代码已经没问题啦,但是还不够,因为Promise/A+规定:then方法可以返回任何值,当然包括Promise对象,而如果是Promise对象,我们就需要将他拆解,直到它不是一个Promise对象,取其中的值。

因为status状态为\’resolve\’和\’reject\’时都需要进行这样的处理,所以我们就可以把处理过程封装成一个函数,代码如下:

then(onResolved, onRejected) {···let resPromise;switch (this.status) {case \"resolve\":resPromise = new ObjPromise((resolve, reject) => {try {// 传入的第一个函数let data = onResolved(this.successVal);this.resolvePromise(data, resolve, reject);} catch (e) {reject(e);}});break;case \"reject\":resPromise = new ObjPromise((resolve, reject) => {try {// 传入的第二个函数let data = onRejected(this.failVal);this.resolvePromise(data, resolve, reject);} catch (e) {reject(e);}});break;}return resPromise;}// data为返回值// newResolve为新的promise的resolve方法// newReject为新的promise的reject方法resolvePromise(data, newResolve, newReject) {// 判断是否是promise,不是直接resolve就行if(!(data instanceof ObjPromise)){return newResolve(data)}try {let then = data.then;const resolveFunction = (newData) => {this.resolvePromise(newData, newResolve, newReject);};const rejectFunction = (err) => {newReject(err);};then.call(data, resolveFunction, rejectFunction)} catch (e) {// 错误处理newReject(e);}}

分析一下:

  • 判断返回值类型,当不是promise时,直接resolve就行
  • 当是promise类型时,用this.resolvePromise(newData, newResolve, newReject)来递归的调用then方法,直到data不为promise,然后resolve结果就行啦
    6.解决then返回值循环引用

现在又有问题了:

如果新的promise出现循环引用的话就永远也递归不到头了

看看执行下面这个代码:

let testPromise = new ObjPromise((resolve, reject) => {resolve(1);})let testPromiseB = testPromise.then((resp) => {console.log(resp); // 1return testPromiseB;})

会报错栈溢出。

解决这个问题的方法就是:通过给resolvePromise()方法传递当前新的promise对象,判断当前新的promise对象和函数执行返回值不同就可以了

class ObjPromise {constructor(executor) {// promise状态this.status = \'pending\';// resolve回调成功,resolve方法里的参数值this.successVal = null;// reject回调成功,reject方法里的参数值this.failVal = null;// 定义resolve函数const resolve = (successVal) => {if (this.status !== \'pending\') {return;}this.status = \'resolve\';this.successVal = successVal;};// 定义rejectconst reject = (failVal) => {if (this.status !== \'pending\') {return;}this.status = \'reject\';this.failVal = failVal;};try {// 将resolve函数给使用者executor(resolve, reject)} catch (e) {// 执行抛出异常时reject(e)}}resolvePromise(resPromise, data, newResolve, newReject) {if (resPromise === data) {return newReject(new TypeError(\'循环引用\'))}if (!(data instanceof ObjPromise)) {return newResolve(data)}try {let then = data.then;const resolveFunction = (newData) => {this.resolvePromise(resPromise, newData, newResolve, newReject);};const rejectFunction = (err) => {newReject(err);};then.call(data, resolveFunction, rejectFunction)} catch (e) {// 错误处理newReject(e);}}then(onResolved, onRejected) {const isFunction = (fn) => {return Object.prototype.toString.call(fn) === \"[object Function]\"};onResolved = isFunction(onResolved) ? onResolved : (e) => e;onRejected = isFunction(onRejected) ? onRejected : err => {throw err};// 定义这个变量保存要返回的promise对象let resPromise;switch (this.status) {case \"resolve\":resPromise = new ObjPromise((resolve, reject) => {try {// 传入的第一个函数let data = onResolved(this.successVal);this.resolvePromise(resPromise, data, resolve, reject);} catch (e) {reject(e);}});break;case \"reject\":resPromise = new ObjPromise((resolve, reject) => {try {// 传入的第二个函数let data = onRejected(this.failVal);this.resolvePromise(resPromise, data, resolve, reject);} catch (e) {reject(e);}});break;}return resPromise;}}

可以在控制台中调用如下代码试试啦:

new ObjPromise((resolve, reject) => {resolve(1);}).then((resp) => {console.log(resp); // 1return 2}).then((resp) => {console.log(resp); // 2return new ObjPromise((resolve, reject) => {resolve(3)})}).then((resp) => {console.log(resp); // 3});

控制台会一次打印出 1 2 3

异步的promise

现在咱们实现了同步版的promise,但是很多情况下,promise的resolve或reject是被异步调用的,异步调用的话,执行到then()方法时,当前的status状态还是\’pending\’。这该如何改进代码呢?

思路其实很简单:

  • 设置两个数组,分别存起来then()方法的回调函数onResolved和onRejected
  • 当等到调用了resolve或者reject时,执行对应数组内存入的回调函数即可
  • 另外为了保证执行顺序,等待当前执行栈执行完成,咱们还需要给constructor的resolve和reject函数里面使用setTimeout包裹起来,避免影响当前执行的任务。
    根据这个思路来改造一下promise:
class ObjPromise {constructor(executor) {// promise状态this.status = \'pending\';// resolve回调成功,resolve方法里的参数值this.successVal = null;// reject回调成功,reject方法里的参数值this.failVal = null;// resolve的回调函数this.onResolveCallback = [];// reject的回调函数this.onRejectCallback = [];// 定义resolve函数const resolve = (successVal) => {setTimeout(()=>{if (this.status !== \'pending\') {return;}this.status = \'resolve\';this.successVal = successVal;//执行所有resolve的回调函数this.onResolveCallback.forEach(fn => fn())})};// 定义rejectconst reject = (failVal) => {setTimeout(()=>{if (this.status !== \'pending\') {return;}this.status = \'reject\';this.failVal = failVal;//执行所有reject的回调函数this.onRejectCallback.forEach(fn => fn())})};try {// 将resolve函数给使用者executor(resolve, reject)} catch (e) {// 执行抛出异常时reject(e)}}// data为返回值// newResolve为新的promise的resolve方法// newReject为新的promise的reject方法resolvePromise(resPromise, data, newResolve, newReject) {if (resPromise === data) {return newReject(new TypeError(\'循环引用\'))}if (!(data instanceof ObjPromise)) {return newResolve(data)}try {let then = data.then;const resolveFunction = (newData) => {this.resolvePromise(resPromise, newData, newResolve, newReject);};const rejectFunction = (err) => {newReject(err);};then.call(data, resolveFunction, rejectFunction)} catch (e) {// 错误处理newReject(e);}}then(onResolved, onRejected) {const isFunction = (fn) => {return Object.prototype.toString.call(fn) === \"[object Function]\"};onResolved = isFunction(onResolved) ? onResolved : (e) => e;onRejected = isFunction(onRejected) ? onRejected : err => {throw err};// 定义这个变量保存要返回的promise对象let resPromise;switch (this.status) {case \"resolve\":resPromise = new ObjPromise((resolve, reject) => {try {// 传入的第一个函数let data = onResolved(this.successVal);this.resolvePromise(resPromise, data, resolve, reject);} catch (e) {reject(e);}});break;case \"reject\":resPromise = new ObjPromise((resolve, reject) => {try {// 传入的第二个函数let data = onRejected(this.failVal);this.resolvePromise(resPromise, data, resolve, reject);} catch (e) {reject(e);}});break;case \"pending\":resPromise = new ObjPromise((resolve, reject) => {const resolveFunction = () => {try {// 传入的第一个函数let data = onResolved(this.successVal);this.resolvePromise(resPromise, data, resolve, reject);} catch (e) {reject(e);}};const rejectFunction = () => {try {// 传入的第二个函数let data = onRejected(this.failVal);this.resolvePromise(resPromise, data, resolve, reject);} catch (e) {reject(e);}};this.onResolveCallback.push(resolveFunction);this.onRejectCallback.push(rejectFunction);});break;}return resPromise;}}

可以用下面代码测试一下:

new ObjPromise((resolve, reject) => {setTimeout(() => {resolve(1);}, 100)}).then((resp) => {console.log(resp); // 1return 2}).then((resp) => {console.log(resp); // 2return new ObjPromise((resolve, reject) => {resolve(3)})}).then((resp) => {console.log(resp); // 3});

我们现在已经基本完成了Promise的then方法啦。

完善promise

到现在已经完成了promise最核心的两个方法:constructor方法和then方法。不过Promise/A+还规定了一些其他的方法,咱们继续来完成。

catch方法

catch()方法就是可以通过回调函数拿到reject的值,这个好办,其实then方法已经实现了,转接一下then方法就行了:

catch(onRejected) {return this.then(null, onRejected)}

这样就实现了catch()方法

Promise.resolve()/reject()方法

大家肯定都见过Promise.resolve()或者Promise.resolve()用法。其实作用就是返回一个新的promise,并且内部调用resolve或者reject。

ObjPromise.resolve = (val) => {return new ObjPromise((resolve, reject) => {resolve(val)})};ObjPromise.reject = (val) => {return new ObjPromise((resolve, reject) => {reject(val)})};

通过这两种方法,咱们可以将现有的数据很方便的转换成promise对象

all方法
all方法也是很常用的方法,它可以传入promise数组,当全部resolve或者有一个reject时,执行结束,当然返回的也是promise对象,来实现一下。

ObjPromise.all = (arrPromise) => {return new ObjPromise((resolve, reject) => {// 传入类型必须为数组if(Array.isArray(arrPromise)){return reject(new TypeError(\"传入类型必须为数组\"))}// resp 保存每个promise的执行结果let resp = new Array(arrPromise.length);// 保存执行完成的promise数量let doneNum = 0;for (let i = 0; arrPromise.length > i; i++) {// 将当前promiselet nowPromise = arrPromise[i];if (!(nowPromise instanceof ObjPromise)) {return reject(new TypeError(\"类型错误\"))}// 将当前promise的执行结果存入到then中nowPromise.then((item) => {resp[i] = item;doneNum++;if(doneNum === arrPromise.length){resolve(resp);}}, reject)}})};

来分析一下:

  • 传入promise数组,返回一个新的promsie对象
  • resp用来保存所有promise的执行结果
  • 用instanceof来判断是否是promise类型
  • 通过调用每个promise的then方法拿到返回值,并且要传入reject方法
  • 用doneNum来保存执行完成的promise数量,全部执行完后,通过resolve传递执行结果resp,并且将当前promise状态改为\’resolve\’,后续就可以通过then方法取值

    race方法

    race方法也偶尔会用到,它可以传入promise数组,当哪个promise执行完,则race就直接执行完,咱们来实现一下:

ObjPromise.race = (arrPromise) => {return new Promise((resolve, reject) => {for (let i = 0; arrPromise.length > i; i++) {// 将当前promiselet nowPromise = arrPromise[i];if (!(nowPromise instanceof ObjPromise)) {return reject(new TypeError(\"类型错误\"))};nowPromise.then(resolve, reject);}})};

来分析一下:

  • 传入promise数组,返回一个新的promsie对象
  • 用instance来判断是否是promise类型
  • 调用每个promise的then方法,并传递resolve、reject方法,哪个先执行完就直接结束了,后续就可以通过then方法取值
    OK,到现在已经实现了一个自己的promise对象!

从底层看async/await实现

手撕完promise,趁热再深入学习一下ES7的新特性async/await。async/await相当牛逼:它是JavaScript 异步编程的一个重大改进,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力,并且使得代码逻辑更加清晰。接下来咱们就来深入了解下async/await为什么能这么牛逼。

async/await使用了Generator和Promise两种技术,Promise咱们已经掌握了,所以要再看一看Generator到底是什么。

生成器Generator
先了解一下生成器Generator是如何工作的,接着再学习Generator的底层实现机制——协程(Coroutine)

如何工作
生成器函数:生成器函数是一个带星号函数,而且是可以暂停执行和恢复执行的

先来看下面这段代码:

function* genFun() {console.log(\"第一段\")yield \'generator 1\'console.log(\"第二段\")return \'generator 2\'}console.log(\'begin\')let gen = genFun()console.log(gen.next().value)console.log(\'main 1\')console.log(gen.next().value)console.log(\'main 2\')

执行这段代码,你会发现gen并不是一次执行完的,而是全局代码和gen代码交替执行。这其实就是生成器函数的特性,它可以暂停执行,也可以恢复执行。

再来看下,它是具体是怎么暂停执行和恢复执行的:

  • 在生成器函数内部执行一段代码,如果遇到yield关键字,那么JavaScript引擎将返回关键字后面的内容给外部,并暂停该函数的执行。
  • 外部函数可以通过next方法恢复生成器函数的执行。
    但是JavaScript引擎V8是如何实现生成器函数的暂停和恢复呢,接着往下看

生成器原理

想要搞懂生成器函数如何暂停和恢复,要先了解一下协程的概念,协程是一种比线程更加轻量级的存在,可以把协程看成是跑在线程上的任务:

  • 一个线程上可以存在多个协程,但是在线程上同时只能执行一个协程。
  • 如果从 A 协程启动 B 协程,我们就把 A 协程称为 B 协程的父协程。
  • 一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。
  • 协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态执行)。因此协程在性能上要远高于线程。
    小知识点:线程 核心态,协程 用户态。也就是说线程被内核调度,协程是由用户的程序自己调度,系统并不知道有协程的存在”
    下面我画了个图来演示上面代码的执行过程:


从图中结合代码可以看出协程的规则:

  • 通过调用生成器函数genFun来创建一个协程gen,创建之后,gen协程并没有立即执行。
  • 要让gen协程执行,需要通过调用gen.next()。
  • 当协程正在执行的时候,可以通过yield关键字来暂停gen协程的执行,并返回主要信息给父协程。
  • 如果协程在执行期间,遇到了return,那么JavaScript引擎会结束当前协程,并将return后面的内容返回给父协程。
    其实规则总的来说:

  • 父协程中执行next(),线程控制权就让给子协程了
  • 子协程中遇到yield,线程控制权就让给父协程了
  • 可以看出父协程和子协程还是互相谦让的
    但是用Generator生成器还是不太好用,我们希望写代码的时候,不要手动控制协程之间的切换,该切换时,JavaScript引擎帮我直接切换好多省事。这时候async/await就登场啦!

再看async/await
已经知道,async/await使用了Generator和Promise两种技术,其实往低层说就是微任务和协程的应用。现在Generator和Promise都已经深入理解啦。但是微任务和协程是如何协作实现了async/await呢?

  1. async是什么:

MDN:async是一个通过异步执行并隐式返回Promise作为结果的函数。”
可以执行下面代码:

async function foo() {return 1}console.log(foo())  // Promise {<resolved>: 1}

可以看到调用async声明的foo()函数返回了一个Promise对象,并且状态是resolved。

  1. await是什么:

MDN:await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。
若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。
若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。”
先来看下面这段代码:

async function foo() {console.log(1)let a = await 99console.log(a)}console.log(0)foo()console.log(3)

想要知道上面这段代码执行结果如何,就先看看这段代码的执行流程图,我已经画出来了:

结合上面这张流程图,分析一下上面代码的执行过程:

  • 首先,执行console.log(0)这个语句,打印出来0。
  • 由于foo函数是被async标记过的,所以当进入该函数的时候,JavaScript 引擎会保存父协程调用栈等信息,然后切换到子协程,执行foo函数中的console.log(1)语句,并打印出 1。
  • 当执行到await 99时,会默认创建一个 Promise 对象,如下:
let newPromise = new Promise((resolve,reject){resolve(99)})

并且在创建的过程中遇到了resolve(99),JavaScript引擎会将该任务推入微任务队列。

  • 然后JavaScript引擎暂停当前子协程的执行,将主线程控制权交给父协程。并且还会把这个新创建的Promise返回给父协程
  • 父协程拿到主线程控制权后,首先调用newPromise.then,把回调函数放入到Promise中,这个回调函数是什么?其实就是相当于生成器函数的next(),调用这个回调函数会调用next(),会将父协程的控制权再交给子协程。
  • 接下来继续执行父协程的流程,这里执行console.log(3),并打印出来3。
  • 之后父协程将执行结束,在结束之前,会进入微任务的检查点,检查微任务,然后执行微任务队列,微任务队列中有resolve(99)的任务等待执行。
  • 执行resolve(99),触发了之前存入的回调函数,回调函数内有next(),父协程的控制权再交给子协程,并同时将 value值99传给该子协程。
  • 子协程foo激活之后,会把接收到的value值99赋给了变量a,然后foo协程执行console.log(a),打印出99,执行完成之后,将控制权归还给父协程。
    上面的就是async/await详细的执行过程啦,可以看出JavaScript引擎帮我们做了好多工作,才能让我们将异步代码写成同步代码的格式。

参考

  • 浏览器工作原理与实践
  • Promise之你看得懂的Promise
  • MDN-async
  • MDN-await

    小结

  • 从零开始了解了JS世界的事件循环机制
  • 明白了为什么会有微任务,以及宏任务与微任务的关系
  • 掌握了如何手撕符合Promise/A+规范的Promise
  • 知道async/await使用了Generator和Promise两种技术,也就是说它是微任务和协程的应用

相关热门推荐

这一次,彻底弄懂 Promise 原理
面试题:说说事件循环机制(满分答案来了)
async/await 原理及执行顺序分析

最后

  • 欢迎加我微信(winty230),拉你进技术群,长期交流学习…
  • 欢迎关注「前端Q」,认真学前端,做个专业的技术人…
赞(0) 打赏
未经允许不得转载:爱站程序员基地 » 深入理解JS的事件循环