采用Symbol和process.nextTick实现Promise
作者简介: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实现的核心要点如下。
- 设置Promise的Symbol、Promise构造函数。
- Promise状态从pending改变的处理逻辑PromiseHandle。
- 形成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
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- U盘上安装多个Linux发行版和PE
- ubuntu18.04部署python3、nginx项目
- CentOS8.x系统配置记录
- js根据经纬度换算行驶里程
- ubuntu18.04 安装docker
- COBBLER无人值守批量安装系统.md
- 使用VSCode 打包你的第一个flutter应用(安卓篇)
- KICKSTART无人值守批量安装系统.md
- Centos7-Firewall防火墙基础讲解
- 优酷iOS插件化页面架构方法
- 处理一次k8s、calico无法分配podIP的心路历程
- 小视频源码,按返回键两次退出
- iOS音视频接入 - TRTC多人音视频通话
- Android平台RTMP推流或轻量级RTSP服务(同屏或摄像头)编码前数据接入类型总结
- 接口测试框架实战(二) | 搞定多环境下的接口测试