本文阅读的源码为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个属性。
-
Promise#value(返回值)
-
Promise#status(状态,0代表pending,+1代表resolved,-1代表rejected)
-
Promise#onResolve(resolve后执行队列)
-
Promise#onReject(reject后执行队列)
Promise#raw变量可以看作Javascript中的空对象{}。
InternalArray相当于Array函数。
global变量代表浏览器window对象,相当于Javascript代码:
%AddNamedProperty宏可以将变量挂载在对象上,可以看作Javascript中代码:
%AddNamedProperty(global, 'Promise', $Promise, DONT_ENUM)将$Promise构造函数挂载到浏览器window对象上。
随后使用InstallFunctions方法分别将defer、accept、reject、all、race、resolve挂载在$Promise上,chain、then、catch挂载在$Promise的原型链上。
InstallFunctions宏相当于Javascript代码:
我们来创建一个Promise对象看看它长什么样子。
Promise的底层依赖于微任务队列,Promise.js中的宏%EnqueueMicrotask会将异步函数挂起放入微任务队列中等待主线程调度。当时间片来临时,Javascrip解释器会依次执行微任务队列中的所有任务。你可以简单地使用setTimeout来模拟宏%EnqueueMicrotask的入队操作:
构造函数
如果你来实现Promise,最容易想到的就是构造一个函数,这个函数包含一个参数,这个参数同样是函数,他接受一个resolve和一个reject,最终的值通过参数函数内部调用resolve和reject来返回。源码的内部实现也差不多,构造函数$Promise调用PromiseSet方法返回一个对象,这个对象就是Promise。PromiseSet方法代码如下:
SET_PRIVATE可以看作Javascript代码
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语法糖。
PromiseDone
Promise的状态只能改变一次,因此PromiseDone方法首先判断Promise状态是否改变,如果没有改变则调用函数PromiseEnqueue,然后调用PromiseSet函数改变Promise的状态和返回值。
PromiseEnqueue
前面我们说过宏%EnqueueMicrotask,PromiseEnqueue函数使用宏%EnqueueMicrotask将任务task包装到PromiseHandle函数中压入微任务队列。
上面的代码很多是针对调试过程的,我们可以忽略宏%DebugAsyncTaskEvent的相关代码,上面的代码可以简化成Javascript代码:
这里我们先考虑最简单的情况,先忽略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两个函数。
PromiseCoerce
承接上面x是resolve返回的值,下面的PromiseCoerce应该直接返回x。
PromiseChain
如果让你来实现Promise的链式调用,能够想到的方法或许是每个then都返回一个promise对象,这个返回的promise对象就是链式调用的关键。事实上Promise.js正是这么做的,只不过它做的更精妙一些,它封装了一个PromiseDeferred方法。每个PromiseChain内部都调用了PromiseDeferred获取一个deferred对象,这个对象包含一个status为0的promise对象和改变promise状态的resolve方法和reject方法。
接着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的链式调用。到这里链式调用的逻辑就走通了。
PromiseAll
下面我们说说Promise的all方法,Promise.all方法用于将多个 Promise 实例,包装成一个新的 Promise 实例,当所有实例都返回时,Promise.all才会完成,只要有一个reject,结果就会reject。
有了前面的分析,这里就可以看出PromiseAll使用PromiseDeferred创建了一个deferred对象,通过count--的逻辑,是否所有promise都返回,如果全都成功就会resolve,一个失败结果就会失败。PromiseAll在实际应用中可以实现并发的逻辑,几个没有依赖关系的接口可以并发调度,减少整个接口的响应延迟。
PromiseOne
PromiseOne的实现也类似,它在实际使用中可以实现接口响应超时的逻辑,例如一个http接口依赖于三方接口,三方接口阻塞怎么都不返回,使用PromiseOne可以实现如果三方接口2秒内不返回就直接给用户返回失败的逻辑,避免用户不必要的等待时间。