js异步机制、事件循环、Promise执行顺序

看了阮老师的《再谈Event Loop》,有些地方还是不太清楚,所以又查了一些资料,资料链接在下面的参考中,总结一下。

线程

浏览器是多线程的,其中有

  • 渲染引擎线程:负责页面的渲染
  • JS引擎线程:负责JS的解析和执行
  • 定时触发器线程:处理定时事件,比如setTimeout, setInterval
  • 事件触发线程:处理DOM事件
  • 异步http请求线程:处理http请求

渲染引擎就是如何渲染页面,Chrome/Safari/Opera用的是Webkit引擎,IE用的是Trident引擎,FireFox用的是Gecko引擎。不同的引擎对同一个样式的实现不一致,就导致了经常被人诟病的浏览器样式兼容性问题。

不同浏览器的JS引擎也各不相同,Chrome用的是V8,FireFox用的是SpiderMonkey,Safari用的是JavaScriptCore,IE用的是Chakra。

其中渲染线程和JS引擎线程不能同时进行。平时我们说的js单线程只是JS引擎线程,而异步就是靠这些其他的线程来处理实现的,然后再通过事件循环、任务队列来传到js线程中。

事件循环

首先我们都知道js有同步和异步任务。

  • 同步任务是指在js主线程上排队执行的任务,只有前一个任务执行完毕,后一个同步任务才能执行。

  • 异步任务不在主线程执行,任务在主线程定义后到其他线程去执行,执行完毕后,会将结果放入任务队列,主线程的执行栈为空时,会读取任务队列,执行其中的任务。每个异步任务都和回调函数相关联。

JS引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环。

每一个 JavaScript 运行的"线程环境"都有一个独立的 Event Loop,每一个 Web Worker 也有一个独立的 Event Loop。

异步任务可分为 task 和 microtask 两类,不同的API注册的异步任务会依次进入自身对应的队列中,然后等待 Event Loop 将它们依次压入执行栈中执行。

具体过程

  • 执行完主执行线程中的任务。
  • 取出Microtask Queue中任务执行直到清空。
  • 取出Macrotask Queue(task)中一个任务执行。
  • 取出Microtask Queue中任务执行直到清空。
  • 重复3和4。

task主要包含:script(整体代码)、setTimeout、setInterval、I/O、UI交互事件、postMessage、MessageChannel、setImmediate(Node.js 环境)

microtask主要包含:Promise.then、MutaionObserver、process.nextTick(Node.js 环境)

在 Node 中,会优先清空 next tick queue,即通过process.nextTick 注册的函数,再清空 other queue,常见的如Promise;此外,timers(setTimeout/setInterval) 会优先于 setImmediate 执行,因为前者在 timer 阶段执行,后者在 check 阶段执行。

setTimeout/Promise 等API便是任务源,而进入任务队列的是他们指定的具体执行任务。来自不同任务源的任务会进入到不同的任务队列。其中setTimeout与setInterval是同源的。

由于JS引擎线程空闲后,会先查看是否有事件可执行,接着再处理其他异步任务。所以如果click事件和setTimeout都该执行时,先执行click事件的回调函数。例如下面程序

setTimeout(function(){
    console.log('timer');
}, 0)

function waitFiveSeconds(){
    var now = (new Date()).getTime();
    while(((new Date()).getTime() - now) < 5000){}
    console.log('finished waiting');
}

document.addEventListener('click', function(){
    console.log('click');
})

console.log('click begin');
waitFiveSeconds();

程序开始后,在5s内点击一次触发click事件,输出结果为

click begin
finished waiting
click
timer

另一个例子

console.log('script start');

setTimeout(function() {
  console.log('timeout1');
}, 10);

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');
script start
promise1
script end
then1
timeout1
timeout2

再两个例子

// example 1
new Promise(resolve => {
    resolve(1)
    Promise.resolve().then(() => console.log(42)) // then1
    console.log(4)
}).then(t => console.log(t)) // then2
console.log(3)

// 输出结果依此为:4, 3, 42, 1

//example 2
let thenable = {
  then: (resolve, reject) => {
    resolve(42)
  }
}
new Promise(resolve => {
    resolve(1)
    Promise.resolve(thenable).then((t) => console.log(t)) // then1
    console.log(4)
}).then(t => console.log(t)) // then2
console.log(3)

// 输出结果依此为:4, 3, 1, 42

在Promise.resolve()的四种参数情况中的“无参数”,直接返回一个resolved状态的 Promise 对象,此时resolve的 Promise 对象是在本轮“事件循环”(event loop)的结束时,而不是在下一轮“事件循环”的开始时。执行到‘Promise.resolve().then(() => console.log(42))’的回调函数then()被立即放入Microtask Queue中记为then1,然后继续执行外层回调函数then(),放入Microtask Queue中记为then2,所以then1优先于then2执行,先输出42后输出1

node事件循环

前面说的都是浏览器中的事件循环

node中的有些不同

process.nextTick方法可以在当前"执行栈"的尾部----下一次Event Loop(主线程读取"任务队列")之前----触发回调函数。也就是说,它指定的任务总是发生在所有异步任务之前。setImmediate方法则是在当前"任务队列"的尾部添加事件,也就是说,它指定的任务总是在下一次Event Loop时执行,这与setTimeout(fn, 0)很像。

在node中事件每一轮循环按照顺序分为6个阶段,来自libuv的实现:

  • timers:执行满足条件的setTimeout、setInterval回调。
  • I/O callbacks:是否有已完成的I/O操作的回调函数,来自上一轮的poll残留。
  • idle,prepare:可忽略
  • poll:等待还没完成的I/O事件,会因timers和超时时间等结束等待。
  • check:执行setImmediate的回调。
  • close callbacks:关闭所有的closing handles,一些onclose事件。

按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空NextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:

  • 清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
  • 清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
  • 进入下轮循环。

参考

https://juejin.im/post/5a6ad46ef265da3e513352c8
http://www.ruanyifeng.com/blog/2014/10/event-loop.html
https://github.com/dwqs/blog/issues/61
https://juejin.im/post/5aa5dcabf265da239c7afe1e

你可能感兴趣的:(javascript)