深切明白 Promise 完成细节

在之前的异步JavaScript与Promise一文中,我引见了Promise以及它在异步JavaScript中的运用意义。一般来说,我们是经由历程种种JavaScript库来运用Promise的。跟着运用Promise的时机越来越多,你也能够像我如许会体贴Promise究竟是如何事变的。明显,相识Promise的完成细节,能够协助我们更好地运用它。尤其是遇到一些Promise的题目时,或许能够更疾速、更准确地定位缘由,并处置惩罚它。

异常光荣,在[Promises/A wiki][]中位于库列表第一位的[Q][],供应了它作为一个Promise库的[基本设想道理剖析][]。本文将主要依据Q的这篇文章,议论Promise的完成细节。

Promise中心申明

只管Promise已有本身的范例,但如今的各种Promise库,在Promise的完成细节上是有差别的,部份API甚至在意义上完整差别。但Promise的中心内容,是相通的,它就是then要领。在相干术语中,promise指的就是一个有then要领,且该要领能触发特定行动的对象或函数。

有关Promise中心申明的细节,引荐浏览[Promises/A+][]。这篇文章是写给Promise库的开发者的,你能够找到种种对Promise特征的申明。[Promises/A+][]愿望开发者顺从这些特征,以完成能够配合运用的Promise(也就是说,差别的Promise库也可共用)。

Promise能够有差别的完成体式格局,因而Promise中心申明并不会议论任何详细的完成代码。

先浏览Promise中心申明的意义是:看,这就是须要写出来的结果,请参照这个结果想想如何用代码写出来吧。

起步:用这一种体式格局明白Promise

追念一下Promise处置惩罚的是什么题目?回调。比方,函数doMission1()代表第一件事变,如今,我们想要在这件事变完成后,再做下一件事变doMission2(),应当如何做呢?

先看看我们罕见的回调情势。doMission1()说:“你要这么做的话,就把doMission2()交给我,我在完毕后帮你挪用。”所以会是:

doMission1(doMission2);

Promise情势又是如何呢?你对doMission1()说:“不可,控制权要在我这里。你应当转变一下,你先返回一个迥殊的东西给我,然后我来用这个东西部署下一件事。”这个迥殊的东西就是Promise,这会变成如许:

doMission1().then(doMission2);

能够看出,Promise将回调情势的主从关联调换了一个位置(翻身做主人!),多个事宜的流程关联,就能够如许集合到骨干道上(而不是疏散在各个事宜函数以内)。

好了,如何做如许一个转换呢?从最简朴的状况来吧,假定doMission1()的代码是:

function doMission1(callback){
    var value = 1;
    callback(value);
}

那末,它能够转变一下,变成如许:

function doMission1(){
    return {
        then: function(callback){
            var value = 1;
            callback(value);
        }
    };
}

这就完成了转换。虽然并非现实有用的转换,但到这里,实在已触及了Promise最为主要的完成要点,即Promise将返回值转换为带then要领的对象

进阶:Q的设想旅程

从defer最先

design/q0.js是Q开端成型的第一步。它建立了一个名为defer的东西函数,用于建立Promise:

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            value = _value;
            for (var i = 0, ii = pending.length; i < ii; i++) {
                var callback = pending[i];
                callback(value);
            }
            pending = undefined;
        },
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else {
                callback(value);
            }
        }
    }
};

这段源码能够看出,运转defer()将获得一个对象,该对象包括resolvethen两个要领。请追念一下jQuery的Deferred(一样有resolvethen),这两个要领将会是近似的结果。then会参考pending的状况,假如是守候状况则将回调保留(push),不然马上挪用回调。resolve则将肯定这个Promise,更新值的同时运转完一切保留的回调。defer的运用示比方下:

var oneOneSecondLater = function () {
    var result = defer();
    setTimeout(function () {
        result.resolve(1);
    }, 1000);
    return result;
};

oneOneSecondLater().then(callback);

这里oneOneSecondLater()包括异步内容(setTimeout),但这里让它马上返回了一个defer()天生的对象,然后将对象的resolve要领放在异步完毕的位置挪用(并附带上值,或者说结果)。

到此,以上代码存在一个题目:resolve能够被执行屡次。因而,resolve中应当到场对状况的推断,保证resolve只要一次有用。这就是Q下一步的design/q1.js(仅差别部份):

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

对第二次及更多的挪用,能够如许抛出一个毛病,也能够直接疏忽掉。

星散defer和promise

在前面的完成中,defer天生的对象同时具有then要领和resolve要领。根据定义,promise体贴的是then要领,至于触发promise转变状况的resolve,是另一回事。所以,Q接下来将具有then要领的promise,和具有resolve的defer星散开来,各自自力运用。如许就好像划清了各自的职责,各自只留肯定的权限,这会使代码逻辑更明了,易于调解。请看design/q3.js:(q2在此跳过)

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

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            if (pending) {
                value = _value;
                for (var i = 0, ii = pending.length; i < ii; i++) {
                    var callback = pending[i];
                    callback(value);
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (callback) {
                if (pending) {
                    pending.push(callback);
                } else {
                    callback(value);
                }
            }
        }
    };
};

假如你细致对照一下q1,你会发明辨别很小。一方面,不再抛出毛病(改成直接疏忽第二次及更多的resolve),另一方面,将then要领移动到一个名为promise的对象内。到这里,运转defer()获得的对象(就称为defer吧),将具有resolve要领,和一个promise属性指向另一个对象。这另一个对象就是唯一then要领的promise。这就完成了星散。

前面另有一个isPromise()函数,它经由历程是不是有then要领来推断对象是不是是promise(duck-typing的推断要领)。为了准确运用和处置惩罚星散开的promise,会像如许须要将promise和其他值辨别开来。

完成promise的级联

接下来会是相称主要的一步。到前面到q3为止,所完成的promise都是不能级联的。但你所熟知的promise应当支撑如许的语法:

promise.then(step1).then(step2);

以上历程能够明白为,promise将能够制造新的promise,且取自旧的promise的值(前面代码中的value)。要完成then的级联,须要做到一些事变:

  • then要领必需返回promise。

  • 这个返回的promise必需用通报给then要领的回调运转后的返回结果,来设置本身的值。

  • 通报给then要领的回调,必需返回一个promise或值。

design/q4.js中,为了完成这一点,新增了一个东西函数ref

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

这是在动手处置惩罚与promise关联的value。这个东西函数将对任一个value值做一次包装,假如是一个promise,则什么也不做,假如不是promise,则将它包装成一个promise。注重这里有一个递归,它确保包装成的promise能够运用then要领级联。为了协助明白它,下面是一个运用的例子:

ref("step1").then(function(value){
    console.log(value); // "step1"
    return 15;
}).then(function(value){
    console.log(value); // 15
});

你能够看到value是如何通报的,promise级联须要做到的也是云云。

design/q4.js经由历程连系运用这个ref函数,将本来的defer转变为可级联的情势:

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

本来callback(value)的情势,都修正成value.then(callback)。这个修正后结果实在和本来雷同,只是由于value变成了promise包装的范例,会须要如许挪用。

then要领有了较多更改,会先新天生一个defer,并在结尾处返回这个defer的promise。请注重,callback不再是直接取用通报给then的谁人,而是在此基本之上增添一层,并把新天生的defer的resolve要领安排在此。此处能够明白为,then要领将返回一个新天生的promise,因而须要把promise的resolve也预留好,在旧的promise的resolve运转后,新的promise的resolve也会随之运转。如许才像管道一样,让事宜根据then衔接的内容,一层一层通报下去。

到场毛病处置惩罚

promise的then要领应当能够包括两个参数,分别是肯定和否认状况的处置惩罚函数(onFulfilledonRejected)。前面我们完成的promise还只能转变为肯定状况,所以,接下来应当到场否认状况部份。

请注重,promise的then要领的两个参数,都是可选参数。design/q6.jsq5也跳过)到场了东西函数reject来协助完成promise的否认状况:

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

它和ref的主要辨别是,它返回的对象的then要领,只会取第二个参数的errback来运转。design/q6.js的其余部份是:

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++) {
                    value.then.apply(value, pending[i]);
                }
                pending = undefined;
            }
        },
        promise: {
            then: function (_callback, _errback) {
                var result = defer();
                // provide default callbacks and errbacks
                _callback = _callback || function (value) {
                    // by default, forward fulfillment
                    return value;
                };
                _errback = _errback || function (reason) {
                    // by default, forward rejection
                    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;
            }
        }
    };
};

这里的主要修正是,将数组pending只保留单个回调的情势,改成同时保留肯定和否认的两种回调的情势。而且,在then中定义了默许的肯定和否认回调,使得then要领满足了promise的2个可选参数的请求。

你或许注重到defer中照样只要一个resolve要领,而没有相似jQuery的reject。那末,毛病处置惩罚要如何触发呢?请看这个例子:

var defer1 = defer(),
promise1 = defer1.promise;
promise1.then(function(value){
    console.log("1: value = ",  value);
    return reject("error happens"); 
}).then(function(value){
    console.log("2: value = ", value);
}).then(null, function(reason){
    console.log("3: reason = ", reason);
});
defer1.resolve(10);

// Result:
// 1: value = 10
// 3: reason = error happens

能够看出,每个通报给then要领的返回值是很主要的,它将决议下一个then要领的挪用结果。而假如像上面如许返回东西函数reject天生的对象,就会触发毛病处置惩罚。

融入异步

终究到了末了的design/q7.js。直到前面的q6,还存在一个题目,就是then要领运转的时刻,多是同步的,也多是异步的,这取决于通报给then的函数(比方直接返回一个值,就是同步,返回一个其他的promise,就能够是异步)。这类不确定性能够带来潜伏的题目。因而,Q的背面这一步,是确保将一切then转变为异步。

design/q7.js定义了另一个东西函数enqueue

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

明显,这个东西函数会将恣意函数推晚到下一个事宜队列运转。

design/q7.js其他的修正点是(只显示修正部份):

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

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

var defer = function () {
    var pending = [], value;
    return {
        resolve: function (_value) {
            // ...
                    enqueue(function () {
                        value.then.apply(value, pending[i]);
                    });
            // ...
        },
        promise: {
            then: function (_callback, _errback) {
                    // ...
                    enqueue(function () {
                        value.then(callback, errback);
                    });
                    // ...
            }
        }
    };
};

即把本来的value.then的部份,都转变为异步。

到此,Q供应的Promise设想道理q0~q7,悉数完毕。

结语

即使本文已是这么长的篇幅,但所报告的也只到基本的Promise。大部份Promise库会有更多的API来应对更多和Promise有关的需求,比方all()spread(),不过,读到这里,你已相识了完成Promise的中心理念,这肯定对你以后运用Promise有所协助。

在我看来,Promise是精致的设想,我花了相称一些时候才差不多明白它。Q作为一个典范Promise库,在思路上走得很明白。能够感受到,再庞杂的库也是先从基本的要点最先的,假如我们本身要做相似的事,也应当坚持如许的心态一点一点提高。

(从新编辑自我的博客,原文地点:http://acgtofe.com/posts/2015…

    原文作者:EdwardUp
    原文地址: https://segmentfault.com/a/1190000002591145
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞