vlambda博客
学习文章列表

js中的event loop之浏览器篇

前言

应该是在很久之前吧,面试某程的时候,面试官向我抛出了一个问题。我想问下:js是一个单线程的语言,是怎么能够执行异步任务的。还有setTimeout定时器设置为0和不设置执行的时候有什么区别。还记得当时面试的我一脸的蒙B,面试之后,特意补了下这方面的知识,现在再把笔记补上。

为什么JS是单线程语言

首先js在设计之初就是为了用于用户的交互,和操作DOM。也是这一特性,解决JS只能是一个单线程语言。如果JS是一个多线程的语言,那么一个线程正在操作某一个DOM,而另一个线程却把DOM进行删除了。那这个时间浏览器执行的时候是一脸蒙B的,我应该信谁的呢?

所以,为了避免复杂性,从一诞生,js就注定是一个单线程语言,这也是这门语言的核心,而且未来也不会改变。而且这个单线程还是==非阻塞==的,也是就是上一个异步事件的执行,并不会影响其它事件进行(ajax方法)

什么是进程和线程呢?

进程是资源分配的最小单位,线程是CPU调度的最小单位。(这是一个比较抽像的解释,我记得操作系统这本书就是这样写的。)

进程(process)

进程是指计算机中已运行的程序。比如,你打开有道云笔记,那么在计算中就会启动一个进程。

线程(thread)

线程是操作系统能够进行运算调度的最小单位。大部分情况下,它被包含在进程之中,是进程中的实际运作单位。

线程和进程的关系

做个简单的比喻:进程=火车,线程=车厢

  • 线程在进程下行进(单纯的车厢无法运行)
  • 一个进程可以包含多个线程(一辆火车可以有多个车厢)
  • 不同进程间数据很难共享(一辆火车上的乘客很难换到另外一辆火车,比如站点换乘)
  • 同一进程下不同线程间数据很易共享(A车厢换到B车厢很容易)

更多参考这里

单线程怎么处理异步

单线程就意味着CPU在其一时间内只能做一件事,所有的任务都要排队来进行执行。任务分为两种:一种是同步任务,另一种是异步任务。

同步任务

同步任务是指在主线程上执行的任务,只有一个任务完成,线程才能执行下一个任务.

异步任务

异步任务是指不进入主线程,而进入任务队列(task queue)的任务。而且只有在同步任务执行完成之后,主线程是空闲的状态下,主线程才会查找事件队列中是否有任务,有的话取出排在第一位的进行执行。

Event Loop(事件循环)

综上,整个js引擎执行机制如下:

  1. 同步的代码在主线程上进行执行,行成一个执行栈(execution context stack)
  2. 在执行同步的代码的时候,如果遇到异步任务,是将这个事件先挂起。继续执行其它同步任务,当异步事件返回结果时,将他放到任务队列中。
  3. 当主线程的所有同步任务执行完成时,主线程就会查找任务队列中是否有任务,如果有则按顺序依次取出,放到执行栈中,执行代码
  4. 主线程重复以上三步

执行的顺序如下图:

宏任务(Macro Task)和微任务(Micro Task)

在任务队列中任务分为:宏任务和微任务。对于宏任务和微任务,别人的文章有一个很形象的解释:

这个就像去银行办业务一样,先要取号进行排号。一般上边都会印着类似:“您的号码为XX,前边还有XX人。”之类的字样。因为柜员同时职能处理一个来办理业务的客户,这时每一个来办理业务的人就可以认为是银行柜员的一个宏任务来存在的,当柜员处理完当前客户的问题以后,选择接待下一位,广播报号,也就是下一个宏任务的开始。所以多个宏任务合在一起就可以认为说有一个任务队列在这,里边是当前银行中所有排号的客户。任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中,就像在银行中排号,如果叫到你的时候你不在,那么你当前的号牌就作废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来以后还需要重新取号 而且一个宏任务在执行的过程中,是可以添加一些微任务的,就像在柜台办理业务,你前边的一位老大爷可能在存款,在存款这个业务办理完以后,柜员会问老大爷还有没有其他需要办理的业务,这时老大爷想了一下:“最近P2P爆雷有点儿多,是不是要选择稳一些的理财呢”,然后告诉柜员说,要办一些理财的业务,这时候柜员肯定不能告诉老大爷说:“您再上后边取个号去,重新排队”。所以本来快轮到你来办理业务,会因为老大爷临时添加的“理财业务”而往后推。也许老大爷在办完理财以后还想 再办一个信用卡?或者 再买点儿纪念币?无论是什么需求,只要是柜员能够帮她办理的,都会在处理你的业务之前来做这些事情,这些都可以认为是微任务。这就说明:你大爷永远是你大爷

两者执行的顺序:在当前的微任务没有执行完成时,是不会执行下一个宏任务的。常见的宏任务如下 :

  • setTimout
  • setInterval
  • I/O
  • script代码加载
  • UI rendering

微任务:

  • Promise
  • MutaionObserver(HTML5 新特性)

如下图执行:

经典题目分析

console.log('script start')

async function async1({
    console.log('1');
    await async2();
    console.log('2');
}

function async2({
  console.log(3)
}

setTimeout(function(){
 console.log(4)
}, 0)

async1();

new Promise(function(resolve){
 console.log(5)
  resolve()
  console.log(6)
}).then(function({
console.log(7)
})
console.log('script end')

// "script start"
// "1"
// 3
// 5
// 6
// "script end"
// "2"
// 7
// 4

题解:

  1. 程序从上到下执行先输出 script start, 接着初始化两个函数,并没有执行
  2. 执行到 setTimeout时,属于宏任务放到下一次下宏任务中
  3. async1,输出 1async1函数调用 async2输出3。console.log(2)在await语言之后,属于微任务,放到微任务中。
  4. 执行new promise函数,输出 5 6 ,console.log(6)虽然在resove方法之后,但是仍属于.then之前的同步方法执行。之后输出console.log('script end')
  5. 主线程执行完成之后,开始执行微任务。输出2, 7
  6. 当前微任务执行完成,最后执行宏任务 4

例子二:

const p = function(){
 return new Promise((resolve)=> {
   const p1 = new Promise(resolve => {
     setTimeout(() => {
       resolve(1)
      },0)
      resolve(2)
    })
    p1.then(res => {
     console.log(res)
    })
    
    console.log(3)
    resolve(4)
  })
}

p().then(res => {
 console.log(res)
})
// 3 2 4
  1. Promise本身是同步的立即执行函数,.then之后的是异步执行函数。setTimeout为宏任务。
  2. 从上到下执行,先把setTimeout放到宏任务中,p1.then放到微任务的第一个位置,之后执行console.log(3),然后把p.then放到微任务第二位中。
  3. 先执行第一个微任务 resove(2)输出 2  ,再执行p.then输出4,最后执行到setTimeout里,整个promise已经resolve状态,所以setTimeout中的代码是不起作用的

总结

浏览器中的eventloop在es7的async 和await出现之前,是很经常见到的。也是了解js解析执行有一定的了解。当然对于面试也特别有用。其实这里要说的深入点,还能讲浏览器渲染,和Promise这一块的顺序执行。这里不在扯开了!

  • Tasks, microtasks, queues and schedules
  • 菲利普·罗伯茨:到底什么是Event Loop呢?| 欧洲 JSConf 2014
  • HTML Living Standard — Last Updated 30 July 2020
  • JavaScript事件循环机制解析
  • 并发模型与事件循环
  • js-代码运行机制,宏任务、微任务