阅读 40

从Promise的模拟实现看JS事件循环

本文思路的开始是模拟实现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,可以这样拆分。

  1. 使用哪种异步任务来模拟。
  2. 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的作用是在数据渲染完成后执行,它的道理是在当前执行栈底放入一个异步任务。

相关参考资料:

  1. 详解JavaScript中的Event Loop(事件循环)机制
  2. Promise A+规范
  3. vue/next-tick.js
关注下面的标签,发现更多相似文章
评论