JavaScript原理--事件循环(Event Loop)

913 阅读7分钟

理解Event Loop

javascript是单线程。单线程就意味着,所有任务需要排队,前一个任务结束,才会执行后一个任务。如果前一个任务耗时很长,后一个任务就不得不一直等着。

于是js所有任务分为两种:同步任务,异步任务

同步任务

调用立即得到结果的任务,同步任务在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务;

 console.log('a');
 for (let i = 0; i <5 ; i++) {
       console.log(i)
 }
 console.log('b');
 // a 0 1 2 3 4 5 b

异步任务

调用无法立即得到结果,需要额外的操作才能预期结果的任务,异步任务不进入主线程、而进入"任务队列"(task queue)的任务,只有"任务队列"通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。

 setTimeout(() => {
     console.log("加入任务队列:0s")
 },0)
 ​
 document.onclick = () => {
     console.log("加入任务队列:onclick")
 }
 ​
 setTimeout(() => {
     console.log("加入任务队列:1s")
 },1000)

JS引擎遇到异步任务(DOM事件监听、网络请求、setTimeout计时器等),会交给相应的线程单独去维护异步任务,等待某个时机(计时器结束、网络请求成功、用户点击DOM),然后由 事件触发线程 将异步对应的 回调函数 加入到消息队列中,消息队列中的回调函数等待被执行。

运行机制如下:

  1. 所有同步任务都在主线程上执行,形成一个[执行栈]

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

  3. 一旦"执行栈"中的所有同步任务执行完毕,就会读取"任务队列",开始执行。

    如何读取任务队列?

    主线程从"任务队列"中读取事件,这个过程是循环不断的,所以整个的这种运行机制又称为Event Loop(事件循环)

  4. 主线程不断重复上面的第三步。

举个例子

 console.log('script start')
 ​
 setTimeout(() => {
   console.log('timer 1 over')
 }, 1000)
 ​
 setTimeout(() => {
   console.log('timer 2 over')
 }, 0)
 ​
 console.log('script end')

image-20220831120526146.png

同步和异步的执行机制在ES5的情况下够用了,但是ES6会有一些问题。

宏任务(MacroTask/Task)和微任务(microtask)

宏任务:script全部代码、计时器、Ajax、读取文件、setImmediate(浏览器暂时不支持,只有IE10支持,具体可见MDN

微任务:Process.nextTick(Node独有)、Promise.then

EventLoop

也就是我们经常使用异步的原理,事件循环,是指浏览器或Node的一种解决javaScript单线程运行时不会阻塞的一种机制。

Node与浏览器的 Event Loop 差异

浏览器和 Node 环境下,microtask 任务队列的执行时机不同

  • Node 端,microtask 在事件循环的各个阶段之间执行
  • 浏览器端,microtask 在事件循环的 macrotask 执行完之后执行

执行顺序

  1. 同步任务

  2. process.nextTick

    process.nextTick()虽然它是异步API的一部分,但从技术上讲,它不是事件循环的一部分。

    它将 callback 添加到next tick队列。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

    换种理解方式:

    • 当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。
  3. 微任务

  4. 宏任务

  5. setmmediate

JS 引擎会将所有任务按照类别分到这两个队列中,首先在 宏任务 的队列中取出第一个任务,执行完毕后取出 微任务 队列中的所有任务顺序执行;之后再取 宏任务,周而复始,直至两个队列的任务都取完。

image-20220831123954209.png 举个例子

 <script> // 宏任务
     
     console.log('script start');
 ​
     setTimeout(function () {
         console.log('setTimeout');
     }, 0);
 ​
     new Promise((resolve) => {
         console.log('promise0');
         resolve('promise1');
     }).then((res) => {
         console.log(res);
     }).then(function () {
             console.log('promise2');
     });
 ​
     console.log('script end');
 </script>
  1. script :宏任务, push到宏任务队列,执行栈读取到有一个宏任务开始执行。

    名称
    Tasks(宏任务)script
    Microtasks(微任务)
    JS stack(JS执行栈)script
    Log
  2. console.log :同步任务,立即执行,打印‘script start’。

    名称
    Tasks(宏任务)script
    Microtasks(微任务)
    JS stack(JS执行栈)script
    Logscript start
  3. setTimeout:宏任务, 0s后push到任务队列。

    名称
    Tasks(宏任务)script,setTimeout callback
    Microtasks(微任务)
    JS stack(JS执行栈)script
    Logscript start
  4. new : 同步任务,立即执行,打印‘promise0’,并返回promise1。

    new Promise 相当于创建一个对象,是立即执行的

    名称
    Tasks(宏任务)script,setTimeout callback
    Microtasks(微任务)
    JS stack(JS执行栈)script
    Logscript start ,promise0
  5. Promise.then : 微任务, push到微任务队列。

    第二个Promise.then是第一个Promise.then执行后才调用,故不会继续push

    名称
    Tasks(宏任务)script,setTimeout callback
    Microtasks(微任务)Promise1.then
    JS stack(JS执行栈)script
    Logscript start ,promise0
  6. console.log:同步任务,立即执行,打印‘script end'’。

    名称
    Tasks(宏任务)script,setTimeout callback
    Microtasks(微任务)Promise1.then
    JS stack(JS执行栈)script
    Logscript start ,promise0,script end
  7. script执行完成出栈,JS执行栈pop。准备去执行微任务

    名称
    Tasks(宏任务)script,setTimeout callback
    Microtasks(微任务)Promise1.then
    JS stack(JS执行栈)
    Logscript start ,promise0,script end,promise1
  8. 执行微任务(即将微任务回调加入JS执行栈),打印‘script end'’后调用Promise.then,。

    名称
    Tasks(宏任务)script,setTimeout callback
    Microtasks(微任务)Promise1.then
    JS stack(JS执行栈)Promise callback
    Logscript start ,promise0,script end,promise1
  9. Promise.then : 微任务, push到微任务队列。

    名称
    Tasks(宏任务)script,setTimeout callback
    Microtasks(微任务)Promise1.then,Promise2.then
    JS stack(JS执行栈)Promise callback
    Logscript start ,promise0,script end,promise1
  10. Promise1.then执行完成,JS执行栈pop, 微任务队列pop。

    名称
    Tasks(宏任务)script,setTimeout callback
    Microtasks(微任务)Promise2.then
    JS stack(JS执行栈)
    Logscript start ,promise0,script end,promise1
  11. 微任务队列内还有值继续执行,执行和Promise1.then一样,打印‘promise2’后发现没有后续;则Promise2.then执行完成,JS执行栈pop, 微任务队列pop。

    名称
    Tasks(宏任务)script,setTimeout callback
    Microtasks(微任务)
    JS stack(JS执行栈)
    Logscript start ,promise0,script end,promise1,promise2
  12. script执行完成,由它而引发的微任务执也行完成,此时可以认为宏任务script执行完成,宏任务队列pop。

    名称
    Tasks(宏任务)setTimeout callback
    Microtasks(微任务)
    JS stack(JS执行栈)
    Logscript start ,promise0,script end,promise1,promise2
  13. 执行下一个宏任务,JS执行栈push,打印‘setTimeout’

    名称
    Tasks(宏任务)setTimeout callback
    Microtasks(微任务)
    JS stack(JS执行栈)setTimeout callback
    Logscript start ,promise0,script end,promise1,promise2,setTimeout
  14. 宏任务setTimeout执行完成,JS执行栈pop,宏任务队列pop

    名称
    Tasks(宏任务)
    Microtasks(微任务)
    JS stack(JS执行栈)
    Logscript start ,promise0,script end,promise1,promise2,setTimeout

异步编程的几种方案

观察者模式,又叫发布/订阅模式

假定,存在一个"信号中心",某个任务执行完成,就向信号中心"发布"(publish)一个信号,其他任务可以向信号中心"订阅"(subscribe)这个信号,从而知道什么时候自己可以开始执行。

 jQuery.subscribe("done", f2);
 ​
 function f1(){
   setTimeout(function () {
     // f1的任务代码
     jQuery.publish("done") ;   // f1执行完成后,向"信号中心"jQuery发布"done"信号,从而引发f2的执行。
   }, 1000);
 }
 ​
 jQuery.unsubscribe("done", f2);  // f2完成执行后,也可以取消订阅(unsubscribe)
 ​

Promise对象

优点:将异步操作以同步操作的流程表达出来,避免了层层嵌套的回调函数。此外,Promise对象提供统一的接口,使得控制异步操作更加容易。

缺点:

  • 无法取消Promise,一旦新建它就会立即执行,无法中途取消。
  • 如果不设置回调函数,Promise内部抛出的错误,不会反应到外部。
  • 当处于Pending状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
 var promise = new Promise(function(resolve, reject) {
   // ... some code
 ​
   if (/* 异步操作成功 */){
     resolve(value);
   } else {
     reject(error);
   }
 });
 // Promise实例生成以后,可以用then方法分别指定Resolved状态和Reject状态的回调函数。
 promise.then(function(value) {
   // success
 }, function(error) {
   // failure
 });
 ​

async与await

async函数完全可以看作多个异步操作,包装成的一个Promise对象,而await命令就是内部then命令的语法糖。 async函数返回一个 Promise对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

 async function timeout(ms) {
   await new Promise((resolve) => {
     setTimeout(resolve, ms);
   });
 }
 ​
 async function asyncPrint(value, ms) {
   await timeout(ms);
   console.log(value);
 }
 ​
 asyncPrint('hello world', 50);
 ​


最后一句

学习心得!若有不正,还望斧正。希望掘友们不要吝啬对我的建议。