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

本文思路的开始是模拟实现Promise,因此先来探讨Promise。vue

Promise 是异步编程的一种解决方案,最先是社区为了避免在回调地狱里沉沦而提出,ES6将其写进了语言标准,统一了用法,原生提供了Promise对象。node

Promise 简单说就是一个容器,里面保存着一个异步操做结束后的结果。git

promise.then(data => console.log(data))

// then 表示异步操做完成
// data 就是结果
复制代码

思考一个问题:如何在异步操做结束后,当即取得其结果?

好比这里有一个异步操做,用setTimeout模拟:github

let data = null
setTimeout(function() { data = 1 }, 1000) 
复制代码

当data发生改变后,我想“马上”输出。编程

第一种方法(显而易见):api

setTimeout(function () { 
    let data = 1 
    console.log(data)
}, 1000)
复制代码

第二种方法(略显而易见):promise

setTimeout(function() { 
    let data = 1 
    setTimeout(() => console.log(data), 0)
}, 1000)
复制代码

为何第二种方式也能取到?浏览器

先来读一遍教科书般(哪都能看到)的《JS事件循环机制》:bash

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
相关文章
相关标签/搜索