一切的开始
去年某天, t0 在群里扔了一道类似下面的题目 (不是这道, 这道来自 https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/):
- console.log('script start');
- setTimeout(function() {
- console.log('setTimeout');
- }, 0);
- Promise.resolve().then(function() {
- console.log('promise1');
- }).then(function() {
- console.log('promise2');
- });
- console.log('script end');
当时大家都说出一个理所应当的答案
- script start
- script end
- promise1
- promise2
- setTimeout
然而, 有细心的同学把用例放到 ie8 下面跑的时候就会发现结果有些细小的差异:
- script start
- script end
- setTimeout
- promise1
- promise2
这里冒出了 2 个问题:
ie8 并不支持 promise
ie8promise 的 callback 执行比 setTimeout 慢
第一个问题很快被解决, 因为打开的页面使用 mn([工程化, 开发构建利器]) 打包, 项目自带了 promise 补丁
第二个问题很令人困惑, 当时对 event loop 和 microTask 的概念还比较模糊, 并不能直接找出问题, 于是开始怀疑是 https://github.com/zloirock/core-js (mn 打的 promise-polyfill) 的 promise 实现有问题, 和原生的表现不一致, 从而开始拜读 corejs 的实现代码 (俄罗斯大哥真的牛逼, 一个人维护整个 core-js)
Promise Polyfill
promise 的 polyfill 有多个实现包, 包括著名的 bluebird, 但是这里只讨论 core-js 的实现
照例先上一段代码:
- // core-js/modules/_microtask.js
- // Node.js
- if (isNode) {
- notify = function () {
- process.nextTick(flush);
- };
- // browsers with MutationObserver
- } else if (Observer) {
- var toggle = true;
- var node = document.createTextNode('');
- new Observer(flush).observe(node, { characterData: true }); // eslint-disable-line no-new
- notify = function () {
- node.data = toggle = !toggle;
- };
- // environments with maybe non-completely correct, but existent Promise
- } else if (Promise && Promise.resolve) {
- var promise = Promise.resolve();
- notify = function () {
- promise.then(flush);
- };
- // for other environments - macrotask based on:
- // - setImmediate
- // - MessageChannel
- // - window.postMessag
- // - onreadystatechange
- // - setTimeout
- } else {
- notify = function () {
- // strange IE + webpack dev server bug - use .call(global)
- macrotask.call(global, flush);
- };
- }
代码很简单, 通过环境, 浏览器的版本等因素找到一个最适合的 promise 的实现, 具体思路如下:
1. node4+ 直接使用 process.nextTick
nextTick 是 node 的私有方法, 允许在当前调用栈结束立刻执行回调.
2. 在支持 MutationObserver 的浏览器使用 Observer
MutationObserver 用于监听 DOM 节点, 当节点发生变化的时候, 执行回调函数.(MutationObserver 同样在 vue 源码中用上了~) 当前 MutationObserver 的 proposal 已经被废弃.
3. 当前环境已经有打 promise 的补丁的话, 直接使用 promise 补丁
因为如果不支持 1,2 两种方式的话, 说明完全标准的 promise 补丁已经没法实现了, 这个时候只能委屈求全了.
4. 当以上情况都不适用, 使用下面的几种 backup
node0.8 - 的 process.nextTick(node0.8 - 的 process.nextTick 属于 macroTask https://github.com/nodejs/node/wiki/API-changes-between-v0.8-and-v0.10 )
Sphere(游戏引擎) 的 Dispatch 方法
MessageChannel(多线程通信, 双向, 支持 webworker)
window.postMessage (单向, 支持 webworker)
IE8 - 监听节点的 ONREADYSTATECHANGE 事件
setTimeout (最后的无奈)
补一张图片说明:
至此可以清晰的了解到 core-js 的补丁思路, 当 1,2 行的通的时候我们会得到一个 "相对标准" 的 promise.3,4 能让我们得到一个 "勉强实现的 promise"
这里回到我们的问题, 当文章的示例脚本跑在 IE8 的时候, 很明显我们逻辑会走到 ONREADYSTATECHANGE 这儿, 那么为什么监听的事件回调为什么比 setTimeout 慢执行呢? 是不是所有的事件回调都比 setTimeout 慢呢? http 请求的呢? 到底异步队列是怎么实现的? 标准如何?
故事很长, 下回再续.
参考资料
- https://developer.mozilla.org/zh-CN/docs/Web/API/MutationObserver
- Caniuse https://caniuse.com/#search=MutationObserver
macroTask and microTask https://github.com/YuzuJS/setImmediate#macrotasks-and-microtasks.
来源: https://juejin.im/entry/5ad85aabf265da504d63bede