前端文章收集

Promises的工作机制以及其他

2018-03-30  本文已影响14人  编程小王子

Promises的工作机制以及其他

本文旨在解释Promise的工作机制以及其他相关问题

项目地址

假如你在写一个不立即返回值的一个函数,最常用的API是将此值作为参数赋给一个回调函数而不是将此值直接返回

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

去除厄运的回调金字塔(Pyramid of Doom)

Javascript 中最常见的反模式做法是回调内部再嵌套回调。

// 回调金字塔
asyncOperation(function(data){
  // 处理 `data`
  anotherAsync(function(data2){
      // 处理 `data2`
      yetAnotherAsync(function(){
          // 完成
      });
  });
});

这当然是个很简单的问题了 但是这里还有很多改进的余地。

最常见的一种解决方案是提供一个返回对应值和抛出异常的匿名函数,很明显有好几种明确的方法来扩展回调模式来处理异常

其中之一就是如下所示,提供一个正常回调和一个异常回调:

var maybeOneOneSecondLater = function (callback, errback) {
    setTimeout(function () {
        if (Math.random() < .5) {
            callback(1);
        } else {
            errback(new Error("Can't provide one."));
        }
    }, 1000);
};

当然还存在其他方式,要么通过定位关键值(返回值)来抛出异常,但是这些都没有达到抛出异常的规范,抛出异常和try/catch语句块是为了推迟显示的异常的异常处理直到程序运行到了合适尝试修复异常的地方,这就需要隐式(表示发生了异常)传递异常 如果异常没有被正常处理。

Promises

通常,函数返回一个代表结果的Promise对象代表操作成功或者失败,代表字面上的意思承诺(一个不论成功和失败必然会执行的操作),
所谓Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息(成功实现或被拒绝)。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。如果Promise被拒绝并且拒绝没有被明确的监测到,任何派生的Promise会因为同样的原因被拒绝。

在这个例子里,我们将回调写在then函数里然后注册回调

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

maybeOneOneSecondLater().then(callback);

但这样的写法存在两个弊端:

更加通用的解决方案是在任意时间接受任意数量的回调,或者,我们可以让Promise对象同时拥有两个状态.

一个Promise最初是解决所有回调函数被添加到数组中待观察。当Promise得到解决时(resolved),所有的观察者都会被通知。在Promise解决之后新的回调被立即调用。我们通过等待回调的数组是否依然存在来区分状态的变化,然后在他们被解决后把他们抛出。

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

这样已经足够把它分解成工具函数是实用的。defer 是一个包含两部分的对象:一个用于注册观察者,另一个用于通知观察者。(参见 q/design/q0.js)

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);
            }
        }
    }
};

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

oneOneSecondLater().then(callback);

这个解决方案有一个缺陷:可以调用多次,更改promise结果的值。这不能模拟一个函数只返回一个值或抛出一个错误的事实。我们可以只允许第一个调用,从而防止意外或恶意重置。

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;
            } else {
                throw new Error("A promise can only be resolved once.");
            }
        },
        then: function (callback) {
            if (pending) {
                pending.push(callback);
            } else {
                callback(value);
            }
        }
    }
};

当然,我们还需要区分promise对象和其他值, (参见 q/design/q2.js)

var Promise = function () {
};

var isPromise = function (value) {
    return value instanceof Promise;
};

var defer = function () {
    var pending = [], value;
    var promise = new Promise();
    promise.then = function (callback) {
        if (pending) {
            pending.push(callback);
        } else {
            callback(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: promise
    };
};

使用原型式继承的缺点是promise的实例可以被在任何地方使用,从而导致一些依赖问题。

另外一个方法是使用duck-typing(动态类型)来区分promises对象和其他,比如CommonJS或Promises A建立的使用then方法来进行区分,当然,当别的对象恰巧也有then方法,这么做好像也不太合适,

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);
                }
            }
        }
    };
};

下一大步是让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;
    });
});

还有几点我们需要做到:

将已经完成的返回值转换成Promise对象并不难理解,这是一个立即通知观察者传入值已经被成功处理了。

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

这个方法可以变为强制转换参数成为一个promise对象不论他是一个value或者他已经是一个promise对象。

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

Now, we need to start altering our "then" methods so that they return promises for the return value of their given callback. The "ref" case is simple. We'll coerce the return value of the callback to a promise and return that immediately.

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

This is more complicated for the deferred since the callback will be called in a future turn. In this case, we recur on "defer" and wrap the callback. The value returned by the callback will resolve the promise returned by "then".

Furthermore, the "resolve" method needs to handle the case where the resolution is itself a promise to resolve later. This is accomplished by changing the resolution value to a promise. That is, it implements a "then" method, and can either be a promise returned by "defer" or a promise returned by "ref". If it's a "ref" promise, the behavior is identical to before: the callback is called immediately by "then(callback)". If it's a "defer" promise, the callback is passed forward to the next promise by calling "then(callback)". Thus, your callback is now observing a new promise for a more fully resolved value. Callbacks can be forwarded many times, making "progress" toward an eventual resolution with each forwarding.

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;
            }
        }
    };
};

The implementation at this point uses "thenable" promises and separates the "promise" and "resolve" components of a "deferred". (参见 q/design/q4.js)

Error Propagation
To achieve error propagation, we need to reintroduce errbacks. We use a new type of promise, analogous to a "ref" promise, that instead of informing a callback of the promise's fulfillment, it will inform the errback of its rejection and the reason why.

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

The simplest way to see this in action is to observe the resolution of an immediate rejection.

reject("Meh.").then(function (value) {
    // we never get here
}, function (reason) {
    // reason === "Meh."
});

We can now revise our original errback use-case to use the promise API.

var maybeOneOneSecondLater = function (callback, errback) {
    var result = defer();
    setTimeout(function () {
        if (Math.random() < .5) {
            result.resolve(1);
        } else {
            result.resolve(reject("Can't provide one."));
        }
    }, 1000);
    return result.promise;
};

To make this example work, the defer system needs new plumbing so that it can forward both the callback and errback components. So, the array of pending callbacks will be replaced with an array of arguments for "then" calls.

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;
            }
        }
    };
};

There is, however, a subtle problem with this version of "defer". It mandates that an errback must be provided on all "then" calls, or an exception will be thrown when trying to call a non-existant function. The simplest solution to this problem is to provide a default errback that forwards the rejection. It is also reasonable for the callback to be omitted if you're only interested in observing rejections, so we provide a default callback that forwards the fulfilled value.

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;
            }
        }
    };
};

At this point, we've achieved composition and implicit error propagation. We can now very easily create promises from other promises either in serial or in parallel (参见 q/design/q6.js). This example creates a promise for the eventual sum of promised values.

promises.reduce(function (accumulating, promise) {
    return accumulating.then(function (accumulated) {
        return promise.then(function (value) {
            return accumulated + value;
        });
    });
}, ref(0)) // start with a promise for zero, so we can call then on it
           // just like any of the combined promises
.then(function (sum) {
    // the sum is here
});

Safety and Invariants
Another incremental improvement is to make sure that callbacks and errbacks are called in future turns of the event loop, in the same order that they were registered. This greatly reduces the number of control-flow hazards inherent to asynchronous programming. Consider a brief and contrived example:

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

This function will either throw an exception or return a promise that will
quickly be fulfilled with the value of 10. It depends on whether foob()
resolves in the same turn of the event loop (issuing its callback on the same
stack immediately) or in a future turn. If the callback is delayed to a
future turn, it will allways succeed.
(参见 q/design/q7.js)

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

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++) {
                    // XXX
                    enqueue(function () {
                        value.then.apply(value, pending[i]);
                    });
                }
                pending = undefined;
            }
        },
        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 {
                    // XXX
                    enqueue(function () {
                        value.then(callback, errback);
                    });
                }
                return result.promise;
            }
        }
    };
};

var ref = function (value) {
    if (value && value.then)
        return 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;
        }
    };
};

There remains one safty issue, though. Given that any object that implements
"then" is treated as a promise, anyone who calls "then" directly is at risk
of surprise.

A "when" method wraps a promise and prevents these surprises.

We can also take the opportunity to wrap the callback and errback
so that any exceptions thrown get transformed into rejections.

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;
};

At this point, we have the means to protect ourselves against several
surprises including unnecessary non-deterministic control-flow in the course
of an event and broken callback and errback control-flow invariants.
(参见 q/design/q7.js)

消息传递/Message Passing

If we take a step back, promises have become objects that receive "then"
messages. Deferred promises forward those messages to their resolution
promise. Fulfilled promises respond to then messages by calling the callback
with the fulfilled value. Rejected promises respond to then messages by
calling the errback with the rejection reason.

We can generalize promises to be objects that receive arbitrary messages,
including "then/when" messages. This is useful if there is a lot of latency
preventing the immediate observation of a promise's resolution, as in a
promise that is in another process or worker or another computer on a network.

If we have to wait for a message to make a full round-trip across a network to
get a value, the round-trips can add up a lot and much time will be wasted.
This ammounts to "chatty" network protocol problems, which are the downfall
of SOAP and RPC in general.

However, if we can send a message to a distant promise before it resolves, the
remote promise can send responses in rapid succession. Consider the case
where an object is housed on a remote server and cannot itself be sent across
the network; it has some internal state and capabilities that cannot be
serialized, like access to a database. Suppose we obtain a promise for
this object and can now send messages. These messages would likely mostly
comprise method calls like "query", which would in turn send promises back.


We must found a new family of promises based on a new method that sends
arbitrary messages to a promise. "promiseSend" is defined by
CommonJS/Promises/D. Sending a "when" message is equivalent to calling the
"then" method.

promise.then(callback, errback);
promise.promiseSend("when", callback, errback);

We must revisit all of our methods, building them on "promiseSend" instead of
"then". However, we do not abandon "then" entirely; we still produce and
consume "thenable" promises, routing their message through "promiseSend"
internally.

function Promise() {}
Promise.prototype.then = function (callback, errback) {
    return when(this, callback, errback);
};

If a promise does not recognize a message type (an "operator" like "when"),
it must return a promise that will be eventually rejected.

Being able to receive arbitrary messages means that we can also implement new
types of promise that serves as a proxy for a remote promise, simply
forwarding all messages to the remote promise and forwarding all of its
responses back to promises in the local worker.

Between the use-case for proxies and rejecting unrecognized messages, it
is useful to create a promise abstraction that routes recognized messages to
a handler object, and unrecognized messages to a fallback method.

var makePromise = function (handler, fallback) {
    var promise = new Promise();
    handler = handler || {};
    fallback = fallback || function (op) {
        return reject("Can't " + op);
    };
    promise.promiseSend = function (op, callback) {
        var args = Array.prototype.slice.call(arguments, 2);
        var result;
        callback = callback || function (value) {return value};
        if (handler[op]) {
            result = handler[op].apply(handler, args);
        } else {
            result = fallback.apply(handler, [op].concat(args));
        }
        return callback(result);
    };
    return promise;
};

Each of the handler methods and the fallback method are all expected to return
a value which will be forwarded to the callback. The handlers do not receive
their own name, but the fallback does receive the operator name so it can
route it. Otherwise, arguments are passed through.

For the "ref" method, we still only coerce values that are not already
promises. We also coerce "thenables" into "promiseSend" promises.
We provide methods for basic interaction with a fulfilled value, including
property manipulation and method calls.

var ref = function (object) {
    if (object && typeof object.promiseSend !== "undefined") {
        return object;
    }
    if (object && typeof object.then !== "undefined") {
        return makePromise({
            when: function () {
                var result = defer();
                object.then(result.resolve, result.reject);
                return result;
            }
        }, function fallback(op) {
            return Q.when(object, function (object) {
                return Q.ref(object).promiseSend.apply(object, arguments);
            });
        });
    }
    return makePromise({
        when: function () {
            return object;
        },
        get: function (name) {
            return object[name];
        },
        put: function (name, value) {
            object[name] = value;
        },
        del: function (name) {
            delete object[name];
        }
    });
};

Rejected promises simply forward their rejection to any message.

var reject = function (reason) {
    var forward = function (reason) {
        return reject(reason);
    };
    return makePromise({
        when: function (errback) {
            errback = errback || forward;
            return errback(reason);
        }
    }, forward);
};

Defer sustains very little damage. Instead of having an array of arguments to
forward to "then", we have an array of arguments to forward to "promiseSend".
"makePromise" and "when" absorb the responsibility for handling the callback
and errback argument defaults and wrappers.

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++) {
                    enqueue(function () {
                        value.promiseSend.apply(value, pending[i]);
                    });
                }
                pending = undefined;
            }
        },
        promise: {
            promiseSend: function () {
                var args = Array.prototype.slice.call(arguments);
                var result = defer();
                if (pending) {
                    pending.push(args);
                } else {
                    enqueue(function () {
                        value.promiseSend.apply(value, args);
                    });
                }
            }
        }
    };
};

The last step is to make it syntactically convenient to send messages to
promises. We create "get", "put", "post" and "del" functions that send
the corresponding messages and return promises for the results. They
all look very similar.

var get = function (object, name) {
    var result = defer();
    ref(object).promiseSend("get", result.resolve, name);
    return result.promise;
};

get({"a": 10}, "a").then(function (ten) {
    // ten === ten
});

The last improvment to get promises up to the state-of-the-art is to rename
all of the callbacks to "win" and all of the errbacks to "fail". I've left
this as an exercise.

未来

Andrew Sutherland did a great exercise in creating a variation of the Q
library that supported annotations so that waterfalls of promise creation,
resolution, and dependencies could be graphically depicited. Optional
annotations and a debug variation of the Q library would be a logical
next-step.

There remains some question about how to ideally cancel a promise. At the
moment, a secondary channel would have to be used to send the abort message.
This requires further research.

CommonJS/Promises/A also supports progress notification callbacks. A
variation of this library that supports implicit composition and propagation
of progress information would be very awesome.

It is a common pattern that remote objects have a fixed set of methods, all
of which return promises. For those cases, it is a common pattern to create
a local object that proxies for the remote object by forwarding all of its
method calls to the remote object using "post". The construction of such
proxies could be automated. Lazy-Arrays are certainly one use-case.

上一篇下一篇

猜你喜欢

热点阅读