浏览器的 Event Loop 宏任务,微任务,事件冒泡

一. 为何JavaScript是单线程?


JavaScript语言的一大特色就是单线程,也就是说,同一个时间只能作一件事。那么,为何JavaScript不能有多个线程呢?这样能提升效率啊。javascript

JavaScript的单线程,与它的用途有关。做为浏览器脚本语言,JavaScript的主要用途是与用户互动,以及操做DOM。这决定了它只能是单线程,不然会带来很复杂的同步问题。好比,假定JavaScript同时有两个线程,一个线程在某个DOM节点上添加内容,另外一个线程删除了这个节点,这时浏览器应该以哪一个线程为准?html

因此,为了不复杂性,从一诞生,JavaScript就是单线程,这已经成了这门语言的核心特征,未来也不会改变。java

为了利用多核CPU的计算能力,HTML5提出Web Worker标准,容许JavaScript脚本建立多个线程,可是子线程彻底受主线程控制,且不得操做DOM。因此,这个新标准并无改变JavaScript单线程的本质。面试

2、浏览器js运行机制


简介

单线程就意味着,全部任务须要排队,前一个任务结束,才会执行后一个任务。若是前一个任务耗时很长,后一个任务就不得不一直等着。ajax

若是排队是由于计算量大,CPU忙不过来,倒也算了,可是不少时候CPU是闲着的,由于IO设备(输入输出设备)很慢(好比Ajax操做从网络读取数据),不得不等着结果出来,再往下执行。数据库

JavaScript语言的设计者意识到,这时主线程彻底能够无论IO设备,挂起处于等待中的任务,先运行排在后面的任务。等到IO设备返回告终果,再回过头,把挂起的任务继续执行下去。promise

因而,全部任务能够分红两种,一种是同步任务(synchronous),另外一种是异步任务(asynchronous)。同步任务指的是,在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;异步任务指的是,不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务能够执行了,该任务才会进入主线程执行。浏览器

具体来讲,异步执行的运行机制以下。(同步执行也是如此,由于它能够被视为没有异步任务的异步执行。)bash

(1)全部同步任务都在主线程上执行,造成一个执行栈(execution context stack)。

(2)主线程以外,还存在一个"任务队列"(task queue)。只要异步任务有了运行结果,就在"任务队列"之中放置一个事件。

(3)一但"执行栈"中的全部同步任务执行完毕,系统就会读取"任务队列",看看里面有哪些事件。那些对应的异步任务,因而结束等待状态,进入执行栈,开始执行。

(4)主线程不断重复上面的第三步。
复制代码

只要主线程空了,就会去读取"任务队列",这就是JavaScript的运行机制。这个过程会不断重复。网络

三. 浏览器的 Event Loop 事件循环


简介

主线程从"任务队列"中读取事件,这个过程是循环不断的,因此整个的这种运行机制又称为Event Loop(事件循环)。为了更好地理解Event Loop,请看下图

事件循环能够简单描述为:

函数入栈,当Stack中执行到异步任务的时候,就将他丢给WebAPIs,接着执行同步任务,直到Stack为空; 在此期间WebAPIs完成这个事件,把回调函数放入CallbackQueue中等待; 当执行栈为空时,Event Loop把Callback Queue中的一个任务放入Stack中,回到第1步。

  • Event Loop是由javascript宿主环境(像浏览器)来实现的;
  • WebAPIs是由C++实现的浏览器建立的线程,处理诸如DOM事件、http请求、定时器等异步事件;
  • JavaScript 的并发模型基于"事件循环";
  • Callback Queue(Event Queue 或者 Message Queue) 任务队列,存放异步任务的回调函数

接下来看一个异步函数执行的例子:

var start=new Date();
setTimeout(function cb(){
    console.log("时间间隔:",new Date()-start+'ms');
},500);
while(new Date()-start<1000){};
复制代码
  1. main(Script) 函数入栈,start变量开始初始化
  2. setTimeout入栈,出栈,丢给WebAPIs,开始定时500ms;
  3. while循环入栈,开始阻塞1000ms;
  4. 500ms事后,WebAPIs把cb()放入任务队列,此时while循环还在栈中,cb()等待;
  5. 又过了500ms,while循环执行完毕从栈中弹出,main()弹出,此时栈为空,Event Loop,cb()进入栈,log()进栈,输出'时间间隔:1003ms',出栈,cb()出栈

四. 宏任务(Macrotasks)和微任务(Microtasks)


简介

JS的异步有一个机制的,就是会分为宏任务和微任务。宏任务和微任务会放到不一样的event queue中,先将全部的宏任务放到一个event queue(macro-task),再将微任务放到一个event queue(micro-task)中。执行完宏任务以后,就会先从微任务中取这个回调函数执行。

讲的详细一点的话

最开始, 执行栈为空, 微任务队列为空, 宏任务队列有一个 script 标签(内含总体代码)

将第一个宏任务出队, 这里即为上述的 script 标签

总体代码执行过程当中, 若是是同步代码, 直接执行(函数执行的话会有入栈出栈操做), 若是是异步代码, 会根据任务类型推入不一样的任务队列中(宏任务或微任务)

当执行栈执行完为空时, 会去处理微任务队列的任务, 将微任务队列的任务一个个推入调用栈执行完

微任务执行完后,检查是否须要从新渲染 UI。

...往返循环直到宏任务和微任务队列为空

总结一下上述循环机制的特色:

出队一个宏任务 -> 调用栈为空后, 执行一队微任务 -> 更新界面渲染 -> 回到第一步

宏任务 macro-task(Task)

一个event loop有一个或者多个task队列。task任务源很是宽泛,好比ajax的onload,click事件,基本上咱们常常绑定的各类事件都是task任务源,还有数据库操做(IndexedDB ),须要注意的是setTimeout、setInterval、setImmediate也是task任务源。总结来讲task任务源:

  • script
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • requestAnimationFrame
  • UI rendering

微任务 micro-task(Job)

microtask 队列和task 队列有些类似,都是先进先出的队列,由指定的任务源去提供任务,不一样的是一个 event loop里只有一个microtask 队列。另外microtask执行时机和Macrotasks也有所差别

  • process.nextTick
  • promises
  • Object.observe
  • MutationObserver

宏任务和微任务的区别

  • 宏队列能够有多个,微任务队列只有一个,因此每建立一个新的settimeout都是一个新的宏任务队列,执行完一个宏任务队列后,都会去checkpoint 微任务。
  • 一个事件循环后,微任务队列执行完了,再执行宏任务队列
  • 一个事件循环中,在执行完一个宏队列以后,就会去check 微任务队列

宏任务和微任务的运行

下图是一个事件循环的流程

举个简单的例子,假设一个script标签的代码以下:

Promise.resolve().then(function promise1 () {
       console.log('promise1');
    })
setTimeout(function setTimeout1 (){
    console.log('setTimeout1')
    Promise.resolve().then(function promise2 () {
       console.log('promise2');
    })
}, 0)

setTimeout(function setTimeout2 (){
   console.log('setTimeout2')
}, 0)
复制代码

运行过程:

script里的代码被列为一个task,放入task队列。

循环1:

  • 【task队列:script ;microtask队列:】

    1. 从task队列中取出script任务,推入栈中执行。
    2. promise1列为microtask,setTimeout1列为task,setTimeout2列为task。
  • 【task队列:setTimeout1 setTimeout2;microtask队列:promise1】

    1. script任务执行完毕,执行microtask checkpoint,取出microtask队列的promise1执行。

循环2:

  • 【task队列:setTimeout1 setTimeout2;microtask队列:】

    1. 从task队列中取出setTimeout1,推入栈中执行,将promise2列为microtask。
  • 【task队列:setTimeout2;microtask队列:promise2】

    1. 执行microtask checkpoint,取出microtask队列的promise2执行。
    (循环2中的 setTimeout2为何不是跟在setTimeout1的后面输出?
    这里我以为应该是setTimeout1和setTimeout2不是在同一个task队列中,
    是两个task队列。在执行完setTimeout1的task队列后,
    event loop去检查microtask队列是否有事件,而且把事推入到主栈。)
    复制代码

循环3:

  • 【task队列:setTimeout2;microtask队列:】

    1. 从task队列中取出setTimeout2,推入栈中执行。
    2. setTimeout2任务执行完毕,执行microtask checkpoint。
  • 【task队列:;microtask队列:】

注:有些文章说的一个事件循环的开始是先执行微任务再执行宏任务,有有些说的是先执行宏任务再执行微任务,我我的以为这两种只是见解的角度不一致

  • 若是把script载入到主堆栈这一过程当作是执行了宏任务,那么就是宏任务先开始。
  • 若是不把这个script的运行当作是宏任务,只看异步函数中的宏任务(setTimeout)那么就是微任务先开始。

宏任务与微任务示例

EXP1 在主线程上添加宏任务与微任务

console.log('-------start--------');

setTimeout(() => {
  console.log('setTimeout');  // 将回调代码放入另外一个宏任务队列
}, 0);

new Promise((resolve, reject) => {
  for (let i = 0; i < 5; i++) {
    console.log(i);
  }
  resolve()
}).then(()=>{
  console.log('Promise'); // 将回调代码放入微任务队列
})

console.log('-------end--------');
复制代码

运行结果:

-------start--------
0
1
2
3
4
-------end--------
Promise
setTimeout
复制代码

由EXP1,咱们能够看出,当JS执行完主线程上的代码,会去检查在主线程上建立的微任务队列,执行完微任务队列以后才会执行宏任务队列上的代码

运行顺序:

主线程 => 主线程上建立的微任务 => 主线程上建立的宏任务

script里的代码被列为一个task,放入task队列。

循环1:

  • 【task队列:script ;microtask队列:】

    1. 从task队列中取出script任务,推入栈中执行。
    2. promise列为microtask,setTimeout列为task。
  • 【task队列:setTimeout ;microtask队列:promise】

    1. script任务执行完毕,执行microtask checkpoint,取出microtask队列的promise执行。

循环2:

  • 【task队列:setTimeout ;microtask队列:】

    1. 从task队列中取出setTimeout,推入栈中执行
    2. setTimeout任务执行完毕,执行microtask checkpoint。
  • 【task队列:;microtask队列:】

EXP2 在微任务中建立微任务

setTimeout(_ => console.log('setTimeout4'))

new Promise(resolve => {
  resolve()
  console.log('Promise1')
}).then(_ => {
  console.log('Promise3')
  Promise.resolve().then(_ => {
    console.log('before timeout')
  }).then(_ => {
    Promise.resolve().then(_ => {
      console.log('also before timeout')
    })
  })
})

console.log(2)
复制代码

运行结果:

Promise1
2
Promise3
before timeout
also before timeout
setTimeout4
复制代码

由EXP2,咱们能够看出,在微任务队列执行时建立的微任务,仍是会排在主线程上建立出的宏任务以前执行(由于微任务只有一条,自增链不断的话 会一直往下执行微任务,不会被中断)

运行顺序:

主线程 => 主线程上建立的微任务1 => 微任务1上建立的微任务2 => 主线程上建立的宏任务

script里的代码被列为一个task,放入task队列。

循环1:

  • 【task队列:script ;microtask队列:】

    1. 从task队列中取出script任务,推入栈中执行。
    2. promise3 列为microtask,setTimeout4 列为task。
  • 【task队列:setTimeout4;microtask队列:promise3】

    1. script任务执行完毕,执行microtask checkpoint,取出microtask队列的promise3执行。
    2. 将before timeout 列为 microtask。
  • 【task队列:setTimeout4;microtask队列:before timeout】

    1. before timeout 执行
    2. 将also before timeout 列为microtask
  • 【task队列:setTimeout4;microtask队列:also before timeout】

    1. also before timeout 执行

循环2:

  • 【task队列:setTimeout4 ;microtask队列:before timeout】

    1. 从task队列中取出setTimeout4,推入栈中执行
    2. setTimeout4任务执行完毕,执行microtask checkpoint。
  • 【task队列:;microtask队列:】

EXP3: 宏任务中建立微任务

// 宏任务队列 1
setTimeout(() => {
  // 宏任务队列 1.1
  console.log('timer_1');
  setTimeout(() => {
    // 宏任务队列 3
    console.log('timer_3')
  }, 0)
  new Promise(resolve => {
    resolve()
    console.log('new promise')
  }).then(() => {
    // 微任务队列 1
    console.log('promise then')
  })
}, 0)

setTimeout(() => {
  // 宏任务队列 2.2
  console.log('timer_2')
}, 0)

console.log('========== Sync queue ==========')

复制代码

运行结果:

========== Sync queue ==========
timer_1
new promise
promise then
timer_2
timer_3
复制代码

运行顺序:

主线程(宏任务队列 1)=> 宏任务队列 1.1 => 微任务队列 1 => 宏任务队列 3=>宏任务队列2.2

循环1:

  • 【task队列:script ;microtask队列:】

    1. 从task队列中取出script任务,推入栈中执行。
    2. timer_1 列为task,timer_2 列为task。
  • 【task队列:timer_1,timer_2;microtask队列:】

    1. script任务执行完毕,执行microtask checkpoint,无microtask队列可执行。

循环2

  • 【task队列::timer_1,timer_2;microtask队列:】

    1. 从task队列中取出 timer_1 推入栈中执行。
    2. 将 timer_3 列为task,promise then 列为microtask
  • 【task队列:timer_2,timer_3;microtask队列:promise then】

    1. 执行microtask checkpoint,取出microtask队列的promise then执行

循环3

  • 【task队列:timer_2,timer_3;microtask队列:】

    1. 从task队列中取出timer_2,推入栈中执行
  • 【task队列:timer_3;microtask队列:】

    1. 执行microtask checkpoint,无microtask队列可执行

循环4

  • 【task队列:timer_3;microtask队列:】

    1. 从task队列中取出timer_3,推入栈中执行
  • 【task队列:;microtask队列:】

    1. 执行microtask checkpoint,无microtask队列可执行

EXP4:微任务队列中建立的宏任务

// 宏任务1
new Promise((resolve) => {
  console.log('new Promise(macro task 1)');
  resolve();
}).then(() => {
  // 微任务1
  console.log('micro task 1');
  setTimeout(() => {
    // 宏任务3
    console.log('macro task 3');
  }, 0)
})

setTimeout(() => {
  // 宏任务2
  console.log('macro task 2');
}, 0)

console.log('========== Sync queue(macro task 1) ==========');
复制代码

运行结果:

========== Sync queue(macro task 1) ==========
new Promise(macro task 1)
micro task 1
macro task 2
macro task 3
复制代码

异步宏任务队列只有一个,当在微任务中建立一个宏任务以后,他会被追加到异步宏任务队列上(跟主线程建立的异步宏任务队列是同一个队列)

运行顺序:

主线程 => 主线程上建立的微任务 => 主线程上建立的宏任务 => 微任务中建立的宏任务

循环1:

  • 【task队列:script ;microtask队列:】

    1. 从task队列中取出script任务,推入栈中执行。
    2. macro task 2 列为task,micro task 1 列为microtask。
  • 【task队列:macro task 2;microtask队列:micro task 1】

    1. script任务执行完毕,执行microtask checkpoint,microtask队列中取出micro task 1 执行。
    2. 执行micro task 1 的时候,把macro task 3列为task

循环2

  • 【task队列:macro task 2,macro task 3;microtask队列:】
    1. 从task队列中取出 micro task2,推入栈中执行。
    2. 执行microtask checkpoint,无microtask队列可执行

循环2

  • 【task队列:macro task 3;microtask队列:】

    1. 从task队列中取出 micro task3,推入栈中执行。
    2. 执行microtask checkpoint,无microtask队列可执行
  • 【task队列:;microtask队列:】

小总结

  • 微任务队列优先于宏任务队列执行,
  • 微任务队列上建立的宏任务会被后添加到当前宏任务队列的尾端,微任务队列中建立的微任务会被添加到微任务队列的尾端。
  • 只要微任务队列中还有任务,宏任务队列就只会等待微任务队列执行完毕后再执行。

五. 当 Event Loop 赶上事件冒泡

手动触发

代码

<div class="outer">
  <div class="inner"></div>
</div>
复制代码
var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

function onClick() {
  console.log('click');

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

  Promise.resolve().then(function() {
    console.log('promise');
  });

}

inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);
复制代码

点击 inner,最终打印结果为:

click
promise
click
promise
timeout
timeout
复制代码

分析

为何打印结果是这样的呢?咱们来分析一下: (0)将 script 标签内的代码(宏任务)放入执行栈执行,执行完后,宏任务微任务队列皆空。

(1)点击 inner,onClick 函数入执行栈执行,打印 "click"。执行完后执行栈为空,由于事件冒泡的缘故,事件触发线程会将向上派发事件的任务放入宏任务队列。

(2)遇到 setTimeout,在最小延迟时间后,将回调放入宏任务队列。遇到 promise,将 then 的任务放进微任务队列

(3)此时,执行栈再次为空。开始清空微任务,打印 "promise"

(4)此时,执行栈再次为空。从宏任务队列拿出一个任务执行,即前面提到的派发事件的任务,也就是冒泡。

(5)事件冒泡到 outer,执行回调,重复上述 "click"、"promise" 的打印过程。

(6)从宏任务队列取任务执行,这时咱们的宏任务队列已经累计了两个 setTimeout 的回调了,因此他们会在两个 Event Loop 周期里前后获得执行。

能够当作是:

function onClick() {
  //模拟outer click事件
  setTimeout(function(){onClick1()},0)
  console.log('click');

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

  Promise.resolve().then(function() {
    console.log('promise');
  });
}
function onClick1() {
  console.log('click1');

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

  Promise.resolve().then(function() {
    console.log('promise1');
  });
}
//模拟inner click事件
onClick()
复制代码

代码触发

代码

inner.click()
复制代码

打印结果为:

click
click
promise
promise
timeout
timeout
复制代码

分析

依旧分析一下:

(0)将 script(宏任务)放入执行栈执行,执行到 inner.click() 的时候,执行 onClick 函数,打印 "click"

(1)当执行完 onClick 后,此时的 script(宏任务)还没返回,执行栈不为空,不会去清空微任务,而是会将事件往上冒泡派发

...(关键步骤分析完后,续步骤就不分析了)

能够当作是:

function onClick() {
  console.log('click');

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

  Promise.resolve().then(function() {
    console.log('promise');
  });
}
onClick();
onClick();
复制代码

总结

在通常状况下,微任务的优先级是更高的,是会优先于事件冒泡的,但若是手动 .click() 会使得在 script代码块 还没弹出执行栈的时候,触发事件派发。

Event Loop总结

浏览器进行事件循环工做方式

  1. 选择当前要执行的任务队列,选择任务队列中最早进入的任务,若是任务队列为空即null,则执行跳转到微任务(MicroTask)的执行步骤。

  2. 将事件循环中的任务设置为已选择任务。

  3. 执行任务。

  4. 将事件循环中当前运行任务设置为null。

  5. 将已经运行完成的任务从任务队列中删除。

  6. microtasks步骤:进入microtask检查点。

  7. 更新界面渲染。

  8. 返回第一步

【执行进入microtask检查点时,浏览器会执行如下步骤:】

  • 设置microtask检查点标志为true。

  • 当事件循环microtask执行不为空时:选择一个最早进入的microtask队列的microtask,将事件循环的microtask设置为已选择的microtask,运行microtask,将已经执行完成的microtask为null,移出microtask中的microtask。

  • 清理IndexDB事务

  • 设置进入microtask检查点的标志为false。

重点

总结以上规则为一条通俗好理解的:

  1. 顺序执行先执行同步方法,碰到MacroTask直接执行,而且把回调函数放入MacroTask执行队列中(下次事件循环执行);碰到microtask直接执行。把回调函数放入microtask执行队列中(本次事件循环执行)
  2. 当同步任务执行完毕后,去执行微任务microtask。(microtask队列清空)
  3. 由此进入下一轮事件循环:执行宏任务 MacroTask (setTimeout,setInterval,callback)

[总结]全部的异步都是为了按照必定的规则转换为同步方式执行。


以上是本人参考如下资料后的理解,若是有错误的地方,请各位大牛帮忙纠正,谢谢。

JavaScript 运行机制详解:再谈Event Loop

浏览器事件循环机制(event loop)

浏览器事件循环 javaScript事件循环 EventLoop

总结事件轮询机制,以及宏任务队列与微任务队列

当 Event Loop 赶上事件冒泡

一次弄懂Event Loop(完全解决此类面试问题)

相关文章
相关标签/搜索