Promise
我们通常学习 Promise 都是基于 Promises/A+ (opens new window) 的实现。但是我不得不告诉你,本文还将分析该 js 实现和 webkit 内核的 JS 引擎的 Promise 的实现差异。具体到代码运行上的差异
# 第一段代码
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
return new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
});
})
.then(() => {
console.log("外部第二个then");
});
这个输出还是比较简单的,外部第一个 new Promise 执行,执行完 resolve ,然后执行外部第一个 then 。外部第一个 then 方法里面 return 一个 Promise,这个 return ,代表 外部的第二个 then 的执行需要等待 return 之后的结果。当然会先执行完内部两个 then 之后,再执行 外部的第二个 then ,机智如你,完全正确。
output:
外部promise;
外部第一个then;
内部promise;
内部第一个then;
内部第二个then;
外部第二个then;
# 这是第二段代码
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
});
})
.then(() => {
console.log("外部第二个then");
});
这段代码和第一段代码就相差一个 return ,然而结果确是不一样的。
那这个怎么理解呢?
我们核心要看 then 的回调函数是啥时候注册的,我们知道,事件机制是 “先注册先执行”,即数据结构中的 “队列” 的模式,first in first out。那么重点我们来看下他们谁先注册的。
外部的第二个 then 的注册,需要等待 外部的第一个 then 的同步代码执行完成。 当执行内部的 new Promise 的时候,然后碰到 resolve,resolve 执行完成,代表此时的该 Promise 状态已经扭转,之后开始内部的第一个 .then 的微任务的注册,此时同步执行完成。我们知道需要执行的动作是一个微任务,那么自然要先执行完同步任务,比如如下:
new Promise((resolve, reject) => {
resolve();
console.log(111);
}).then(() => {
consle.log(222);
});
这个代码显然优先输出执行 1111,再执行 222。 因为 222 的输出是微任务的执行,111 是同步执行。
同理回到上面的代码,内部的 resolve 之后,当然是先执行内部的 new Promise 的第一个 then 的注册,这个 new Promise 执行完成,立即同步执行了后面的 .then 的注册。
然而这个内部的第二个 then 是需要第一个 then 的的执行完成来决定的,而第一个 then 的回调是没有执行,仅仅只是执行了同步的 .then 方法的注册,所以会进入等待状态。
这个时候,外部的第一个 then 的同步操作已经完成了,然后开始注册外部的第二个 then,此时外部的同步任务也都完成了。同步操作完成之后,那么开始执行微任务,我们发现 内部的第一个 then 是优先于外部的第二个 then 的注册,所以会执行完内部的第一个 then 之后,然后注册内部的第二个 then ,然后执行外部的第二个 then ,然后再执行内部的第二个 then。
output:
外部promise;
外部第一个then;
内部promise;
内部第一个then;
外部第二个then;
内部第二个then;
我们发现,这里显然是执行完一个 then ,接着会注册该 then 之后的下一个 then,按照任务队列的原理,我们可以发现,内外 then 是交替执行,然后交替注册的。所以才会出现输出内外交替内容。
另外,我这里所说的 then 的注册,是指微任务队列的注册,并不是 .then 的方法的执行,实际上 .then 方法的执行,我们可以理解为仅仅只是初始化而已。如果看过源码的会知道,.then 的执行确实是同步的,内部是再开启一个 new Promise ,但是由于上一个状态未流转,该 then 并不会此时注册到微任务队列中,而是会等待上一个的执行完成,所以我们把 .then 没注册微任务就理解成尚没执行是没有问题的。
# 再看第三段代码
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
let p = new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
});
p.then(() => {
console.log("内部第一个then");
});
p.then(() => {
console.log("内部第二个then");
});
})
.then(() => {
console.log("外部第二个then");
});
这段代码的差异,就是内部的 Promise 的代码的写法变了,不再是链式调用。
这里怎么理解呢?
这里在执行内部的 new Promise 的 resolve 执行完成之后(扭转了该 Promise 的状态),new Promise 之后的两个同步 p.then 是两个执行代码语句,都是同步执行,自然是会同步注册完。
两种方式的最主要的区别是:
- 链式调用的注册是前后依赖的 比如上面的外部的第二个 then 的注册,是需要外部的第一个的 then 的执行完成。
- 变量定义的方式,注册都是同步的 比如这里的 p.then 和 var p = new Promise 都是同步执行的。
所以这里的代码执行就比较清晰了,内部都执行完成之后(因为都优先于外部的第二个 then 的注册),再执行外部的第二个 then :
output:
外部promise;
外部第一个then;
内部promise;
内部第一个then;
内部第二个then;
外部第二个then;
# 第四段代码
let p = new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
});
p.then(() => {
console.log("外部第一个then");
new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
});
});
p.then(() => {
console.log("外部第二个then");
});
这段代码中,外部的注册采用了非链式调用的写法,根据上面的讲解,我们知道了外部代码的 p.then 是并列同步注册的。所以代码在内部的 new Promise 执行完,p.then 就都同步注册完了。
内部的第一个 then 注册之后,就开始执行外部的第二个 then 了(外部的第二个 then 和 外部的第一个 then 都是同步注册完了)。然后再依次执行内部的第一个 then ,内部的第二个 then。
output:
外部promise;
外部第一个then;
内部promise;
外部第二个then;
内部第一个then;
内部第二个then;
我相信,如果能看懂上面的四段代码之后,对 Promise 的执行和注册非常了解了。
如果还是不太懂,麻烦多看几遍,相信你一定能懂~~~~~~~~
核心思想:
Promise 的 then 的 注册微任务队列 和 执行 是分离的。
注册 : 是完全遵循 JS 和 Promise 的代码的执行过程。
执行 : 先 同步,再 微任务 ,再 宏观任务。
只有分开理解上述,才能真正理解它们的执行顺序~~~~~~~~~~~~~~~~
# 第五段代码
经过上面仔细深度文字解析之后,我相信你会豁然开朗了。
再来一道巩固的题目:
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
})
.then(() => {
console.log("内部第二个then");
});
return new Promise((resolve, reject) => {
console.log("内部promise2");
resolve();
})
.then(() => {
console.log("内部第一个then2");
})
.then(() => {
console.log("内部第二个then2");
});
})
.then(() => {
console.log("外部第二个then");
});
这段代码,其实就是结合了第一道题目和第二道题目综合而成。
外部的第二个 then ,依赖于内部的 return 的执行结果,所以会等待 return 执行完成。
内部的第一段 new Promise 变成和内部的第二段 new Promise 的交替输出了,理解方式和第二段代码一样。
output:
外部promise;
外部第一个then;
内部promise;
内部promise2;
内部第一个then;
内部第一个then2;
内部第二个then;
内部第二个then2;
外部第二个then;
# Promise/A+ 和 webkit 内核的 JS 引擎的 Promise 的实现差异
我们知道 ES6 的 Promise 是需要考虑向下兼容的,开发当中往往没有用系统内核的 Promise ,而是使用 npm install promise 来引入的。那么 promise 的 js 实现和浏览器的实现是完全一致的吗?
按照上面的四段代码的解析,我们理解到了,Promise 的 then 的执行,是依赖于上一个 then 的执行完成之后,即 resolve 状态之后,才开始注册到微任务队列中的。
我们一起看一道题目,这里的区别是 then 返回了一个 Promise.resolve();
new Promise((resolve, reject) => {
console.log("外部promise");
resolve();
})
.then(() => {
console.log("外部第一个then");
new Promise((resolve, reject) => {
console.log("内部promise");
resolve();
})
.then(() => {
console.log("内部第一个then");
return Promise.resolve();
})
.then(() => {
console.log("内部第二个then");
});
})
.then(() => {
console.log("外部第二个then");
})
.then(() => {
console.log("外部第三个then");
});
我们先忽略内部第一个 then 的 return ,按照上面所学习到的,正常理解,我们能得出依然是内外交替注册和运行。
output:
外部promise;
外部第一个then;
内部promise;
内部第一个then;
外部第二个then;
内部第二个then;
外部第三个then;
此题执行顺序图:
上面我们是使用 Promise 的 js 实现的代码输出的结果。
然而你把这段代码放在 chrome/safari 上跑一下,发现结果不一样,如下是 webkit 内核浏览器跑出来的结果。
这个是什么原因呢?
为啥多了一个 return Promise.resolve(),就把外层的 then 都执行完了呢?
要理解这个,我们还是要从注册和执行来区分理解。
在执行输出 “内部第一个 then ”之后,碰到 return Promise.resolve();我们就来分析这个 Promise.resolve();
Promise/A+ 的实现:
执行 return Promise.resolve() ,创建一个 Promise 实例,将 Promise 实例设置为 resolve 状态,这个 Promise.resolve() 是同步的,且该 Promise 已经完成了,所以他并不会影响到其他 then 的注册。所以上述我们分析是完全正确的。
如下是 Promise.resolve 的实现,我们发现,完全是同步的,所以不影响最终结果。
Promise.resolve = function(value) {
if (value instanceof Promise) return value;
if (value === null) return NULL;
if (value === undefined) return UNDEFINED;
if (value === true) return TRUE;
if (value === false) return FALSE;
if (value === 0) return ZERO;
if (value === "") return EMPTYSTRING;
if (typeof value === "object" || typeof value === "function") {
try {
var then = value.then;
if (typeof then === "function") {
return new Promise(then.bind(value));
}
} catch (ex) {
return new Promise(function(resolve, reject) {
reject(ex);
});
}
}
return valuePromise(value);
};
Promise 的 浏览器(webkit)的实现:
执行 return Promise.resolve() ,创建一个 Promise 实例,执行 resolve ,此时将该 Promise 的 resolve 的 value(这里是 undefined) 进入微任务队列,将该 Promise 的状态扭转为 resolve。然后接着执行了之前注册好的 "外部第二个 then" ,然后注册 “外部第三个 then” ,接着执行 “内部第一个 then” 的 return 的 resolve 的这个 undefined value 的 Promise,执行完成之后,然后注册下一个 then ,但是没有下一个 then 了,执行完成,整个 return 任务完成,本次同步任务也执行完成,接着执行注册的 “外部第三个 then” ,执行完成之后,注册 “外部第四个 then”,此时 ”内部第一个 then“ 执行完成,注册 ”内部第二个 then”,最后执行完“外部第四个 then”,再执行 刚刚注册的“内部第二个 then”.
源代码如下:
void Promise::Resolver::Resolve(Handle<Value> value) {
i::Handle<i::JSObject> promise = Utils::OpenHandle(this);
i::Isolate* isolate = promise->GetIsolate();
LOG_API(isolate, "Promise::Resolver::Resolve");
ENTER_V8(isolate);
EXCEPTION_PREAMBLE(isolate);
i::Handle<i::Object> argv[] = { promise, Utils::OpenHandle(*value) };
has_pending_exception = i::Execution::Call(
isolate,
isolate->promise_resolve(),
isolate->factory()->undefined_value(),
arraysize(argv), argv,
false).is_null();
EXCEPTION_BAILOUT_CHECK(isolate, /* void */ ;);
}
PromiseResolve = function PromiseResolve(promise, x) {
PromiseDone(promise, +1, x, promiseOnResolve);
};
function PromiseDone(promise, status, value, promiseQueue) {
if (GET_PRIVATE(promise, promiseStatus) === 0) {
PromiseEnqueue(value, GET_PRIVATE(promise, promiseQueue), status);
PromiseSet(promise, status, value);
}
}
有了上面的理解之后,如果外层再加一个 then ,那么也知道结果了,执行完刚刚注册的 “内部第二个 then”,之后,开始执行注册的 “外部第五个 then”。
# 巩固一下
结合上面已经学会的 Promise 的执行顺序,你应该能答出如下这道题的答案了吧,如果还不会,可以考虑再仔细看一篇。
new Promise((resolve, reject) => {
console.log("外部 promise");
resolve();
})
.then(() => {
console.log("外部第一个 then");
new Promise((resolve, reject) => {
console.log("内部 promise");
resolve();
})
.then(() => {
console.log("内部第一个 then");
})
.then(() => {
console.log("内部第二个 then");
});
return new Promise((resolve, reject) => {
console.log("内部 promise2");
resolve();
})
.then(() => {
console.log("内部第一个 then2");
})
.then(() => {
console.log("内部第二个 then2");
});
})
.then(() => {
console.log("外部第二个 then");
});