前端开发

从Google V8引擎剖析Promise实现

2019-05-26  本文已影响13人  tallriham

本文阅读的源码为Google V8 Engine v3.29.45,此版本的promise实现为js版本,在后续版本Google继续对其实现进行了处理。引入了es6语法等,在7.X版本迭代后,逐渐迭代成了C版本实现。

贴上源码地址,大家自觉传送。

代码中所有类似%functionName的函数均是C语言实现的运行时函数。

编程模型

常见的编程模型有三种:单线程同步模型、多线程同步模型、异步编程模型。

单线程同步模型 多线程同步模型 异步编程模型

单线程同步模型就像一个先入先出的队列,后一个任务必须等待前一个任务完成才能继续。多线程同步模型克服了单线程等待的问题,但是多线程执行由内核调度,线程间切换代价高,高并发环境下对CPU、内存消耗较大。Javascript的作者意识到大部分耗时来源于IO等待,没有必要为IO等待耗费昂贵的计算资源。同步任务必须在主线程上排队执行,前一个任务结束,后一个任务才能执行。异步任务可以被挂起进入异步队列等待调度。浏览器的异步队列分为两种,宏任务队列(Scirpt、setTimeout、setInterval)和微任务队列(Promise、process.nextTick)。Javascript运行时会将同步代码放入执行栈,当执行栈为空或者同步代码执行完毕,主线程会先执行微任务队列里的任务,再执行宏任务,如此反复执行,直到清空执行栈和异步队列。

Promise解决了什么问题

Javascript是函数式编程语言,函数是一等公民,异步任务被主线程挂起包装成回调函数放入任务队列。这样的设计解决了性能问题,但是多级回调的关联变得难以维护。Promise正是针对这个问题而诞生的。

Google V8 Promise.js实现

整体实现

整体结构

Promise.js声明$Promise构造函数,使用PromiseSet初始化Promise对象,每个Promise对象具有4个属性。

1. Promise#value(返回值)

2. Promise#status(状态,0代表pending,+1代表resolved,-1代表rejected)

3. Promise#onResolve(resolve后执行队列)

4. Promise#onReject(reject后执行队列)

Promise#raw变量可以看作Javascript中的空对象{}。

UNDEFINED, DONT_ENUM, InternalArray相当于Array函数。

global变量代表浏览器window对象,相当于Javascript代码:

var Global = (function() {

    try { return self.self } catch (x) {}

    try { return global.global } catch (x) {}

    return null;

})();

%AddNamedProperty宏可以将变量挂载在对象上,可以看作Javascript中代码:

function AddNamedProperty(target, name, value) {

    if (!_defineProperty) {

        target[name] = value;

        return;

    }

    _defineProperty(target, name, {

        configurable: true,

        writable: true,

        enumerable: false,

        value: value

    });

}

%AddNamedProperty(global, 'Promise', $Promise, DONT_ENUM)将$Promise构造函数挂载到浏览器window对象上。

随后使用InstallFunctions方法分别将defer、accept、reject、all、race、resolve挂载在$Promise上,chain、then、catch挂载在$Promise的原型链上。

InstallFunctions宏相当于Javascript代码:

function InstallFunctions(target, attr, list) {

    for (var i = 0; i < list.length; i += 2)

        AddNamedProperty(target, list[i], list[i + 1]);

}

我们来创建一个Promise对象看看它长什么样子。

> var p = new $Promise(function(){})

> Promise {Promise#status: 0, Promise#value: undefined, Promise#onResolve: Array(0), Promise#onReject: Array(0)}

Promise的底层依赖于微任务队列,Promise.js中的宏%EnqueueMicrotask会将异步函数挂起放入微任务队列中等待主线程调度。当时间片来临时,Javascrip解释器会依次执行微任务队列中的所有任务。你可以简单地使用setTImeout来模拟宏%EnqueueMicrotask的入队操作:

var runLater = (function() {

    return function(fn) { setTimeout(fn, 0); };

})();

var EnqueueMicrotask = (function() {

    var queue = null;

    function flush() {

        var q = queue;

        queue = null;

        for (var i = 0; i < q.length; ++i)

            q[i]();

    }

    return function PromiseEnqueueMicrotask(fn) {

        // fn must not throw

        if (!queue) {

            queue = [];

            runLater(flush);

        }

        queue.push(fn);

    };

})();

构造函数

如果你来实现Promise,最容易想到的就是构造一个函数,这个函数包含一个参数,这个参数同样是函数,他接受一个resolve和一个reject,最终的值通过参数函数内部调用resolve和reject来返回。源码的内部实现也差不多,构造函数$Promise调用PromiseSet方法返回一个对象,这个对象就是Promise。PromiseSet方法代码如下:

function PromiseSet(promise, status, value, onResolve, onReject) {

    SET_PRIVATE(promise, promiseStatus, status);

    SET_PRIVATE(promise, promiseValue, value);

    SET_PRIVATE(promise, promiseOnResolve, onResolve);

    SET_PRIVATE(promise, promiseOnReject, onReject);

    return promise;

}

SET_PRIVATE可以看作Javascript代码

function SET_PRIVATE(obj, prop, val) { obj[prop] = val; }

promiseStatus = "Promise#status"代表Promise的状态,初始值为0代表pending,它的状态只能改变1次,要么成功为+1,要么失败为-1。

promiseValue = "Promise#value"用来记录Promise回调运行结果。

promiseOnResolve = "Promise#onResolve"初始值为空数组,当异步操作串联前面的回调没有resolve的时候,promiseOnResolve用来记录后续成功回调操作。

promiseOnReject = "Promise#onReject"类似

此外构造函数还在内部调用了传入的函数resolver,并给resolver传入了两个值function(x) { PromiseResolve(promise, x) }function(r) { PromiseReject(promise, r) }),这两个值便是我们经常在Promise内执行的resolve和reject函数。好了,看来关键逻辑都在function(x) { PromiseResolve(promise, x) }function(r) { PromiseReject(promise, r) })这两个值上

PromiseResolve和PromiseReject

说到这PromiseResolvePromiseReject就不得不说PromiseDone,因为他们只是的PromiseDone语法糖。

PromiseResolve = function PromiseResolve(promise, x) {

    PromiseDone(promise, +1, x, promiseOnResolve);

};

PromiseReject = function PromiseReject(promise, r) {

    PromiseDone(promise, -1, r, promiseOnReject);

};

PromiseDone

Promise的状态只能改变一次,因此PromiseDone方法首先判断Promise状态是否改变,如果没有改变则调用函数PromiseEnqueue,然后调用PromiseSet函数改变了Promise的状态和返回值。

function PromiseDone(promise, status, value, promiseQueue) {

    if (GET_PRIVATE(promise, promiseStatus) === 0) {

        PromiseEnqueue(value, GET_PRIVATE(promise, promiseQueue), status);

        PromiseSet(promise, status, value);

    }

}

PromiseEnqueue

前面我们说过宏%EnqueueMicrotaskPromiseEnqueue函数使用宏%EnqueueMicrotask将任务task包装到PromiseHandle函数中压入微任务队列。

function PromiseEnqueue(value, tasks, status) {

    var id, name, instrumenting = DEBUG_IS_ACTIVE;

    %EnqueueMicrotask(function() {

      if (instrumenting) {

        %DebugAsyncTaskEvent({ type: "willHandle", id: id, name: name });

      }

      for (var i = 0; i < tasks.length; i += 2) {

        PromiseHandle(value, tasks[i], tasks[i + 1])

      }

      if (instrumenting) {

        %DebugAsyncTaskEvent({ type: "didHandle", id: id, name: name });

      }

    });

    if (instrumenting) {

      id = ++lastMicrotaskId;

      name = status > 0 ? "Promise.resolve" : "Promise.reject";

      %DebugAsyncTaskEvent({ type: "enqueue", id: id, name: name });

    }

  }

上面的代码很多是针对调试过程的,我们可以忽略宏%DebugAsyncTaskEvent的相关代码,上面的代码可以简化成Javascript代码:

function PromiseEnqueue(value, tasks, status) {

    EnqueueMicrotask(function() {

        for (var i = 0; i < tasks.length; i += 2)

            PromiseHandle(value, tasks[i], tasks[i + 1]);

    });

}

这里我们先考虑最简单的情况,先忽略then链式调用,因此task应该为空。到这里就可以理解new Promise(function(resolve, reject){resolve();})的运行过程了。

PromiseThen

下面我们进入then链式调用的过程,PromiseThen方法在原型链上,因此new Promise(function(resolve, reject){resolve();})返回的Promise对象可以调用then方法级联。PromiseThen方法内部主要通过PromiseChain方法实现。我们忽略特殊情况,x应该为resolve返回的值,因此PromiseChain方法接收onResolve和onReject两个函数。

PromiseThen = function PromiseThen(onResolve, onReject) {

    onResolve = IS_SPEC_FUNCTION(onResolve) ? onResolve : PromiseIdResolveHandler;

z    onReject = IS_SPEC_FUNCTION(onReject) ? onReject : PromiseIdRejectHandler;

    var that = this;

    var constructor = this.constructor;

    return PromiseChain.call(

        this,

        function(x) {

            x = PromiseCoerce(constructor, x);

            return x === that ? onReject(MakeTypeError('promise_cyclic', [x])) :

                IsPromise(x) ? x.then(onResolve, onReject) :

                onResolve(x);

        },

        onReject);

}

PromiseCoerce

承接上面x是resolve返回的值,下面的PromiseCoerce应该直接返回x。

function PromiseCoerce(constructor, x) {

    if (!IsPromise(x) && IS_SPEC_OBJECT(x)) {

        var then;

        try {

            then = x.then;

        } catch(r) {

            return PromiseRejected.call(constructor, r);

        }

        if (IS_SPEC_FUNCTION(then)) {

            var deferred = PromiseDeferred.call(constructor);

            try {

                then.call(x, deferred.resolve, deferred.reject);

            } catch(r) {

                deferred.reject(r);

            }

            return deferred.promise;

        }

    }

    return x;

}

PromiseChain

如果让你来实现Promise的链式调用,能够想到的方法或许是每个then都返回一个promise对象,这个返回的promise对象就是链式调用的关键。事实上Promise.js正是这么做的,只不过它做的更精妙一些,它封装了一个PromiseDeferred方法。每个PromiseChain内部都调用了PromiseDeferred获取一个deferred对象,这个对象包含一个status为0的promise对象和改变promise状态的resolve方法和reject方法。

function PromiseDeferred() {

    if (this === $Promise) {

        // Optimized case, avoid extra closure.

        var promise = PromiseInit(new $Promise(promiseRaw));

        return {

            promise: promise,

            resolve: function(x) { PromiseResolve(promise, x) },

            reject: function(r) { PromiseReject(promise, r) }

        };

    } else {

        var result = {};

        result.promise = new this(function(resolve, reject) {

            result.resolve = resolve;

            result.reject = reject;

        });

        return result;

    }

}

接着PromiseChain进行了一次switch判断,前面我们忽略了then链式调用,PromiseEnqueue的时候task为空。现在我们带上then的链式调用,当前序调用未完成,执行then的时候就匹配promiseStatus为0的情况,这时就将[onResolve, deferred]加入数组promiseOnResolve,[onReject, deferred]加入数组promiseOnReject,在执行微任务时依次执行。如果前序resolve调用完成,就会匹配promiseStatus为+1的情况,这时就可以执行OnResolve了,因为Promise是异步的不会立即执行,而是加入异步队列,因此将当前promise的onResolve使用PromiseEnqueue方法入队。前序reject的情况类似。最后返回一个新的promise对象deferred.promise,保证then的链式调用。到这里链式调用的逻辑就走通了。

PromiseChain = function PromiseChain(onResolve, onReject) {

    onResolve = IS_UNDEFINED(onResolve) ? PromiseIdResolveHandler : onResolve;

    onReject = IS_UNDEFINED(onReject) ? PromiseIdRejectHandler : onReject;

    var deferred = PromiseDeferred.call(this.constructor);

    switch (GET_PRIVATE(this, promiseStatus)) {

        case UNDEFINED:

            throw MakeTypeError('not_a_promise', [this]);

        case 0:  // Pending

            GET_PRIVATE(this, promiseOnResolve).push(onResolve, deferred);

            GET_PRIVATE(this, promiseOnReject).push(onReject, deferred);

            break;

        case +1:  // Resolved

            PromiseEnqueue(GET_PRIVATE(this, promiseValue), [onResolve, deferred], +1);

            break;

        case -1:  // Rejected

            PromiseEnqueue(GET_PRIVATE(this, promiseValue), [onReject, deferred], -1);

            break;

    }

    // Mark this promise as having handler.

    SET_PRIVATE(this, promiseHasHandler, true);

    return deferred.promise;

l}

PromiseAll

下面我们说说Promise的all方法,Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例,当所有实例都返回时,Promise.all才会完成,只要有一个reject,结果就会reject。

function PromiseAll(values) {

    var deferred = PromiseDeferred.call(this);

    var resolutions = [];

    if (!IsArray(values)) {

        deferred.reject(MakeTypeError('invalid_argument'));

        return deferred.promise;

    }

    try {

        var count = values.length;

        if (count === 0) {

            deferred.resolve(resolutions);

        } else {

            for (var i = 0; i < values.length; ++i) {

                this.resolve(values[i]).then(

                    (function() {

                        // Nested scope to get closure over current i (and avoid .bind).

                        var i_captured = i;

                        return function(x) {

                            resolutions[i_captured] = x;

                            if (--count === 0) deferred.resolve(resolutions);

                        };

                    })(),

                    function(r) { deferred.reject(r) });

            }

        }

    } catch (e) {

        deferred.reject(e);

    }

    return deferred.promise;

}

有了前面的分析,这里就可以看出PromiseAll使用PromiseDeferred创建了一个deferred对象,通过count--的逻辑,是否所有promise都返回,如果全都成功就会resolve,一个失败结果就会失败。PromiseAll在实际应用中可以实现并发的逻辑,几个没有依赖关系的接口可以并发调度,减少整个接口的响应延迟。

PromiseOne

PromiseOne的实现也类似,它在实际使用中可以实现接口响应超时的逻辑,例如一个http接口依赖于三方接口,三方接口阻塞怎么都不返回,使用PromiseOne可以实现如果三方接口2秒内不返回就直接给用户返回失败的逻辑,避免用户不必要的等待时间。

function PromiseOne(values) {

    var deferred = PromiseDeferred.call(this);

    if (!IsArray(values)) {

        deferred.reject(MakeTypeError('invalid_argument'));

        return deferred.promise;

    }

    try {

        for (var i = 0; i < values.length; ++i) {

            this.resolve(values[i]).then(

                function(x) { deferred.resolve(x) },

                function(r) { deferred.reject(r) });

        }

    } catch (e) {

        deferred.reject(e);

    }

    return deferred.promise;

}

上一篇下一篇

猜你喜欢

热点阅读