7月跳槽计划——手动实现Promise

手写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的解析分为resolvereject两种,只能以其中一种方式解析,不能重复解析。
规范里也对解析函数进行了解释: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或者rejectpromsie

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.PromiseFulfillReactionstriggerReactions(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)
});
复制代码

运行测试,结果红了一大片,改进一点,红的变少了,再改进再变少。经过两天断断续续的改进(翻阅它的测试用例),终于结果不负有心人:

image.png

这一切很美好,不是吗!

写在最后

阅读一段时间ECMAScript® 2022 Language Specification,感觉这更像是对实现进行了详细陈述的文档,虽然他们声明与具体实现技术无关,但毫无疑问,阅读这个文档会让你对ECMA有一个更深入的理解。

我是在阅读完Promise部分后手动实现了它,也让我对Promise有了更深的认识和理解,这也是本文得以产生的根本原因,虽然开始会非常不习惯,一方面它是全英文,另一方面它有一些晦涩的用语。但当你习惯了以后会就好了。

最后,由于篇幅所限,很多复杂的概念和实现本文没有涉及,但是代码里都有的,项目所有代码和测试用例已经托管到 Github, 如果对本文或Promise有任何疑惑或高见,欢迎浏览讨论哦,感谢阅读!

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享