资讯专栏INFORMATION COLUMN

深入理解 Promise 实现细节

junbaor / 1003人阅读

摘要:显然,了解的实现细节,可以帮助我们更好地应用它。本文将主要根据的这篇文章,探讨的实现细节。核心说明尽管已经有自己的规范,但目前的各类库,在的实现细节上是有差异的,部分甚至在意义上完全不同。到前面到为止,所实现的都是不能级联的。

在之前的异步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...)

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/85560.html

相关文章

  • [译] 深入理解 Promise 五部曲:1. 异步问题

    摘要:当引擎开始执行一个函数比如回调函数时,它就会把这个函数执行完,也就是说只有执行完这段代码才会继续执行后面的代码。当条件允许时,回调函数就会被运行。现在,返回去执行注册的那个回调函数。 原文地址:http://blog.getify.com/promis... 在微博上看到有人分享LabJS作者写的关于Promise的博客,看了下觉得写得很好,分五个部分讲解了Promise的来龙去脉。从...

    CHENGKANG 评论0 收藏0
  • [译] 深入理解 Promise 五部曲:4. 扩展问题

    摘要:有一个和相关的更大的问题。最后,请负有责任感并且使用安全的扩展。深入理解五部曲异步问题深入理解五部曲转换问题深入理解五部曲可靠性问题深入理解五部曲扩展性问题深入理解五部曲乐高问题最后,安利下我的个人博客,欢迎访问 原文地址:http://blog.getify.com/promis... 现在,我希望你已经看过深入理解Promise的前三篇文章了。并且假设你已经完全理解Promises...

    Shimmer 评论0 收藏0
  • [译] 深入理解 Promise 五部曲:3. 可靠性问题

    摘要:简单的说,即将到来的标准指出是一个,所以作为一个,必须可以被子类化。保护还是子类化这是个问题我真的希望我能创建一个忠实的给及以下。 原文地址:http://blog.getify.com/promis... 如果你需要赶上我们关于Promise的进度,可以看看这个系列前两篇文章深入理解Promise五部曲--1.异步问题和深入理解Promise五部曲--2.控制权转移问题。 Promi...

    XboxYan 评论0 收藏0
  • ES2017异步函数现已正式可用

    摘要:标准已于年月份正式定稿了,并广泛支持最新的特性异步函数。为了领会,我们需要回到普通回调函数中进一步学习。从此编写回调函数不再那么痛苦。回调是一个函数,可以将结果传递给函数并在该函数内进行调用,以便作为事件的响应。 ES2017标准已于2017年6月份正式定稿了,并广泛支持最新的特性:异步函数。如果你曾经被异步 JavaScript 的逻辑困扰,这么新函数正是为你设计的。 异步函数或多或...

    android_c 评论0 收藏0
  • JavasScript重难点知识

    摘要:忍者级别的函数操作对于什么是匿名函数,这里就不做过多介绍了。我们需要知道的是,对于而言,匿名函数是一个很重要且具有逻辑性的特性。通常,匿名函数的使用情况是创建一个供以后使用的函数。 JS 中的递归 递归, 递归基础, 斐波那契数列, 使用递归方式深拷贝, 自定义事件添加 这一次,彻底弄懂 JavaScript 执行机制 本文的目的就是要保证你彻底弄懂javascript的执行机制,如果...

    forsigner 评论0 收藏0

发表评论

0条评论

junbaor

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<