Javascript的并发模型与事件循环(Concurrency model and Event Loop)

为什么要关注这么晦涩的东西

  1. 面试要问的,不问显不出高大上
  2. 就编程来说,自己写的代码,要能正确的预期最终输出的结果

虽然说在实际的业务代码开发过程中,不会写得这么晦涩难懂,或者预期不了结果的代码,但是下雨天打孩子,闲着也是闲着。

Javascript最大的特点就是单线程,所谓单线程就是一次只能干一件事情。 但为啥不是多线程呢? JS的主要作用场景就是操作DOM。如果2个线程同时操作一个DOM,结果将无法预期。

但是现在出现了Web Worker,可以为 JavaScript 创造多线程环境。
允许主线程创建 Worker 线程,将一些任务分配给后者运行。在主线程运行的同时,Worker 线程在后台运行,两者互不干扰。等到 Worker 线程完成计算任务,再把结果返回给主线程。这样的好处是,一些计算密集型或高延迟的任务,被 Worker 线程负担了,主线程(通常负责 UI 交互)就会很流畅,不会被阻塞或拖慢。但是web worker不能操作DOM。

关于Event Loop的现实原型的类比

很简单,去银行办理业务(打印流水和办理网银),大厅里面有很多人取号排队。每一号(或人)就是一个宏任务。银行的叫号的系统就是一个消息队列。因为js是单线程,所以就假设银行只有一个窗口开放。 窗口问你:“先生/ 女士,需要办理什么业务?” 你说:“打印最近一个月的流水。” 流水打完。 窗口问:“请问还需要办理其他业务吗?” 你回答:“还要办理网银。” 办理网银就是一个微任务。当一个人(宏任务)所有的业务都办理完成,(消息队列)才会叫下一个号。 这大约就是整个Event Loop的过程。

什么时候会产生这样问题

一个JS文件执行方式,是按行(hang)执行,但是碰到异步操作的时候就会产生这样的问题。很常见的就是setTimeout和ajax请求。这个时候我们就需要清楚知道什么时候能拿到什么值。具体的过程如下图所示:

eventloop.png

我总结的规律

  1. 碰到setTimeout创建一个宏任务,插入队列的末端。
  2. 碰到new Promise,创建Promise对象立即执行,.then 创建一个微任务,插入到当前宏任务, 如果没有宏任务,插入到当前队列最前。
  3. 当前描述是指代码按行执行,队列的执行是在代码块执行完成之后。
  4. 当前队列按顺序执行,直到整个队列执行完成。

哪些可以触发宏任务: I/O,setTimeout,setInterval,setImmediate【node】,requestAnimationFrame【浏览器】 哪些可以触发微任务: process.nextTick【node】,MutationObserver【浏览器】,Promise.then catch finally

说明:

  1. 标明环境的说明只支持该环境,否则都支持
  2. I/O:针对浏览器可以是click,ajax等,针对node可以是file reader,http request等
  3. 这里所描述的不一定正确,我也没有找到确定的说法,但是大致差不多。

简单的判断输出顺序

  • console.log 直接输出。
  • 碰到setTimeout所有的console.log先丢到最后。
  • 碰到new Promise,如果是在创建的过程 console.log 直接输出。
  • 如果在创建的过程中有setTimeout,直接丢到最后。如果resolve和reject在setTimeout里面,那么Promise.then catch finally里面的console就在resolve或reject的后面。
  • 如果在创建的过程中没有有setTimeout,直接resolve(),则这个console.log在所有的setTimeout之前输出
  • async和await与Promise类似。

搞个例子验证下:

console.log('1') // 直接输出 1
new Promise((resolve) => {
  console.log('2') // 创建的过程 跟在1后面,直接输出 2
  setTimeout(() => { // 有setTimeout 丢到最后
    console.log('4')
    resolve()
  }, 0)
}).then(() => {
  console.log('3') // resolve 在setTimeout里面 3肯定是在 4之后打印出来
})
console.log(5) // 在2后面直接输出 5

// 所以最终的结果是 -> 1 2 5 4 3

举例考试

console.log('1')
setTimeout(() => {
  console.log('2')
}, 0)
Promise.resolve().then(() => {
  console.log('3')
}).then(() => {
  console.log('4')
})
console.log('5')

// 输出 -> 1 5 3 4 2
console.log('1')
new Promise((resolve) => {
  console.log('2')
  setTimeout(()=>{
    console.log('4')
    resolve()
    }, 0)
}).then(() => {
  console.log('3')
})

// 输出 -> 1 2 4 3
console.log('1')
setTimeout(() => {
  console.log('2')
  new Promise((resolve) => {
    console.log('3')
    resolve()
  }).then(() => {
    console.log('4')
  })
})
new Promise((resolve) => {
  console.log('5')
  resolve()
}).then(() => {
  console.log('6')
})
console.log('7')
setTimeout(() => {
  console.log('8')
  new Promise((resolve) => {
    console.log('9')
    resolve()
  }).then(() => {
    console.log('10')
  })
})

// 输出 -> 1 5 7 6 2 3 4 8 9 10
async function async1(){
  console.log('1')
  await async2()
  console.log('2')
}
async function async2(){
  console.log('3')
}
console.log('4')
setTimeout(() => {
  console.log('5')
}, 0)  
async1()
new Promise(function(resolve){
  console.log('6')
  resolve();
}).then(function(){
  console.log('7')
})
console.log('8')

// 输出 ->  4 1 3 6 8 2 7 5

几个名词解释

  • 栈(Stack)它是一种运算受限的线性表。限定仅在表尾进行插入和删除操作的线性表。后进先出
  • 堆(Heap)对象被分配在堆中,堆是一个用来表示一大块(通常是非结构化的)内存区域的计算机术语。
  • 队列(Queue)队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。

参考网站