采用Symbol和process.nextTick实现Promise

时间:2022-04-25
本文章向大家介绍采用Symbol和process.nextTick实现Promise,主要内容包括V8 Promise的实现、实现Promise的主逻辑、定义microtask算法、定义constructor、定义prototype.then和prototype.catch、阶段性总结、结语、基本概念、基础应用、原理机制和需要注意的事项等,并结合实例形式分析了其使用技巧,希望通过本文能帮助到大家理解应用这部分内容。

作者简介:slashhuang 研究型程序员 现就职于爱屋吉屋

Promise已经成为处理Node.js异步流程的标配技术。

V8的async/await语法构筑在Promise之上、处理generator的co模块基于Promise实现。

处理http请求的axios、gulp4的构建流程、主流的测试框架mocha/ava等等都围绕Promise为开发者量身打造。

Promise的核心特点在于异步流程chaining、状态存储、then/catch条件分支明确、microtask处理等等。

为了对异步流程的处理更有把控力,笔者借鉴了V8的Promise.js源码和Promise A+的开源社区的实现,自己写了个Promise实现。

在数据结构上采用了链表模拟callback queue,在算法上面采用了process.nextTick来模拟microtask,在私有方法模拟上采用Symbol来实现。

下面我先给大家介绍下V8层面的Promise的实现,再分享下个人对Promise的实现方案。

V8 Promise的实现

下载完Node.js源码后,可以看到Promise的代码位置在deps/v8/src/js/promise.js。

在同一个文件夹下面还有个macros.py文件用来定义JS数据类型的方法和js到C++层面的counter。

基本可以认为macros.py只是个简单的bridge和util,这边不进行特别的讨论。

打开promise.js文件,基本可以看到两块语法,一块是标准的JS语法,一块是在JS层面添加%标示C++实现的代码。

后者的作用主要是在C++逐语句执行JS做的hook,将JS的control flow夺取过来。

Node.js中V8对promise.js实现的核心要点如下。

  1. 设置Promise的Symbol、Promise构造函数。
  2. Promise状态从pending改变的处理逻辑PromiseHandle。
  3. 形成Promise的数据结构NewPromiseCapability。

由于是V8环境的js代码,所以promise.js的实现是function的堆叠,而且异步逻辑用%做了c++的hook,所以整体上不是特别适合阅读。

既然理顺了V8的Promise展示的主要逻辑点,我就顺藤摸瓜写了个easy模式的Promise,核心逻辑大概130行。

实现Promise的主逻辑

我们谋定后动,先做好数据结构和算法的选型。

定义数据结构

我们先看一个基本的使用形式

//经过100ms,改变p的状态为fulfilled、值为1let p = new Promise((res,rej)=>setTimeout(res,100,1));//100ms后,打印1,pNext状态为fulfilled、值为1let pNext = p.then(console.log)

如上是个简单的Promise使用范例。

要做到fn能够100ms后执行,p.then的作用势必只是将fn存储起来而已供100ms后调用。

同时fn的执行时机和p强挂钩,所以p和pNext存在引用接口。

针对以上功能点,then的方法逻辑基本要定如下的数据结构。

p能够拿到pNext的引用 pNext提供了接口给p,当p状态改变的时候执行这个接口进行通知。 如上是我们的基本数据结构,很多同学可能会觉得很像pub/sub设计模式。

但是从数据结构的角度看,用链表来描述更贴切。

下面我们看下算法层面,如何实现microtask和接口通知。

定义microtask算法

如上由于then/catch的执行是一个microtask机制,因此要采用一个异步api模拟这种能力。在浏览器环境可以选型mutationObserver,在Node环境我这边是采用的process.nextTick。

到这边,算法和数据结构选型定的差不多了,下面我们就从Promise的constructor、then、catch来实现Promise。

定义constructor

constructor在形式上有一个executor函数参数,executor的参数是resolve/reject。

我们将resolve/reject定义在同一个namespace下面,并采用Symbol定义它们的方法名。

Symbol的主要好处是不可enumerate,这里不做过多讨论。

class Promise{    constructor(executor){    if(typeof executor!=='function'){        throw new TypeError(`${executor} is not a function`)    };    let resolveFn = val=>this[resolveSymbol](val);    let rejectFn = error=>this[rejectSymbol](error);    defineProperty(this,stateSymbol,pendingState)    try{       executor(resolveFn,resolveFn)    }catch(err){          rejectFn(err)    }   }   [resolveSymbol](val){    defineProperty(this,stateSymbol,fulfillState);    this.PromiseVal =  val;   }   [rejectSymbol](error){    defineProperty(this,stateSymbol,rejectState);    this.PromiseVal =  error;   }}

如上即为我们的第一步,基本上和Promise的构造函数使用方式保持了一致。

在resolve或者reject函数执行的时候,执行的功能是修改this的stateSymbol来标明它的状态是fulfilled还是rejected.

关于代码中的defineProperty,就是个简单的Obj[prop]=val的细致版本,为了专注主逻辑,这边也不多解释。

定义完constructor和resolve/reject函数后,我们就要考虑prototype.then/catch的逻辑了。

定义prototype.then和prototype.catch

then和catch要做两件事,第一件是存储microtask,另一件是如果状态不为pending要autoRun。

由于then和catch只是一个处理fulfill,一个处理reject而已,而且then如果有第二个参数也可以兼容catch的处理逻辑。

所以我把then和catch的逻辑归为一类,并定义[nextThenCatchSymbol]方法来处理。

class SuperPromise{    constructor(executor){    if(typeof executor!=='function'){      throw new TypeError(`${executor} is not a function`)    };    let resolveFn = val=>this[resolveSymbol](val);    et rejectFn = error=>this[rejectSymbol](error);    defineProperty(this,stateSymbol,pendingState)    try{       executor(resolveFn,resolveFn)    }catch(err){       rejectFn(err)    }    }   [resolveSymbol](val){    defineProperty(this,stateSymbol,fulfillState);    this.PromiseVal =  val;    this.RunLater()   }  [rejectSymbol](error){    defineProperty(this,stateSymbol,rejectState);    this.PromiseVal =  error;    this.RunLater()  } [nextThenCatchSymbol](fnArr,type){    //将then和catch方法归为一类    let method = 'resolve';    let resolveFn = fnArr[0];    let rejectFn = fnArr[1];    if(type=='catch'){        method = 'catch';        rejectFn = fnArr[0];    };    return new Promise((res,rej)=>{}) } then(fn,fn1){    return this[nextThenCatchSymbol]([fn,fn1],'resolve') } catch(fn){    return [nextThenCatchSymbol]([fn],'reject') }}

如上[nextThenCatchSymbol]返回了一个空的Promise,没有定义接口也没有任何功能。为了实现chaining,必须给这个空Promise定义接口,同时将它添加进chain上一级的microtask.

于是改造这个function如下

[nextThenCatchSymbol](fnArr,type){    let method = 'resolve';    let resolveFn = fnArr[0];    let rejectFn = fnArr[1];    if(type=='catch'){        method = 'catch';        rejectFn = fnArr[0];    };    //返回新的Promise,pending状态    let newPromise =  new SuperPromise((resolve,reject)=>{});    //添加对外接口    newPromise[resolveFnSymbol]=function(val){        let nextValue = resolveFn(val);        if(nextValue instanceof SuperPromise){            nextValue.then(val=>{                this[resolveSymbol](val)            })        }else{            this[resolveSymbol](nextValue)        }    }    newPromise[rejectFnSymbol]=function(val){        let nextValue = rejectFn(val);        if(nextValue instanceof SuperPromise){            nextValue.catch(val=>{                this[rejectSymbol](val)            })        }else{            this[rejectSymbol](nextValue)        }    }    //在上个Promise内部注册microtask    this.microtask = {        newPromise     };    //microtask异步执行    this.RunLater();    return newPromise}

如上我们手动给newPromise指定了两个接口[rejectFnSymbol],[resolveFnSymbol],

同时在上个Promise实例上挂了microtask,并立即执行了Runlater。

写到这里,大部分的数据结构已经完成,接下来就是Runlater方法的实现。

let RunLater = process.nextTick;RunLater(){    if(!this.microtask){        return     }    let state =  this[stateSymbol];    let PromiseVal = this.PromiseVal;    let { newPromise } = this.microtask;    let hookFn= '';    if(state == fulfillState || state == rejectState){    hookFn = state == fulfillState?resolveFnSymbol:rejectFnSymbol;    RunLater(()=>newPromise[hookFn](PromiseVal))    }}

RunLater的逻辑很简单,就是根据当前的promise的情况来决定是执行resolve还是reject的逻辑而已。

阶段性总结

写到这边,大部分的逻辑都已经实现完毕。

我们接下来看下如何实现state存储和多级嵌套。

当then(fn)执行的时候,如果是个普通值就直接把promise的值改为那个值即可。

如果fn执行返回的是一个Promise,我们必须把当前的Promise挂钩在返回的Promise上面。

要实现后者其实很简单,只需要将当前的Promise挂在fn()的结果后面接口实现依赖关系的转换。fn().then(val=>this[resolveSymbol])

newPromise[resolveFnSymbol]=function(val){    let nextValue = resolveFn(val);    if(nextValue instanceof SuperPromise){        nextValue.then(val=>{            this[resolveSymbol](val)        })    }else{        this[resolveSymbol](nextValue)    }}

写到这里,Promise的chaining , microTask,state track等基本已经实现完毕。

有兴趣的同学,可以看我的源码实现Promise实现

结语

Promise对于Node.js开发者来说是个技术标配,研究它对于异步处理技巧的理解也是大有好处。

希望通过本文能够给大家一个直观的Promise实现机理。

接下来几篇文章,我将参考Tj大神的co重新写一个co,同时对Node.js内部bootstrap过程分项下个人心得。

作者专栏:https://zhuanlan.zhihu.com/slashhuang