本文阅读的源码为Google V8 Engine v3.29.45,此版本的promise实现为js版本,在后续版本Google继续对其实现进行了处理。引入了es6语法等,在7.X版本迭代后,逐渐迭代成了C版本实现。
贴上源码地址,大家自觉传送。
代码中所有类似%functionName的函数均是C语言实现的运行时函数。
事件循环
JavaScript的一大特点就是单线程,而这个线程中拥有唯一的一个事件循环。
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
任务队列又分为macro-task(宏任务)与micro-task(微任务)。
宏任务包括setTimeout, setInterval,微任务包括process.nextTick, Promise, Object.observe等。
不同的任务会进入不同的队列,而Promise属于微任务,会进入微任务队列进行处理。
编程模型
常见的编程模型有三种:单线程同步模型、多线程同步模型、异步编程模型。
单线程同步模型就像一个先入先出的队列,后一个任务必须等待前一个任务完成才能继续。多线程同步模型克服了单线程等待的问题,但是多线程执行由内核调度,线程间切换代价高,高并发环境下对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中的空对象{}。
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
说到这PromiseResolve和PromiseReject就不得不说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
前面我们说过宏%EnqueueMicrotask,PromiseEnqueue函数使用宏%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;
}