Event Loop


进程与线程 -- 涉及?试题:进程与线程区别? JS 单线程带来的好处?

  • JS 是单线程执?的,但是你是否疑惑过什么是线程?
  • 讲到线程,那么肯定也得说?下进程。本质上来说,两个名词都是 CPU ?作时间?的?个描述。
  • 进程描述了 CPU 在运?指令及加载和保存上下?所需的时间,放在应?上来说就代表了?个程序。
  • 线程是进程中的更?单位,描述了执??段指令所需的时间。
    --- 把这些概念拿到浏览器中来说,当你打开?个 Tab ?时,其实就是创建了?个进程,?个进程中可以有多个线程,?如渲染线程、 JS 引擎线程、HTTP 请求线程等等。当你发起?个请求时,其实就是创建了?个线程,当请求结束后,该线程可能就会被销毁。
  • 上?说到了 JS 引擎线程和渲染线程,?家应该都知道,在 JS 运?的时候可能会阻? UI 渲染,这说明了两个线程是互斥的。这其中的原因是因为 JS 可以修改 DOM ,如果在 JS 执?的时候 UI 线程还在?作,就可能导致不能安全的渲染 UI 。这其实也是?个单线程的好处,得益于 JS 是单线程运?的,可以达到节省内存,节约上下?切换时间,没有锁的问题的好处。

执?栈 -- 涉及?试题:什么是执?栈?
* 可以把执?栈认为是?个存储函数调?的栈结构,遵循先进后出的原则。
当开始执? JS 代码时,?先会执??个 main 函数,然后执?我们的代码。
根据先进后出的原则,后执?的函数会先弹出栈,在图中我们也可以发现, foo 函数后执?,当执?完毕后就从栈中弹出了。

              function foo() {
                 throw new Error('error')
              }
              function bar() {
                 foo()
              }
              bar()
  ?家可以在上图清晰的看到报错在 foo 函数, foo 函数?是在 bar 函数 中调?的。

当我们使?递归的时候,因为栈可存放的函数是有限制的,?旦存放了过多的函数且没有得到释放的话,就会出现爆栈的问题。

              function bar() {
                 bar()
              }
              bar()

浏览器中的 Event Loop
涉及?试题:异步代码执?顺序?解释?下什么是 Event Loop ?

 * 众所周知 JS 是??阻塞单线程语?,因为在最初 JS 就是为了和浏览器交互?诞?的。
 * 如果 JS 是?多线程的语?话,我们在多个线程中处理 DOM 就可能会发?问题(?个线程中新加节点,另?个线程中删除节点)。
  • JS 在执?的过程中会产?执?环境,这些执?环境会被顺序的加?到执?栈中。如果遇到异步的代码,会被挂起并加?到 Task (有多种 task ) 队列中。?旦执?栈为空,Event Loop 就会从 Task 队列中拿出需要执?的代码并放?执?栈中执?,所以本质上来说 JS 中的异步还是同步?为。

            console.log('script start');
            setTimeout(function() {
               console.log('setTimeout');
            }, 0);
            console.log('script end');
    
  • 不同的任务源会被分配到不同的 Task 队列中,任务源可以分为 微任务 ( microtask ) 和 宏任务( macrotask )。

  • 在 ES6 规范中,microtask 称为 jobs , macrotask 称为 task 。

      console.log('script start');
      setTimeout(function() {
         console.log('setTimeout');
      }, 0);
      new Promise((resolve) => {
         console.log('Promise')
         resolve()
      }).then(function() {
         console.log('promise1');
      }).then(function() {
         console.log('promise2');
      });
      console.log('script end');
      // script start => Promise => script end => promise1 => promise2 => setTimeout
    

--- 以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于 微任务 而 setTimeout 属于宏任务。

微任务

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

宏任务

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

-- 宏任务中包括了 script ,浏览器会先执??个宏任务,接下来有 异步代码 的话就先执?微任务。

所以正确的?次 Event loop 顺序是这样的

  • 执?同步代码,这属于宏任务;
  • 执?栈为空,查询是否有微任务需要执?;
  • 执?所有微任务 ;
  • 必要的话渲染 UI ;
  • 然后开始下?轮 Event loop ,执?宏任务中的异步代码。

--- 通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有?量的计算 并且需要操作 DOM 的话,为了更快的响应界?响应,我们可以把操作 DOM 放?微任务中。

Node 中的 Event loop

  • Node 中的 Event loop 和浏览器中的不相同。

  • Node 的 Event loop 分为 6 个阶段,它们会按照顺序反复运?。

    ┌───────────────────────┐
    ┌─> │ timers │
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │ I/O callbacks │
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    │ │ idle, prepare │
    │ └──────────┬────────────┘ ┌───────────────┐
    │ ┌──────────┴────────────┐ │ incoming: │
    │ │ poll │<──connections─── │
    │ └──────────┬────────────┘ │ data, etc. │
    │ ┌──────────┴────────────┐ └───────────────┘
    │ │ check │
    │ └──────────┬────────────┘
    │ ┌──────────┴────────────┐
    └─ ─┤ close callbacks │
    └───────────────────────┘

timer
timers 阶段会执? setTimeout 和 setInterval。
?个 timer 指定的时间并不是准确时间,?是在达到这个时间后尽快执?回调,可能会因 为系统正在执?别的事务?延迟 I/O
I/O 阶段会执?除了 close 事件,定时器和 setImmediate 的回调 poll
poll 阶段很重要,这?阶段中,系统会做两件事情 执?到点的定时器 执? poll 队列中的事件
并且当 poll 中没有定时器的情况下,会发现以下两件事情 如果 poll 队列不为空,会遍历回调队列并同步执?,直到队列为空或者系统限制 如果 poll 队列为空,会有两件事发? 如果有 setImmediate 需要执?, poll 阶段会停?并且进?到 check 阶段执?
setImmediate
如果没有 setImmediate 需要执?,会等待回调被加?到队列中并?即执?回调 如果有别的定时器需要被执?,会回到 timer 阶段执?回调。 check
check 阶段执? setImmediate
close callbacks
close callbacks 阶段执? close 事件
并且在 Node 中,有些情况下的定时器执?顺序是随机的
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

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

// 这?可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进? event loop ?了不到 1 毫秒,这时候会执? setImmediate
// 否则会执? setTimeout

上?介绍的都是 macrotask 的执?情况, microtask 会在以上每个阶段完 成后?即执?

    setTimeout(()=>{
       console.log('timer1')
       Promise.resolve().then(function() {
         console.log('promise1')
       }) 
    }, 0)
    setTimeout(()=>{
       console.log('timer2')
       Promise.resolve().then(function() {
         console.log('promise2')
       }) 
    }, 0)
    // 以上代码在浏览器和 node 中打印情况是不同的
    // 浏览器中?定打印 timer1, promise1, timer2, promise2
    // node 中可能打印 timer1, timer2, promise1, promise2
    // 也可能打印 timer1, promise1, timer2, promise2

Node 中的 process.nextTick 会先于其他 microtask 执?,

    setTimeout(() => {
      console.log("timer1");
      Promise.resolve().then(function() {
         console.log("promise1"); 
      }); 
    }, 0);
    process.nextTick(() => {
       console.log("nextTick"); 
    });
    // nextTick, timer1, promise1

对于 microtask 来说,它会在以上每个阶段完成前清空 microtask 队 列,下图中的 Tick 就代表了 microtask。