Promise的出现和实现

  这篇文章总结一下JS本身单线程异步的局限性(promise出现的原因),以及实现一个简易的Promise。

单线程与异步

  JavaScript是一个单线程执行的语言,在不考虑异步编程的情况下,它执行的顺序就是一个eventLoop的简单循环。比如书写一段简单的JS代码:

// 声明两个变量,和一个函数
var demoVariA = 100;
var demoVariB = 200;
// 函数的功能是把入参的两个数值相加
function addTowNum (a, b) {
    return a + b;
}
addTowNum(demoVariA, demoVariB);

  那么在不考虑预解析的情况下(变量函数作用域提升),我们试着把上面这段代码执行顺序用一个eventLoop来简单表示出来。

// 代码的执行队列
eventLoop = [
    '赋值100给内存demoVariA',
    '赋值200给内存demoVariB',
    '申请堆栈内存声明为addTowNum,赋值为一个函数',
    '执行函数'
];
// 现在JS代码需要分析执行
// 这里不考虑任何异步的情况,包括宏任务和微任务
while (eventLoop.length > 0) {
    // 这里表示对词法token进行编译借的过程的抽象
    var event = eventLoop.shift();
    try {
        event();
    } catch (error) {
        report(error);
    }
}

  我们可以看到,不需要考虑多线程共享数据时线程执行先后对程序造成的影响,不需要使用进程锁的概念,词法的执行顺序是多么的清晰,单线程编程是多么的令人愉快。直到异步进入到我们的编程世界。

  同样是上面的例子,我们把demoVariA,demoVariB的数据请求改为异步的请求获取值asyncGetSomeValue,事情就完全不一样了。

var demoVariA = asyncGetSomeValue('pathA');
var demoVariB = asyncGetSomeValue('pathB');
function addTowNum (a, b) {
    return a + b;
}
addTowNum(demoVariA, demoVariB); // undefined  + undefined = NaN

  asyncGetSomeValue必须在某个异步操作之后,再获取到值,因此按照JS的常规做法,我们必须把demoVariA,demoVariB放到回调当中去获取值,我们把代码修改如下:

var demoVariA;
var demoVariB;
asyncGetSomeValue('pathA', function (responseValue) {
    demoVariA = responseValue;
});
asyncGetSomeValue('pathB', function (responseValue) {
    demoVariB = responseValue;
});
function addTowNum (a, b) {
    return a + b;
}

  现在问题来了addTowNum,该放到哪里去执行才能a和b同时获取到呢。有的同学此时会说,简单,我把demoVariB获取放到demoVariA获取的回调下,再去执行addTowNum

var demoVariA;
var demoVariB;
asyncGetSomeValue('pathA', function (responseValue) {
    demoVariA = responseValue;
    asyncGetSomeValue('pathB', function (responseValue) {
        demoVariB = responseValue;
        addTowNum(demoVariA, demoVariB);
    });
});

function addTowNum (a, b) {
    return a + b;
}

  实际上,demoVariB的请求并没有依赖到demoVariA,因此把demoVariB的取值请求放到demoVariA的后面的做法是错误的,这样会导致addTowNum这个函数的调用时间将会变成两个异步请求费时的总和。这两个获取值得请求严格意义上应该是并发的概念。我们要做的是需要声明一个为gate的函数,当你传入的所有变量存在的时候,去执行传入的函数。

/**
 * @param {function} gateFunction 所有变量存在后执行的函数
 * @param 剩余变量
  */
function gate (gateFunction) {
    var testArray = arguments.slice(1);
    // 全部变量都存在的时候,才执行gateFunction
    if (testArray.every(variable => variable)) {
        gateFunction.apply(this, testArray);
    }
}

  此时加入gate函数,我们上面的修改为异步的例子才算简单完成。实际业务编程当中,我们还要考虑asyncGetSomeValue的异常抛错等问题。

var demoVariA;
var demoVariB;
asyncGetSomeValue('pathA', function (responseValue) {
    demoVariA = responseValue;
    gate(addTowNum, a, b);
});
asyncGetSomeValue('pathB', function (responseValue) {
    demoVariB = responseValue;
    gate(addTowNum, a, b);
});
function addTowNum (a, b) {
    return a + b;
}

  通过这个简单的例子我们不难发现异步编程的问题:

  1. 多异步返回的执行顺序不可控。
  2. 多异步的异常错误处理非常繁杂。
  3. 多异步嵌套,会导致回调地狱。

  我们急需要一个能够保证异步执行顺序,保证执行和抛出错误的异步处理的保证范式来解决这些问题。ES6给我们的答案就是Promise(承诺)。

Promise的特性

都是将来

  Promise的使用方法不做阐述,想要了解的可以去查询一下MDN。

  Promise的回调than有一个非常重要的特性,那就是无论是现在还是将来,统一都是将来。我们来猜一下下面代码的执行顺序:

var promise = new Promise(function (resolve, reject) {
    console.log(1);
    setTimeout(() => {
        console.log(2);
    }, 0)
    resolve(3);
});
promise.then(val => console.log(val));
console.log(4);

  简单分析一下执行顺序,代码解析按照从上到下进行解析运行。在new Promise的过程中,里面的代码会被执行。因此执行console.log(1)输出1。然后执行到setTimeout,JS按照宏任务的逻辑把setTimeout放到本次事件循环结束的最末端之后。然后注册resolve

  new Promise执行完毕,返回Promise的实例对象给变量promisepromise执行then方法。Promisethen方法是一个微任务,不管里面的代码是什么内容,这段代码都会被放到当前事件队列的最末尾

  代码现在跑到console.log(4),正常输出。执行完毕后,到达事件循环的末端,此时执行微任务,promisethen回调执行。本次事件循环结束。事件循环结束后执行宏任务setTimeout

其他特性

  Promise本身具有pending fulfilled rejected三个状态,它们之间是互相不可逆的。
  其次,Promise通过多次调用 .then(),可以添加多个回调函数,它们会按照插入顺序并且独立运行。new Promise的内容只会被执行一次

开写Promise

搭建框架

  我们先简单搭建一下需要实现的myPromise的大体框架。然后根据Promise的特性来实现。首先,Promise通过new去执行,是一个构造函数的特征。它接受一个函数作为参数,该函数接受resolvereject,并且在new的时候就被执行。

  其次我们需要有一个_status来复现Promise的三个状态。

  接着我们实现一下then catch方法,这些构造函数也能使用的方法,我们定义在prototype上面。

  现在我们能根据这些需求搭建出来myPromise的框架。

window.myPromise = function (executor) {

    this._resolve = function (value) {

    }

    this._reject = function (error) {

    }

    // pending fulfilled rejected
    this._status = 'pending';
    // 使用bind来绑定作用域,保证可以访问到实例上面的属性
    executor(this._resolve.bind(this), this._reject.bind(this));
}

myPromise.prototype.then = function (thenCallBack) {

}

myPromise.prototype.catch = function (errorCallBack) {
    
}

实现then和_reslove。

  我们需要思考一个核心点,那就是then中的回调,如果保证构造函数存在异步逻辑时,在reslove之后去执行。其实很简单,就是在then当中传入的回调thenCallBackthen中不执行,而在_resolve当中执行就可以了。

  简单来说,在then方法中只进行事件的注册,每次调用then传入的方法,我们把它保存起来,在_resolve中去执行。是一个简单的发布-订阅模式。现在我们在myPromise中注册一个thenEventList,用来保证调用then的执行顺序。

  现在在_resolve中,我们可以像之前eventLoop的逻辑一样去循环调用then注册进来的事件。这里我们使用setTimeout来模拟微任务的特性,把then注册的回调放到事件队列的最末端去执行。

window.myPromise = function (executor) {

    // 保存 then 注册的回调
    this.thenEventList = [];

    this._resolve = function (value) {
        // 用箭头函数确定this值
        setTimeout(() => {
            this.thenEventList.reduce((accumulator, resolveHandler) => {
                // 如果已经发生错误,不需要继续执行,直接返回即可
                if (this._status === 'rejected') return;
                return resolveHandler(accumulator);
            }, value);
            // 全部执行完毕表明状态更改
            this._status === 'fulfilled'
        }, 0);

    }

    // pending fulfilled rejected
    this._status = 'pending';

    executor(this._resolve.bind(this), this._reject.bind(this));
}


myPromise.prototype.then = function (thenCallBack) {
    this.thenEventList.push(thenCallBack);
    // 链式调用
    return this;
}

实现catch和_reject。

  按照then_reslove的实现思路,我们可以很简单的把catch_reject写出来。值得一提的是,catch不返回Promise链,自然也不用像then一样去注册一个事件列表,只需要保存一下errorCallBack即可。


window.myPromise = function (executor) {

    this._reject = function (error) {
        if (this._status === 'fulfilled') return;
        // 用箭头函数确定this值
        setTimeout(() => {
            if (typeof this.errorCallBack === 'function') {
                this.errorCallBack(error);
            }
        }, 0);
    }

    // pending fulfilled rejected
    this._status = 'pending';

    executor(this._resolve.bind(this), this._reject.bind(this));
}

myPromise.prototype.catch = function (errorCallBack) {
    this.errorCallBack = errorCallBack;
    this._status = 'rejected';
}

  catch还有一个作用,就是可以抓到Promise链中运行的程序错误。所以我们用 try catch来抓一下_resolve的执行,然后补完myPromise


window.myPromise = function (executor) {

    this._reject = function (error) {
        if (this._status === 'fulfilled') return;
        // 用箭头函数确定this值
        setTimeout(() => {
            if (typeof this.errorCallBack === 'function') {
                this.errorCallBack(error);
            }
        }, 0);
    }

    // 保存 then 注册的回调
    this.thenEventList = [];

    this._resolve = function (value) {
        // 用箭头函数确定this值
        setTimeout(() => {
            this.thenEventList.reduce((accumulator, resolveHandler) => {
                if (this._status === 'rejected') return;
                try {
                    return resolveHandler(accumulator);
                } catch (error) {
                    if (error) {
                        this._status = 'rejected';
                        this._reject(error);
                    }
                }

            }, value);
            this._status = 'fulfilled';
        }, 0);
    }

    // pending fulfilled rejected
    this._status = 'pending';

    executor(this._resolve.bind(this), this._reject.bind(this));
}

myPromise.prototype.then = function (thenCallBack) {
    this.thenEventList.push(thenCallBack);
    // 链式调用
    return this;
}

myPromise.prototype.catch = function (errorCallBack) {
    this.errorCallBack = errorCallBack;
}

总结

  Promise产生的意义就是为了解决JS异步回调不可控的问题,它保证了异步回调执行的顺序执行次数,完善了异常情况的处理。用微任务放置到任务回调末尾的处理方式来解决同步异步代码混用的执行顺序问题,后续精进的使用Generator函数 + Promise完成的async await语法糖,是社区的终极异步解决方案,其实也就是用同步的方式来处理异步。

  本次简易实现中,我们使用订阅发布的观察者模式来处理than方法的事件注册,然后在传入的executor调用的resolve后去执行。使用数组添加的方式来保证than函数注册的执行顺序。使用setTimeout略显hack的方式,来模拟了微任务调度的特性。真实的Promiseprototype还有诸如race all的方法,对应的reslove方法也可以展开传入的任意thanable结构的对象。但这些方法的实现,都是建立在简易实现的思路根本上,进行一些细节的处理和完善。Promise的核心,始终没有变过。

你可能感兴趣的:(Promise的出现和实现)