EventLoop

695 阅读5分钟

一.JS是单线程还是多线程

JS其实是没有线程的概念的,所谓JS是单线程的其实是相对于多线程而言,所以JS不具备并行任务处理的能力。

二.不同环境下的EventLoop(异步回调原理)

2.1 宏任务与微任务

image.png

image.png

  • 微任务是ES6语法规定的
  • 宏任务是由浏览器规定的

2.2 Nodejs与浏览器

  • nodejs的event是基于libuv,而浏览器的event loop则在html5的规范中明确定义。
  • libuv已经对event loop作出了实现,而html5规范中只是定义了浏览器中event loop的模型,具体实现留给了浏览器厂商。

所以说在不同的浏览器环境下异步回调执行的结果是不相同的,我们这里主要阐述Nodejs与Chrome浏览器环境下的EventLoop实现原理

三.Chrome浏览器下的EventLoop

3.1 示例一

async function async1(){
  console.log('async1 start')
  await async2();   
  console.log('async1 end')
}
new Promise((resolve,reject)=>{
  console.log('promise1')
  resolve()
}).then(()=>{ 
  console.log('promise2')
})
async function async2(){
  console.log('async2')
}
async1()
console.log('script start')
setTimeout(()=>{
  console.log('setTimeout')  
},0)
console.log('script end')
  • 首先执行同步代码输出promise1 async1 start async2 script start script end
  • 在执行同步代码的过程中我们需要保存async1函数的上下文并跳出aync1函数注意这里并没有把await后面的代码推入微任务队列!!!!
  • 将Promise.then推入微任务队列,将setTimeout推入宏任务队列
  • 执行微任务输出promise2
  • 返回async1函数输出async1 end
  • 执行宏任务输出setTimeout

image.png

注意但是在新版浏览器下是吧await后面的代码推入到微任务队列当中的!

3.2 示例二

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="" />
  </head>
  <style type="text/css">
    .outer{
      width: 200px;
      height:200px;
      background-color: blue;
    }
    .inner{
      width:100px;
      height:100px;
      background-color: green;
      margin:0 auto;
    }
  </style>
  <body>
    <div class="outer">
      <div class="inner"></div>
    </div>
  </body>
  <script>
    var outer = document.querySelector('.outer');
    var inner = document.querySelector('.inner');
    new MutationObserver(function () {
      console.log('mutate');
    }).observe(outer, {
      attributes: true,
    });
    function onClick() {
      console.log('click');
      setTimeout(function () {
        console.log('timeout');
      }, 0);
      Promise.resolve().then(function () {
        console.log('promise');
      });
      outer.setAttribute('data-random', Math.random());
    }

    inner.addEventListener('click', onClick);
    outer.addEventListener('click', onClick);
  </script>
</html>

分析一:当我们仅触发outer的点击事件时

  • 同步代码click
  • 微任务队列:Promise.then() new MutationObserver()
  • 宏任务队列:setTimeout
  • 输出:click->promise->mutate->timeout

分析二:当我们触发inner的点击事件时(注意冒泡事件!!!)

  • 同步代码click
  • 微任务队列:Promise.then() new MutationObserver()
  • 宏任务队列:setTimeout
  • 输出:click->promise->mutate
  • 事件冒泡(outer)
  • 同步代码click
  • 微任务队列:Promise.then() new MutationObserver()
  • 宏任务队列:setTimeout
  • 输出:click->promise->mutate
  • 输出宏任务 timeout->timeout

3.3 DOM渲染时机

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="" />
  </head>
  <style type="text/css"></style>
  <body>
    <div id="container"></div>
  </body>
  <script>
    const container = document.getElementById('container')
    const p = "<p>DOM渲染时机</p>"
    container.innerHTML = p
    alert(container.children.length)
  </script>
</html>

此时JS执行栈中有alert函数,所以DOM不会渲染。当alert函数执行完毕之后我们才渲染DOM。 DOM渲染是在每次JS执行栈为空的时候去渲染,如果需要去渲染则去渲染DOM

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="" />
  </head>
  <style type="text/css"></style>
  <body>
    <div id="container"></div>
  </body>
  <script>
    const container = document.getElementById('container')
    const p = "<p>DOM渲染时机</p>"
    container.innerHTML = p
    setTimeout(()=>{
      alert(`宏任务 ${container.children.length}`)
    },0)
    Promise.resolve().then(()=>{
      alert(`微任务 ${container.children.length}`)
    })
  </script>
</html>

DOM渲染是在微任务执行之后宏任务执行之前

3.4 原理

image.png

JS 中所有的方法都会被推入栈中执行,执行完成被弹出,在遇到异步代码的时候,例如 setTimeout MutationObserver Promise 异步的部分会由其他掌管 webAPI 的地方执行,等异步有结果之后,回调函数会进入相应的队列,Promise MutationObserver 回调进入微任务队列,setTimeout setInterval requestAnimationFrame 进入宏任务队列。 等待主线程的执行栈空了,微任务队列立刻被推入栈中执行,执行完毕开始执行宏任务队列

注意:当主线程的执行栈为空之后微任务才会执行,如果主线程不为空则不会执行微任务

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0">
    <title>Document</title>
    <link rel="stylesheet" type="text/css" href="" />
  </head>
  <style type="text/css">
    .outer{
      width: 200px;
      height:200px;
      background-color: blue;
    }
    .inner{
      width:100px;
      height:100px;
      background-color: green;
      margin:0 auto;
    }
  </style>
  <body>
    <div class="outer">
      <div class="inner"></div>
    </div>
  </body>
  <script>
    var outer = document.querySelector('.outer');
    var inner = document.querySelector('.inner');
    new MutationObserver(function () {
      console.log('mutate');
    }).observe(outer, {
      attributes: true,
    });
    function onClick() {
      console.log('click');
      setTimeout(function () {
        console.log('timeout');
      }, 0);

      Promise.resolve().then(function () {
        console.log('promise');
      });
      outer.setAttribute('data-random', Math.random());
    }
    inner.addEventListener('click', onClick);
    outer.addEventListener('click', onClick);
    inner.click();
  </script>
</html>
  • 案例二中需要我们手动去点击内部元素触发点击事件将点击事件推入JS执行栈中,当运行完毕后又会被推出,此时在冒泡之前,执行栈是空的所以可以执行微任务队列。
  • 但是当我们使用脚本去调用的时候,只有当内外元素的点击事件都处理完成后,这个脚本才会被推出JS的执行栈,所以需要等到最后才能执行微任务队列。

四.Nodejs下的EventLoop

4.1 区别

  • 浏览器下的JS堆栈只能执行一个任务也就是说,你的微任务或者宏任务只能在JS堆栈为空的时候压入栈中。
  • NodeJS下的你可以同时执行多个宏任务。(但是这一特点在11.x版本之后就被修复为与浏览器一致)
  • NodeJS定义了多个宏任务种类以及对应的优先级
  • process.nextTick()的优先级高于所有的微任务,每一次清空微任务列表的时候,都是先执行 process.nextTick()

4.2 六个阶段(可参照NodeJS官网)

  • timers:执行setTimeout() 和 setInterval()中到期的callback。
  • I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
  • idle, prepare:仅内部使用
  • poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段(文件读取,http请求)
  • check:执行setImmediate的callback
  • close callbacks:执行close事件的callback,例如socket.on("close",func)

image.png

4.3 案例

const fs = require('fs');

setImmediate(() => {
  console.log('setImmediate');
});

fs.readdir(__dirname, () => {
  console.log('fs.readdir');
});

setTimeout(()=>{
  console.log('setTimeout');
});

Promise.resolve().then(() => {
  console.log('promise');
});
const fs = require('fs');
fs.readdir(__dirname, () => {
  console.log('fs.readdir');
  setImmediate(() => {
    console.log('setImmediate');
  });
  setTimeout(()=>{
    console.log('setTimeout');
  });
});
  • 当 setTimeout() 和 setImmediate() 都写在 main 里面的时候 不一定谁先执行谁后执行
  • 当 setTimeout() 和 setImmediate() 都写在一个 I/O 回调 或者说一个 poll 类型宏任务的回调里面的时候 一定是先执行 setImmediate() 后执行 setTimeout(),因为第五阶段处于第二或第四阶段后面。

Node 并不能保证 timers 在预设时间到了就会立即执行,因为 Node 对 timers 的过期检查不一定靠谱,它会受机器上其它运行程序影响,或者那个时间点主线程不空闲 虽然 setTimeout 延时为 0,但是一般情况 Node 把 0 会设置为 1ms,所以,当 Node 准备 event loop 的时间大于 1ms 时,进入 timers 阶段时,setTimeout 已经到期,则会先执行 setTimeout;反之,若进入 timers 阶段用时小于 1ms,setTimeout 尚未到期,则会错过 timers 阶段,先进入 check 阶段,而先执行 setImmediate

setImmediate(() => {
  console.log('timeout1')
  Promise.resolve().then(() => console.log('promise resolve'))
  process.nextTick(() => console.log('next tick1'))
});
setImmediate(() => {
  console.log('timeout2')
  process.nextTick(() => console.log('next tick2'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
process.nextTick(()=>console.log('tick'))
Promise.resolve().then(()=>{console.log('promise')})

process.nextTick()优先于所有微任务之前执行