Promise的实现与标准

转载同事写的文章,从promise的标准角度来说明其实现,这样不管在看Q,还是bluebird的时候,都会容易很多。

JS是如何运行的

每当谈起JS的时候,单线程异步回调非阻塞event loop这些词汇总是会出现。但是JS到底是如何运行的呢,不妨看一看Philip Roberts在JSConf上讲解event loop的视频。Philip Roberts还自己动手做了一个JS runtime的可视化程序,这个可视化程序他在演讲中也有展示。
让我们来看一段代码吧

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

process.nextTick(function() {
    console.log('nextTick');
});

setImmediate(function() {
    console.log('setImmediate');
});

console.log('script end');

可以看到这段代码中用到了setTimeoutprocess.nextTicksetImmediate,这三个方法在JS中都是异步去执行的,输出结果如下

script start
script end
nextTick
setTimeout
setImmediate

为什么同样是异步方法process.nextTick中的回调函数就会在setTimeoutsetImmediate中的回调函数之前执行?这就和task和microtask的机制有关了。关于task和microtask这里有一篇文章(Tasks, microtasks, queues and schedules)讲述的非常好,可以观摩一下,学习学习。

为了更好的理解JS是如何运行的,可以看下图。


image.png

在JS运行的过程中,event loop每一次循环都会将一个task从Task Queue中取出,task执行的过程中会调用不同的函数并压栈。栈中的代码会调用一些API,当这些API执行结束后会将完成的任务加入Task Queue(具体的实现因API而异,有些API可能会在单独的线程中去处理这些操作)。当stack中的代码全部执行完成时会再次从Task Queue中取出一个新的任务来执行,这样就开始了新的一轮loop。

那么Microtask是在什么时候执行的呢?JS会在每一轮loop结束,也就是stack中的代码全部执行完毕时,去执行Microtask Queue中的任务。当Microtask Queue中的任务全部执行完成后再从Task Queue中取出下一个任务。可以理解为执行过程为

Task1 -> Microtask ->Task2

以Task方式运行的有setTimeOutsetImmediate,而已MicroTask方式运行的有process.nextTickMutationObserver。这也就是上面的例子中process.nextTick中回调优先执行的原因。因为process.nextTick中回调被添加到了Microtask Queue,而setTimeOutsetImmediate中的回调则被添加到了Task Queue的末尾,他们在之后的几轮loop中才会被执行。

这里需要提一下有些地方将Task称为MacroTask,将MicroTask称为Jobs。

而在一些具体的实现中,可能会存在多个Task Queue,根据不同的实现目的不同的Task Queue之间存在不同的优先级(例如有些浏览器可能更加注重UI渲染的性能,所以将UI相关任务的Task Queue优先级提高)。

猜测Promise的实现

熟悉Promise的人都知道Promise有三个状态,pendingreslovedrejected。一旦Promise的状态发生改变就再也不会变动,且Promise包含的值也不会被改变。

//e.g.1
console.log('script start');

let promise = new Promise(function(resolve, reject) {
    console.log('in promise');
    resolve('reslove promise');
});

promise.then(function(value) {
    console.log('resolve: ', value);
}, function(reason) {
    console.log('reason: ', reason);
});

console.log('script end')

上面这段代码对于经常使用Promise的人再简单不过了,可以看下他的输出结果。

script start
in promise
script end
resolve:  reslove promise

e.g.1的输出结果可以看到传给then方法的回调是在最后执行的,所以可以判断出new Promise(function)中的function是同步执行的,而then(reslove,reject)中的resolve或reject是异步执行的。

熟悉Promise的人对下面一段代码也自然不会感到陌生。

//e.g.2
promise.then((value) => {
    //do some stuff
}).then((value) => {
    //do some stuff
}).then((value) => {
    //do some stuff
}).catch((reason) => {
    //do some stuff
});

为什么Promise能写成链式的,在.then之后还能接着.then?基于这一点可以判断出then方法return的是一个Promise,那么既然是Promise就一定会有状态,那么调用then之后return的这个Promise的状态是如何确定的呢?接着看下面的栗子。

//e.g.3
let promise1 = new Promise(function(resolve, reject) {
    resolve('reslove promise');
});

let promise2 = promise1.then(function onReslove(value) {
    console.log('1 resolve: ', value);
    return 1;
}, function onReject(reason) {
    console.log('1 reason: ', reason);
});

promise2.then(function onReslove(value) {
    console.log('2 resolve: ', value);
}, function onReject(reason) {
    console.log('2 reason: ', reason);
});

执行结果:
1 resolve:  reslove promise
2 resolve:  1

可以看到当在onReslove中返回一个基础类型的时候promise2的状态变成了resolved
如果把上面的return 1;改为throw new Error('error');会是什么样呢?输出结果如下:

1 resolve:  reslove promise
2 reason:  Error: error
    at onReslove (/Users/lx/Documents/projects/VsTest/PromiseExample.js:40:11)
    at <anonymous>
    at process._tickCallback (internal/process/next_tick.js:188:7)
    at Function.Module.runMain (module.js:607:11)
    at startup (bootstrap_node.js:158:16)
    at bootstrap_node.js:575:3

可以看到此时promise2的状态变为了rejected。那么如果我在onResolve()中return一个处于不同状态Promise会怎么样呢?

//e.g.4
let promise1 = new Promise(function(resolve, reject) {
    resolve('reslove promise');
});

let promise2 = promise1.then(function onReslove(value) {
    console.log('1 resolve: ', value);
    return promiseReturn;
}, function onReject(reason) {
    console.log('1 reason: ', reason);
});

promise2.then(function onReslove(value) {
    console.log('2 resolve: ', value);
    return 1;
}, function onReject(reason) {
    console.log('2 reason: ', reason);
});

//依次使promiseReturn等于以下值:

//pending状态的Promise,5s后变为resolved状态
let promiseReturn = new Promise(function(reslove, reject) {
    setTimeout(() => {
        reslove(1)
    }, 5000);
});

//输出结果为:
1 resolve:  reslove promise
//5s之后
2 resolve:  1

//resolved状态的Promise
let promiseReturn = Promise.resolve(1);

//输出结果为:
1 resolve:  reslove promise
2 resolve:  1

//rejected状态的Promise
let promiseReturn = Promise.reject(new Error('error'));

//输出结果为:
1 resolve:  reslove promise
2 reason:  Error: error
    at Object.<anonymous> (/Users/lx/Documents/projects/VsTest/PromiseExample.js:33:36)
    at Module._compile (module.js:569:30)

通过上面的例子可以看到,当onResolve()return一个Promise时,promise2的状态是和return的Promise的状态相同的。

PromiseA+标准

[图片上传失败...(image-a86d48-1516949283239)]

ES标准中的Promise,Q以及bluebird都是PromiseA+标准的实现。 PromiseA+标准主要从三部分提出了对Promise实现的要求,第一部分规定了Promise的状态已经状态的变化。第二部分则指定Promise的then方法的行为。第三部分则是说明了如何决定then方法返回的Promise的状态,并且支持了不同PromiseA+标准实现的Promise之间的兼容性。

PromiseA+标准如下(更具体的标准戳这里)

Promise的状态

Promise必须处于pending,resolved,rejected三个状态之一

  • 当Promise处于pending状态时可以转换到resolvedrejected状态
  • 当Promise处于resolved状态时无法再转换到其他状态,并且有一个无法改变value
  • 当Promise处于rejected状态时无法再转换到其他状态,并且有一个无法改变的reason(reason一般为一个Error对象)

Promise的then方法

Promise的then方法接受两个参数

promise.then(onResolved, onRejected);
  • onResolvedonRejected参数都是可选的,如果onResolvedonRejected不是function,则忽略相应的参数。onResolvedonRejected都不能被调用超过一次。

  • onResolvedonRejected需要通过异步的方式执行,可以用“macro-task”或“micro-task”机制来执行。

  • 同一个Promise的then方法可以被调用多次,当该Promise状态变为resolvedrejected状态时,注册在该Promise上的回调应该根据注册的顺序被调用。

  • then方法会返回一个Promise

    promise2 = promise1.then(onResolved, onRejected);
    
    1. 如果onResolvedonRejected返回一个x,那么promise2的状态需要根据x来决定(至于如何决定promise2的状态,会在第三部分中说明)。
    2. 如果onResolvedonRejected抛出一个异常e,那么promise2必须rejected且reason = e
    3. 如果promise1是resolved状态且onResolved不是一个function那么promise2必须resolved,并且promise2的value必须与promise1相同
    4. 如果promise1是rejected状态且onRejected不是一个function那么promise2必须rejected,并且promise2的reason必须与promise1相同

The Promise Resolution Procedure

个人感觉这个标题不好“生翻”,直面的翻译可能反倒容易让人误解。可以把这个部分理解为一种操作,该操作需要接受两个参数(promise, x),会根据x的情况来决定promise的状态。
在我们的onResolved回调中一般会return一个value(如果没有写return xxx,那么value就等于undefined)。这里就可以把x当做这个value。调用then方法时返回的Promise的状态就是由这个x来决定的。
如果x是一个thenable(带有then方法的对象或function),那么可以假设x和Promise的行为相似。这一点是为了让不同PromiseA+标准的实现可以兼容。

The Promise Resolution Procedure这个操作的步骤如下:

  • 1.如果xpromise是同一个对象的引用(x === promise),那么reject promise并将一个TypeError赋值给reason

  • 2.如果x是一个Promise(x instanceof Promise),那么promise的状态入下:

    • 2.1 如果x处于pending状态那么promise也处于pending状态,直到x状态变为resolved或rejected。

    • 2.2 如果x处于resolved状态,那么用x的value来resolve promise

    • 2.3 如果x处于rejected状态,那么用x的reason来reject promise

  • 3.如果x是一个对象或function

    • 3.1 如果获取属性x.then的过程中抛出异常e,那么将e作为reason来reject promise

    • 3.2 如果x.then是一个function,那么调用x.then传入参数resolvePromiserejectPromise

      • 3.2.1 如果resolvePromise被调用且传入的参数为y,那么再次执行此操作,参数为(promise, y)

      • 3.2.2 如果rejectPromise被调用且传入的参数r,那么将r作为reason来reject promise

      • 3.2.3 如果resolvePromiserejectPromise同时被调用,或者被调用多次,那么优先处理第一次调用,之后的调用都应该被忽略。

      • 3.2.4 如果调用x.then抛出了异常e,若在抛出异常前resolvePromiserejectPromise已经被调用,那么忽略异常即可。若resolvePromiserejectPromise没有被调用过,那么将e作为reason来reject promise

    • 3.3 如果x.then不是一个function,那么用x来resolve promise

  • 4.如果x既不是对象也不是function,那么用x来resolve promise

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容