本文思路的开始是模拟实现Promise,所以先来探讨Promise。
Promise 是异步编程的一种解决方案,最早是社区为了不在回调地狱里沉沦而提出,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。
Promise 简单说就是一个容器,里面保存着一个异步操作结束后的结果。
promise.then(data => console.log(data))
// then 表示异步操作完成
// data 就是结果
思考一个问题:如何在异步操作结束后,立即取得其结果?
比如这里有一个异步操作,用setTimeout模拟:
let data = null
setTimeout(function() { data = 1 }, 1000)
当data发生改变后,我想“立刻”输出。
第一种方法(显而易见):
setTimeout(function () {
let data = 1
console.log(data)
}, 1000)
第二种方法(略显而易见):
setTimeout(function() {
let data = 1
setTimeout(() => console.log(data), 0)
}, 1000)
为什么第二种方式也能取到?
先来读一遍教科书般(哪都能看到)的《JS事件循环机制》:
1.所有任务都在主线程上执行,形成一个执行栈。
2.主线程之外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。
3.一旦"执行栈"中的所有同步任务执行完毕,系统就会读取"任务队列"。那些对应的异步任务,结束等待状态,进入执行栈并开始执行。
4.主线程不断重复上面的第三步。
然后我们来看第二种方法。
setTimeout(function() {
// 这个function内,对应一个执行栈
let data = 1 // 同步任务
setTimeout(() => console.log(data), 0) // 一个异步任务,执行到这时,会将该异步任务先放进"任务队列"
// 同步任务 "let data = 1" 执行完,执行异步任务
}, 1000)
这里调整两行代码顺序,结果是一样的。
setTimeout(function() {
setTimeout(() => console.log(data), 0)
let data = 1
}, 1000)
但是promise.then(data => console.log(data))结构不太一样,这里用一个回调函数取得异步操作后的结果。
他是怎么做到的。
// 简易 Promise 定义
function Promise(excutor) {
this.callback = function() {}
let that = this
function resolve(value) {
// 放置一个异步任务,在异步任务执行回调
setTimeout(() => that.callback(value), 0)
}
excutor(resolve)
}
// then 方法只是保存callback函数
Promise.prototype.then = function (callback) {
this.callback = callback
}
const promise = new Promise(function(resolve) {
setTimeout(() => resolve(1), 1000)
})
promise.then(data => console.log(data))
先把data => console.log(data)函数保存,再在resolve接收到异步数据后执行。
这里能按照这样的先后顺序,跟上面第二种方法道理是一样的。
setTimeout(function() {
setTimeout(() => console.log(data), 0) // => setTimeout(() => that.callback(value), 0) => 放置异步任务
let data = 1 // => promise.then(data => console.log(data)) => 都是同步任务
}, 1000)
这大致是promise的基本原理,以上我们使用setTimeout来实现异步任务,从而达到模拟promise的效果。
谈到异步任务,就要引申出微任务(micro task)和宏任务(macro task)了。
在浏览器中,异步任务大致有:
宏任务 (MacroTask):setTimeout、setInterval、I/O、UI渲染
微任务 (MicroTask):Promise、MutationObsever
在node环境中,异步任务大致有:
宏任务 (MacroTask):setTimeout、setInterval、I/O、setImmediate
微任务 (MicroTask):Promise、process.nextTick
在一个执行栈中,会先执行同步代码,遇到异步任务,会将其压到"任务队列"(task queue)中。
分别有"宏任务队列"、"微任务队列"。同步代码执行完,将"微任务队列"首任务的回调加入执行栈,执行。
循环"微任务队列",直到队列空。再循环"宏任务队列"。
不同的"宏任务"、"微任务"之间还有优先级,会影响其执行顺序。
回到Promise。
引擎里实现的Promise,会创建一个"微任务"。并且提供了一些api,Promise会尊崇一些规范。
所以,我们所说的模拟实现Promise,可以这样拆分。
- 使用哪种异步任务来模拟。
- Promise完整规范如何实现。
异步任务我们要看执行环境,有哪些可选。
至于规范,要看 Promise A+规范(原版) or Promise A+规范(翻译版)。
这里面最难理解的是Promise解决过程[[Resolve]](promise, x)。
Promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值,
我们表示为 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一个 Promise ,
解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来执行 promise 。
可以看到Promise A+规范是很细节的,要想完全通过他的测试,必须满足他的所有约束。
这里有篇文章实现的挺好---Promise原理讲解 && 实现一个Promise对象 (遵循Promise/A+规范)。
在他的resolve方法里可以看到,是用setTimeout来模拟。
其实最好是先用微任务模拟,如果环境不支持,再降级为宏任务。
这个思路类似与vue中的nextTick,源码传送门。
可以看到他的降级策略是:
Promise -> MutationObserver -> setImmediate -> setTimeout
nextTick的作用是在数据渲染完成后执行,它的道理是在当前执行栈底放入一个异步任务。
相关参考资料: