promise源码解析(译)

4,896 阅读15分钟

最新项目中有用promise几个api,对代码结构看起来的确很爽。然后想着去网上找了几篇promise库源码解析的文章。但是看了几篇,感觉还是不能够很理解,然后看到一篇翻译文章有说道q.js库的作者有对promise实现的递进讲解,看了一下,还不错,

Q.js作者源码分析:Q.js作者promise递进讲解实现

网上找的promise源码翻译。文章有些地方翻译的很好,但是对比原文发现少了一些内容,所以读起来很不顺畅。所以自己根据原文也翻译了一遍。对了,本文适合用过promise的人阅读。如果你还没有接触过。可以右转阮一峰的promise讲解


一、极简版异步回调

假如你正在写一个函数不立即返回值,而是等待几秒钟后才返回执行结果,你会怎么写呢?思考几秒钟。

最简单的做法自然是写一个回调函数依靠定时器来返回值,比如下面这个

var oneOneSecondLater = function (callback) {
    setTimeout(function () {
        callback(1);
    }, 1000);
};

这是一个很简单解决问题的方法,但是还有改进的地方,比如能够添加代码执行错误时给出提示。

var maybeOneOneSecondLater = function (callback, errback) {
    setTimeout(function () {
        //进行判断情况,是执行成功的回调,还是执行错误的回调
        if (Math.random() < .5) {
            callback(1);
        } else {
            errback(new Error("Can't provide one."));
        }
    }, 1000);
};

一般的做法是提供一个能同时返回值并且能抛出错误的工具。上面这个例子则演示同时提供回调和错误处理。但是这种写法实在是太定制化了,并不好。

二、Promise基本雏形设计

所以考虑到大多数的情况,代替最简单的返回值和抛出异常,我们更希望函数通常会返回一个对象用来表示最后执行成功或者失败的结果,而这个返回的对象就是promise。从名字上理解,promise表示承诺,那么最终这个promise(承诺)是要被resolve(履行,执行)掉的。

接下去我们开始迭代设计promise。我们先设计一个具有“then”方法的promise模型,通过“then”方法,我们能注册回调函数并且延迟执行。

var maybeOneOneSecondLater = function () {
    var callback;
    setTimeout(function () {
        callback(1);
    }, 1000);
    return {
        then: function (_callback) {
            callback = _callback;
        }
    };
};

maybeOneOneSecondLater().then(callback1);

代码写好了。但是大家仔细观察发现该方案仍然还有两个缺点

  • 一是现在方案只能执行一个添加的回调函数。最好的做法是每一个通过then添加进来的回调都能被通知到然后顺序执行。
  • 二是如果这个回调函数是在promise创建好1s之后通过then添加进去,它将无法被调用。

敲黑板,注意注意,接下去开始慢慢搭建promise了。

正常情况下,我们希望可以接收任何数量的回调,且不管是否超时,仍然可以继续注册回调。为了实现这些,我们将创建一个包含两个功能的promise对象

我们暂时设计了一个defer对象,他的返回值一个包含两部分的对象(这个对象就是promise),一个用来注册观察者(就是"then方法添加回调),一个用来通知所有的观察者执行代码(就是resolve去执行之前添加的所有回调)。

promise没有被resolve之前,所有回调函数会存储在一个"pengding"的数组中。

promiseresolve之后,立即执行之前存储的所有回调函数,当回调函数全部执行完毕之后,我们将根据"pengding"来区分状态。

let defer = () => {
    let pending = [],value;
    return {
        resolve(_value){
            value = _value
            for(let i = 0;i < pending.length; i++){
                pending[i](value)
            }
            pending = undefined;
        },
        then(_callback){
            if(pending){
                pending.push(_callback)
            }else{
                _callback();
            }
        }
    }
}
let oneOneSecondLater = () => {
    let result = defer();
    setTimeout(()=> {
        result.resolve(1);
    }, 1000);
    return result;
};

oneOneSecondLater().then(callback);

这开始的第一步很关键啊,因为此时我们已经可以做到

  1. 可以任意时间添加任意多的回调;
  2. 可以人为决定什么时候resolve;
  3. 当promise被resolve之后,还可以添加回调,只不过此时立即就执行了

但是还有一些问题,比如

  1. defer可以被resolve执行多次,我们并没有给出一个错误的提示。而且事实上为了避免恶意或者无意的不断去resolve,我们仅允许第一次调用可以通知回调并执行。
  2. 添加回调只能通过defer.then添加,不能链式调用,即defer.then(callback).then(callback)

那么接下来我们先修正第一个问题

let defer = () => {
    let pending = [],value
    return {
        resolve(_value){
            if(pending){
                value = _value
                for(let i = 0;i < pending.length; i++){
                    pending[i](value)
                }
                pending = undefined;
            }else{
                throw new Error("A promise can only be resolved once.")
            }
        },
        then(_callback){
            if(pending){
                pending.push(_callback)
            }else{
                _callback();
            }
        }
    }
}

好,现在我们已经保证不能重复defer.resolve()的问题了,那么我们还希望可以实现通过链式调用来添加回调。可是目前要只能通过defer().then(callback1),defer().then(callback2),defer().then(callback3)这种方式添加回调,这显然不是我们想要的方式。接下来我们将一步一步实现。

三、promise职责分离

但是在实现链式回调之前,为了后期结构,我们希望对我们的promise进行职责区分,一个注册观察者,一个执行观察者。根据最少授权原则,我们希望如果授权给某人一个promise,这里只允许他增加观察者;如果授权给某人resolver,他应当仅仅能决定什么时候给出解决方案。因为大量实验表明任何任何不可避免的越权行为会导致后续的改动变得很难维护。(其实就是希望把添加回调的then功能移植到promise中,从defer.then转变成defer.promise.then,保证功能的纯粹性)

let defer = () => {
    let pending = [],value;
    return {
        resolve(_value){
            if(pending){
                value = _value
                for(let i = 0;i < pending.length; i++){
                    pending[i](value)
                }
                pending = undefined;
            }else{
                throw new Error("A promise can only be resolved once.")
            }
        },
        promise: {
            then (callback) {
                if (pending) {
                    pending.push(callback);
                } else {
                    callback(value);
                }
            }
        }
    }
}

当职责分离完之后,我们就可以接下去实现一步关键的改造

四、promise的链式调用

上文说道要实现链式回调,我们首先要能在下一个回调函数里接受上一个回调的值。依靠上一步的职责分离的基础,我们接下来要跨非常大的一步,就是使用旧的promise去驱动新的promise。我们希望通过promise组合的使用,来实现值的传递。

举个例子,让你写一个相加的函数,接受两个回调函数返回的数字相加。大家可以考虑如何实现。

var twoOneSecondLater = function (callback) {
    var a, b;
    var consider = function () {
        if (a === undefined || b === undefined)return;
        callback(a + b);
    };
    oneOneSecondLater(function (_a) {
        a = _a;
        consider();
    });
    oneOneSecondLater(function (_b) {
        b = _b;
        consider();
    });
};

twoOneSecondLater(function (c) {
    // c === 2
});

上面这个方法虽然做到了,但是这个方法是脆弱的,因为我们在执行相加函数时,需要额外的代码去判断相加的数字是否有效。

于是我们希望用更少的代码去实现上面的需求,比如就像下面这样

//上面的函数如果用更少的步骤来表达就是
var a = oneOneSecondLater();
var b = oneOneSecondLater();
var c = a.then(function (a) {
    return b.then(function (b) {
        return a + b;
    });
});

上面这个例子其实想表达的就是实现callback返回值的传递,如callback1的返回值传给callback2,将callback2的返回值传给callback3。 为了实现上面例子的这种效果,我们要实现以下几点

  • 每个then方法后必须要返回一个promise
  • 每一个promise被resolve后,返回的必然是一个新的promise或者是一个执行过的值
  • 返回的promise最终可以带着回调的值被resolve掉(这句话有点难翻译,感觉就是promise.resolve(_value));

我们实现一个函数可以将获得的值传给下一个回调使用

let ref = (value) => {
    return {
        then(callback){
            callback(value);
        }
    }
}

不过考虑到有时候返回的值不仅仅是一个值,而且还可能是一个promise函数,所以我们需要加个判断

let ref = (value) => {
    if(value && typeof value.then === "function"){
        return value;
    }
    return {
        then(callback){
            callback(value);
        }
    }
}

这样子我们在使用中就不需要考虑传入的值是一个普通值还是一个promise了。

接下来,为了能使then方法也能返回一个promise,我们来改造下then方法;我们强制将callback的返回值传入下一个promise并立即返回。 这个例子存储了回调的值,并在下一个回调中执行了。但是上面第三点没有实现,因为返回值可能是一个promise,那么我们继续改进一下方法

let ref = (value) => {
    if(value && typeof value.then === "function"){
        return value;
    }
    return {
        then(callback){
            return ref(callback(value));
        }
    }
}

通过这一步增强之后,基本上就可以做到获得上一个回调值并不断链式调用下去了。

接下去我们考虑到一种比较复杂的情况,就是defer中存储的回调会在未来某个时间调用。于是我们需要在defer里面将回调进行一次封装,我们将回调中执行完后通过then方法去驱动下一个promise并传递一个返回值。

此外,resolve方法应该能处理本身是一个promise的情况,resolve可以将值传递给promise。因为不管是ref还是defer都可以返回一个then方法。如果promise是ref类型的,将会通过then(callback)立即执行回调。如果是promise是defer类型的,callback暂时被存储起来,依靠下一个then(callback)调用才能执行;所以变成了callback可以监听一个新的promise以便能获取完全执行后的value。

根据以上要求,得出了下面最终版的promise

let isPromise = (value) => {
    return value && typeof value.then === "function";
};

let ref = (value) => {
    if (value && typeof value.then === "function")
        return value;
    return {
        then (callback) {
            return ref(callback(value));
        }
    };
};

let defer = () => {
    let pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = ref(_value); // values wrapped in a promise
                for (let i = 0, ii = pending.length; i < ii; i++) {
                    let callback = pending[i];
                    value.then(callback); // then called instead
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (_callback) {
                let result = defer();
                // callback is wrapped so that its return
                // value is captured and used to resolve the promise
                // that "then" returns
                let callback = function (value) {
                    result.resolve(_callback(value));
                };
                if (pending) {
                    pending.push(callback);
                } else {
                    value.then(callback);
                }
                return result.promise;
            }
        }
    };
};

let a = defer();
a.promise.then(function(value){console.log(value);return 2}).then(function(value){console.log(value)});
a.resolve(1);

将defer分为两个部分,一个是promise,一个是resolve

到了这一步基本上的promise功能已经实现了,可以链式调用,可以在自己控制在未来某个时间resolve。接下去就是功能的增强和补足了。

这一块回调基本上就写完了,看了很久原文的描述,对着代码理解作者想表达的意思。不过英语不太好,写的磕磕绊绊。╮(╯▽╰)╭,感觉还是有些地方写的不对。希望有人能够纠错出来。

五、提供错误的回调

为了实现错误消息的传递,我们还需要一个错误的回调函数(errback)。就像promise完全执行时调用callback一样,它会告知执行errback以及告诉我们拒绝的原因。

实现一个类似于前面ref的函数。

let reject = (reason) => {
    return {
        then(callback,errback){
            return ref(errback(reason);
        }
    }
}

最简单的实现方法是当监听到返回值时,立即执行代码

reject("Meh.").then((value) => {},(reason) => {
    throw new Error(reason);
}

那么接下来我们改进原来promsie这个API,引入“errback”。

为了将错误回调添加到代码中,defer需要添加一种新的容器来添加成功回调和错误回调。因此之前那个存储在数组(pending)中的只有一种待处理回调函数,我们需要重新设计一个同时包含成功回调和错误回调的数组([callback,errback]),根据then传入的参数决定调用哪个。

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = ref(_value);
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    // apply the pending arguments to "then"
                    value.then.apply(value, pending[i]);
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (_callback, _errback) {
                var result = defer();
                var callback = function (value) {
                    result.resolve(_callback(value));
                };
                var errback = function (reason) {
                    result.resolve(_errback(reason));
                };
                if (pending) {
                    pending.push([callback, errback]);
                } else {
                    value.then(callback, errback);
                }
                return result.promise;
            }
        }
    };
};

let ref = (value) => {
    if (value && typeof value.then === "function")
        return value;
    return {
        then: function (callback) {
            return ref(callback(value));
        }
    };
};

let reject = (reason) => {
    return {
        then: function (callback, errback) {
            return ref(errback(reason));
        }
    };
};

代码写完了,但是仍然还有地方可以改进。

比如作者说到这一步有一个问题,就是如果按照上面这么写,那么所有的then函数就必须提供错误回调函数(_errback),如果不提供就会出错。所以最简单的解决方法是提供一个默认的回调函数。甚至文中还说,如果仅仅是对错误回调有需要,那么忽略不写成功回调(_callback)也是可以的。所以为了满足需求,我们为_callback和_errback都提供一个默认的回调函数。(好吧,其实我就是觉得这是一个好的库的容错处理)

var defer = function () {
    ...
    return{
        ...
        promise : {
             then: function (_callback, _errback) {
                var result = defer();
                // 提供一个默认的成功回调和错误回调
                _callback = _callback || function (value) {
                    // 默认执行
                    return value;
                };
                _errback = _errback || function (reason) {
                    // 默认拒绝
                    return reject(reason);
                };
                var callback = function (value) {
                    result.resolve(_callback(value));
                };
                var errback = function (reason) {
                    result.resolve(_errback(reason));
                };
                if (pending) {
                    pending.push([callback, errback]);
                } else {
                    value.then(callback, errback);
                }
                return result.promise;
            }
        }
        }
    }
    
}

好了,现在我们已经实现了接收构造或者隐含的错误回调这一步的完成版

六、安全性和稳定性

我们还有需要需要提高的地方就是要保证callbacks和errbacks在未来他们被调用的时候,应该是和注册时的顺序是保持一致的。这将显著降低异步编程中流程控制出错可能性。文中举了一个有趣的小例子.

var blah = function () {
    var result = foob().then(function () {
        return barf();
    });
    var barf = function () {
        return 10;
    };
    return result;
};

上面这个函数在执行后会出现两种情况,一是抛出一个异常,二是顺利执行并返回了值10。而决定是哪个结果的是foob()是否在正确顺序里。因为我们希望哪怕回调在未来被延迟执行了,它能够执行成功。

下面添加了一个enqueue方法,我的理解就是依靠setTimeout的异步将所有回调按照顺序添加到任务队列中,保证按照顺序执行代码。

let enqueue = (callback) => {
    setTimeout(callback,1)
}
let enqueue = (callback) => {
    //process.nextTick(callback); // NodeJS
    setTimeout(callback, 1); // Naïve browser solution
};

let defer = function () {
    let pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = ref(_value);
                for (let i = 0, ii = pending.length; i < ii; i++) {
                    enqueue(function () {
                        value.then.apply(value, pending[i]);
                    });
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (_callback, _errback) {
                let result = defer();
                _callback = _callback || function (value) {
                    return value;
                };
                _errback = _errback || function (reason) {
                    return reject(reason);
                };
                let callback = function (value) {
                    result.resolve(_callback(value));
                };
                let errback = function (reason) {
                    result.resolve(_errback(reason));
                };
                if (pending) {
                    pending.push([callback, errback]);
                } else {
                    // XXX
                    enqueue(function () {
                        value.then(callback, errback);
                    });
                }
                return result.promise;
            }
        }
    };
};

let ref = function (value) {
    if (value && value.then)
        return value;
    return {
        then: function (callback) {
            let result = defer();
            // XXX
            enqueue(function () {
                result.resolve(callback(value));
            });
            return result.promise;
        }
    };
};

let reject = function (reason) {
    return {
        then: function (callback, errback) {
            var result = defer();
            // XXX
            enqueue(function () {
                result.resolve(errback(reason));
            });
            return result.promise;
        }
    };
};

虽然将需要的回调依照次序添加到了队列中

作者有考虑到一些新的问题,比如

  • callback或者errback必须以同样的顺序被调用
  • callback或者errback可能会被同时调用
  • callback或者errback可能会被调用多次

于是我们需要找个机会then的回调函数,为了保证当回调函数中程序出错时,可以转入到报错函数中。(其实又是一个库的容错处理,保证代码出错时不中断程序的执行)。

用when方法封装下promise以此阻止错误发生,确保不会有哪些突发性的错误,包括哪些非必需的事件流控制,并且也能使callback和errback各自保持独立。

var when = function (value, _callback, _errback) {
    var result = defer();
    var done;

    _callback = _callback || function (value) {
        return value;
    };
    _errback = _errback || function (reason) {
        return reject(reason);
    };

    var callback = function (value) {
        try {
            return _callback(value);
        } catch (reason) {
            return reject(reason);
        }
    };
    var errback = function (reason) {
        try {
            return _errback(reason);
        } catch (reason) {
            return reject(reason);
        }
    };

    enqueue(function () {
        ref(value).then(function (value) {
            if (done)
                return;
            done = true;
            result.resolve(ref(value).then(callback, errback));
        }, function (reason) {
            if (done)
                return;
            done = true;
            result.resolve(errback(reason));
        });
    });

    return result.promise;
};

这一步的完整版

六、消息传递

现在这一步来看,promise已经成为了一个具有接受消息功能的类了。Deferred promise根据获得的消息来执行对应的回调函数,返回对应的值。当你接收到完全成功执行的值,则在then中执行成功的回调函数返回msg;获得错误的值则在then中执行错误回调函数,返回错误的原因

因此我们基本可以认为promise这个类可以接受任何的值,包括"then/when"这些信息。这对于一些非立即执行函数的监听非常有用。举个例子,当你发了一个网络请求,等待返回值才能执行函数。我们等待这个请求的往返的过程中浪费了许多时间,而promise仿佛在电脑中另外开了一个线程进行监听这些返回值,然后执行对应的回调函数(这个例子是自己理解举的,非原文,如有不对,欢迎改正)。

翻到这里有点崩溃了,捂下脑子,接下去感觉有点头疼了,以后再补吧,因为基本形态的promise已经出来。接下去是另外一种需求的promise了

接下来我们要包装一种新型的promise,这套promise基于一些能发送任意消息的方法之上,可以满足 "get", "put", "post"能发送相应的消息,并且能根据返回结果中执行相应的promise。


第一次尝试翻译,真的是个体力活,花了快2天的时间,整个人都是炸的。不过所幸是比以前明白了一些恭喜。 原文大概讲解了基本的promise构成,但是现在还是有许多方法并没有分析,接下去我按照自己的想法去实现以下promise.all方法。如果写的不好,欢迎大家指正,帮我进步一下,谢谢。(手动捂脸)