前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >采用Symbol和process.nextTick实现Promise

采用Symbol和process.nextTick实现Promise

作者头像
前端黑板报
发布2018-01-29 16:45:26
7250
发布2018-01-29 16:45:26
举报
文章被收录于专栏:前端黑板报前端黑板报

作者简介: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的主逻辑

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

定义数据结构

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

代码语言:javascript
复制
//经过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,这里不做过多讨论。

代码语言:javascript
复制
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]方法来处理。

代码语言:javascript
复制
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如下

代码语言:javascript
复制
[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方法的实现。

代码语言:javascript
复制
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])

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-03-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端黑板报 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • V8 Promise的实现
  • 实现Promise的主逻辑
  • 定义microtask算法
  • 定义constructor
  • 定义prototype.then和prototype.catch
  • 阶段性总结
  • 结语
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档