手写Promsie的话题已不新鲜,今天不会延续审美疲劳,来一点新鲜的:结合ECMA Spec文档从语法层面介绍并实现Promise的一些核心feature,力求语言扁平朴实,易理解
关于本文
这是专栏《7月跳槽计划》的第一篇,相信读者看了标题大概也明白了,为了迎接即将到来的7月份跳槽,我会作一些准备工作,学习或复习内容,然后把这些记录在这个专栏里,我会定期更新,欢迎关注。
最近刚好也在看ECMA的官方规范文档,看了Promise的部分,感觉收获颇多,就顺手实现了Promise,也记录一下其中的心得体会
如何创建Promsie对象
首先,我们谈谈什么是Promise对象,用权威的术语:Promise是延迟(可能是异步)计算的最终结果的占位符,所以Promsie是用于处理延迟计算的,如果你不打算处理延迟计算,那么你最好别用它。
ECMA specification对Promise对象定义了占位属性
,主要有:
PromiseState
,promsie的状态PromiseResult
,promise的最终结果PromiseFulfillReactions
,存放回调函数的数组(当promise由pending变为fufilled时触发调用)PromiseRejectReactions
,存放回调函数的数组(当promise由pending变为rejected时触发调用)
那么定义一个Promsie构造函数就很简单了:
function Promise(executor){
this.PromiseState = "pending";
this.PromiseResult = undefined;
this.PromiseFulfillReactions = [];
this.PromiseRejectReactions = [];
//对executor类型判断
if(typeof executor !== 'function'){
throw "executor must be a function!"
}
//待完善
executor()
}
new Promise(function(res,rej){
});
复制代码
很明显,我们还需要定义解析函数来处理promsie
定义Promise解析函数
首先声明一下,promsie的解析分为resolve
和reject
两种,只能以其中一种方式解析,不能重复解析。
规范里也对解析函数进行了解释:CreateResolvingFunctions,该函数接受一个promise对象入参,将解析函数与promise进行绑定,简单实现:
function createResolvingFunctions(promise){
const AlreadyResolved = {
value:false
}
//这是promsie的resolve函数
const resolveFunction = (resolution)=>{
//avoid unnecessary invoke
if(resolveFunction.AlreadyResolved.value){
return;
}
rejectFunction.AlreadyResolved.value = true;
//待完善
}
//这是promise的reject函数
const rejectFunction = (resolution)=>{
//avoid unnecessary invoke
if(rejectFunction.AlreadyResolved.value){
return;
}
rejectFunction.AlreadyResolved.value = true;
//待完善
}
//banding with promise
resolveFunction.promise = promise;
rejectFunction.promise = promise;
resolveFunction.AlreadyResolved = rejectFunction.AlreadyResolved = AlreadyResolved;
return {
resolve:resolveFunction,
reject:rejectFunction
}
}
复制代码
补充Promise的构造函数:
function Promise(executor){
//省略...
//开始完善
const promsieResolveFns = createResolvingFunctions(this);
executor(promsieResolveFns.resolve,promsieResolveFns.reject)
}
new Promise(function(res,rej){
res("fulfilled")
});
复制代码
当调用res("fulfilled")
时,应该更新promsie的内部状态,我们将这段逻辑拆分出去,另起两个函数负责fulfill
或者reject
promsie
fulfill和reject函数
顾名思义,这两个哥们用来直接修改promise的内部状态的:
function fulfill(promsie,value){
if(promise.PromiseState === 'pending'){
//extrat reactions from promise
const reactions = promise.PromiseFulfillReactions;
//update promise properties
promise.PromiseState = "fulfilled";
promise.PromiseResult = value;
promise.PromiseFulfillReactions = undefined;
promise.PromiseRejectReactions = undefined;
//execute then functions, base on this change
triggerReactions(reactions,value)
}
}
//reject类似,不再赘述
复制代码
上面有两行有趣的逻辑:promise.PromiseFulfillReactions
和triggerReactions(reactions,value)
,他们跟调用promsie的.then
方法有关:
- 当调用一个处于
pending
状态的promsie的.then(onFulfilled,onRejected)
方法时,onFulfilled
,onRejected
不会执行 - 当该promsie的状态改为
fulfilled
或者rejected
时,提取出所有注册的回调函数,依次触发执行
完善一下解析函数就是:
const resolveFunction = (resolution)=>{
//省略...
//开始完善
const promise = resolveFunction.promise;
fulfill(promsie,resolution)
}
const rejectFunction = (resolution)=>{
//省略...
//开始完善
const promise = rejectFunction.promise;
reject(promsie,resolution)
}
复制代码
接下来,再讲讲then
方法。
每次.then都会新建一个promise
根据ECMA规范,每次调用.then
都会新建一个PromiseCapability
:它包含一个新的promsie和关联该promise的解析函数,创建它的工厂函数为:
function NewPromiseCapability(){
const promsie = new Promise(()=>{});
const resolvingFns = createResolvingFunctions(promise);
return {
promise,
resolve:resolvingFns.resolve,
reject:resolvingFns.reject
}
}
复制代码
注意,.then调用的时机可以有两种:
- 调用时promise已经
fulfilled
或者rejected
- 调用时promise处于
pending
必须兼顾这两种情况,要考虑收集回调函数供未来执行:
Promise.prototype = {
then(onFulfilled,onRejected){
const promise = this;
// 新建PromiseCapability
const capability = NewPromiseCapability();
// 收集两类回调函数及附属信息
const fulfillReaction = {
Handler:onFulfilled,
Type:'fulfill',
Capability:capability
}
const rejectReaction = {
Handler:onRejected,
Type:'reject',
Capability:capability
}
setTimeout(()=>{
switch(promise.PromiseState){
case "fulfilled":
executeReaction(fulfillReaction,promise.PromiseResult)
break;
case "rejected":
executeReaction(rejectReaction,promise.PromiseResult)
break;
default:
//默认pending,收集起来
promise.PromiseFulfillReactions.push(fulfillReaction);
promise.PromiseRejectReactions.push(rejectReaction);
break;
}
},0)
//返回新建的promsie
return capability.promise;
}
}
复制代码
上面的代码很有趣,可以看到,我们通过一个setTimeout将执行执行部分包了起来,因为如果不这么做的会导致以下结果:
var promsie = fulfilledPromise.then(function(result){
//此时的promsie为undefined
return promsie;
})
复制代码
原因很简单,onFulfilled还没执行完毕,所以返回值为空。因此我们先同步return,再进行异步判断执行,问题就得到了解决。
接下来就是如何访问promsie的值了,恭喜眼尖的小伙伴:executeReaction(fulfillReaction,promise.PromiseResult)
:
function executeReaction(reaction,value){
const capability = reaction.Capability;
//取出回调函数
const handler = reaction.Handler;
const type = reaction.Type;
//调用该handler,此时你的then的第一个参数(函数)已经被调用了并记录结果
let handlerResult = handler(value);
//将上述调用结果(返回值)作为then方法里新建promsie的resolve调用入参
capability.resolve(handlerResult);
}
复制代码
最后一行我们调用了capability.resolve(handlerResult)
,此时你可以继续链式操作并访问到handlerResult
的值:
p.then((res)=>{
return "a new value"
}).then(value=>{
console.log(value);//"a new value"
})
复制代码
遗留问题
虽然,骨架已经搭建完毕,但是还有不少问题:
Promise
的一些其他原型方法(catch)和静态方法没有实现(resolve
,all
,race
)- 代码基本都是同步的(这个有点尴尬哦,能用同步代码处理异步操作?逗我呢!)
- 对promise解析时没有进行类型校验(我们知道then方法可能返回一个新的promise对象的哦)
- 没有做
error handling
(这个是最基本的,错误都懒得处理还是写毛代码) - 没有做测试
第一个问题可以根据情况慢慢补充,其他几个问题都是要解决的,不然怎么跳槽呢?
- 对于同步的问题,有一处注意一下就行了:在执行onFulfilled或者onRejected的时候用setTimeout包一下,不要影响其他同步代码的执行。
- 错误捕获,对一些关键函数调用通过try-catch进行处理
- 类型校验,这个做几个条件判断就行
费了九牛二虎之力,完成了改进整理,迎来了最好一个关键的问题:测试
恼人的测试
开始,我根据自己使用原声Promsie的经验,基于nodejs
环境写了几个简单的测试用例,运行就被一个问题卡住了:循环依赖
。
所谓的循环依赖就是模块之间的依赖形成了闭环,例如A-B-C-A
,这样会导致某些模块输出为空。
后来谷歌发现可以将Promsie构造函数添加到global里暴露到内存里,这样将导致循环依赖的关键模块添加到global运行内存里就暂时解决了这个问题,测试可以进行下去了。
基本测试后,又想到借助Promsie A+的测试方案。
于是按照说明写了个adaptor:
//将Promise变量暴露到全局global
require("../index");
const {createResolvingFunctions} = require("../src/resolve")
module.exports = {
resolved(value){
return new Promise((res)=>{
res(value)
})
},
rejected(value){
return new Promise((res,rej)=>{
rej(value)
})
},
deferred(){
const promise = new Promise(()=>{});
const resolveingFunctions = createResolvingFunctions(promise);
return {
promise,
resolve:resolveingFunctions.resolve,
reject:resolveingFunctions.reject
}
}
}
复制代码
引入adaptor
var promisesAplusTests = require("promises-aplus-tests");
const adapter = require("./adaptor")
promisesAplusTests(adapter, function (err) {
// All done; output is in the console. Or check `err` for number of failures.
console.log(err)
});
复制代码
运行测试,结果红了一大片,改进一点,红的变少了,再改进再变少。经过两天断断续续的改进(翻阅它的测试用例),终于结果不负有心人:
这一切很美好,不是吗!
写在最后
阅读一段时间ECMAScript® 2022 Language Specification,感觉这更像是对实现进行了详细陈述的文档,虽然他们声明与具体实现技术无关,但毫无疑问,阅读这个文档会让你对ECMA有一个更深入的理解。
我是在阅读完Promise部分后手动实现了它,也让我对Promise有了更深的认识和理解,这也是本文得以产生的根本原因,虽然开始会非常不习惯,一方面它是全英文,另一方面它有一些晦涩的用语。但当你习惯了以后会就好了。
最后,由于篇幅所限,很多复杂的概念和实现本文没有涉及,但是代码里都有的,项目所有代码和测试用例已经托管到 Github, 如果对本文或Promise有任何疑惑或高见,欢迎浏览讨论哦,感谢阅读!