事件循环 宏任务、微任务

485 阅读7分钟

任务队列( Event Queue )

image.png

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列。主线程内的任务执行完毕为空,会去任务队列读取对应的任务,推入主线程执行。 上述过程的不断重复就是 Event Loop (事件循环)。

在事件循环中,每进行一次循环操作称为tick,每一次 tick 的任务处理模型关键的步骤可以总结如下:

  • 1.在此次 tick 中选择最先进入队列的任务,如果有则执行(一次)
  • 2.检查是否存在 Microtasks ,如果存在则执行,直至清空Microtask Queue
  • 3.更新 render
  • 4.主线程重复执行上述步骤

什么是宏任务和微任务

宏任务 macrotasks:

  • setTimeout
  • setInterval
  • requestAnimationFrame
  • I/O
  • UI 交互事件
  • setImmediate (Node.js 环境)
  • script (整体代码)

requestAnimationFrame 和 setInterval对比说明

微任务 microtasks:

  • process.nextTick (Node.js 环境)
  • Promises
  • Object.observe (实时监测js中对象的变化)
  • MutationObserver (监视对DOM树所做更改)

microtask会在两种情况下执行:

  1. 任务队列(macrotask = task queue)回调后执行,前提条件是当前没有其他执行中的代码。
  2. 每个task末尾执行。

另外在处理microtask期间,如果有新添加的microtasks,也会被添加到队列的末尾并执行。

结合来看事件循环

执行 宏任务 => 微任务的Event Queue => 宏任务的Event Queue

  • 事件循环从宏任务开始;
  • 一个事件循环有一个或者多个任务队列;
  • 每个事件循环都有一个 microtask 队列;
  • macrotask队列就是我们常说的任务队列(task queue),microtask队列不是任务队列;
  • 一个任务可以被放入到 macrotask 队列,也可以放入 microtask 队列;
  • 当一个任务被放入 microtask 或者 macrotask 队列后,准备工作就已经结束,这时候可以开始执行任务了。

简要说明:开始 -> 取 task queue第一个task执行 -> 取microtask全部任务依次执行 -> 取task queue下一个任务执行 -> 再次取出microtask全部任务执行 -> ... 这样循环往复

image.png

Promise运行顺序总结:

  • promise的构造函数是同步执行,promise.then中的函数是异步执行。
  • 构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用。promise状态一旦改变则不能再变。
  • promise 的 .then 或者 .catch 可以被调用多次,但这里 Promise 构造函数只执行一次。或者说 promise 内部状态一经改变,并且有了一个值,那么后续每次调用 .then 或者 .catch 都会直接拿到该值。
  • 如果在一个then()中没有返回一个新的promise,则 return 什么下一个then就接受什么,如果then中没有return,则默认return的是 undefined.
  • then()的嵌套会先将内部的then()执行完毕再继续执行外部的then();
  • catch和then的连用,如果每一步都有可能出现错误,那么就可能出现catch后面接上then的情况。如果在catch中也抛出了错误,则后面的then的第一个函数不会执行,因为返回的 promise状态已经为rejected了

async、await执行顺序

使用 async 定义的函数,当它被调用时,它返回的其实是一个 Promise 对象。

当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。

await是一个让出线程的标志 。await后面的函数会先执行一遍,然后就会跳出整个async函数来执行后面js栈的代码,等本轮事件循环执行完了之后又会跳回到async函数中等待await后面表达式的返回值,如果返回值为非promise则继续执行async函数后面的代码,否则将返回的promise放入promise队列。 点击查看

示例

示例一

   setTimeout(function(){
        console.log(1)
    },0);
    new Promise(function(resolve){
        console.log(2)
        for( var i= 100000; i > 0 ; i-- ){
            i==1 && resolve()
        }
        console.log(3)
    }).then(function(){
        console.log(4)
    });
    console.log(5);
    

1、遇到 setTimeout,其回调函数console.log(1) 被分发到宏任务 Event Queue (任务队列)
2、遇到 Promise,先执行 console.log(2),输出 2;下一步执行for循环,即使for循环要累加到10万,也是在执行栈里面,等待for循环执行完毕以后,将Promise的状态从fulfilled切换到resolve,随后把回调函then里面的 console.log(4) 推到 微任务队列 里面去。接下来马上执行马上 console.log(3) ,输出 3
3、遇到 console.log(5),输出 5
4、第一轮执行完毕,没有正在执行的代码。符合上面讲的 microtask 执行条件,因此会将 microtask 中的任务优先执行,因此执行 console.log(4) ,输出 4
5、最后还剩 macrotask 里的setTimeout放入的函数 console.log(1) 最后执行,输出 1
6、至此,所有的都队列都已清空,执行完毕

// 2 3 5 4 1

示例二

console.log('script start');

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

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

console.log('script end');

1、整体 script 作为第一个宏任务进入主线程,遇到 console.log,输出 script start
2、遇到 setTimeout,其回调函数被分发到宏任务 Event Queue (任务队列)
3、遇到 Promise,其 then函数被分到到 微任务队列 中,记为 then1,之后又遇到了 then 函数,将其分到 微任务队列 中,记为 then2
4、遇到 console.log,输出 script end
5、第一轮执行完毕,没有正在执行的代码。符合上面讲的 microtask 执行条件,因此会将 microtask 中的任务优先执行,执行 then1,输出 promise1; 接着执行 then2,输出 promise2, 微任务清空
6、检查宏任务队列, 任务队列的setTimeout放入的函数 console.log('setTimeout') 最后执行,输出 setTimeout
7、至此,所有的都队列都已清空,执行完毕

// script start
// script end
// promise1
// promise2
// setTimeout

示例三

console.log('script start');

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

new Promise(resolve => {
    console.log('promise1');
    resolve();
    setTimeout(() => console.log('timeout2'), 10);
}).then(function() {
    console.log('then1')
})

console.log('script end');

1、事件循环从宏任务 (macrotask) 队列开始,最初始,宏任务队列中,只有一个 script(整体代码)任务;当遇到任务源 (task source) 时,则会先分发任务到对应的任务队列中去。所以,就和上面例子类似,首先遇到了console.log,输出 script start
2、遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1;
3、遇到 promise,new promise 中的代码立即执行,输出 promise1, 然后执行 resolve ,遇到 setTimeout ,将其分发到任务队列中去,记为 timemout2, 将其 then 分发到微任务队列中去,记为 then1;
4、接着遇到 console.log 代码,直接输出 script end
5、第一轮结束,接着检查微任务队列,发现有个 then1 微任务,执行,输出then1, 再检查微任务队列,发现已经清空
6、开始检查宏任务队列,执行 timeout1,输出 timeout1; 接着执行 timeout2,输出 timeout2
7、至此,所有的都队列都已清空,执行完毕

// script start
// promise1
// script end
// then1
// timeout1
// timeout2

可看流程图 image.png

示例四

setTimeout(function () {
  console.log("1");
}, 0);
async function async1() {
  console.log("2");
  const data = await async2();
  console.log("3");
  return data;
}
async function async2() {
  return new Promise((resolve) => {
    console.log("4");
    resolve("async2的结果");
  }).then((data) => {
    console.log("5");
    return data;
  });
}
async1().then((data) => {
  console.log("6");
  console.log(data);
});
new Promise(function (resolve) {
  console.log("7");
  //   resolve()
}).then(function () {
  console.log("8");
});

1、事件循环从宏任务 (macrotask) 队列开始,遇到 setTimeout 任务源,将其分发到任务队列中去,记为 timeout1;
2、遇到async1(),立即执行,输出 输出 2
3、执行await 右边的async2(),遇到promise,new promise 中的代码立即执行,输出 4, 然后执行 resolve ,将其 then 分发到微任务队列中去,记为 then1,resolve处理的data为 "async2的结果" ;
4、跳出async1,先执行后面的内容,遇到promise,一样立即执行new promise中的代码 输出 7,没有resolve,then失效( 只有resolve()之后,传递给then时,then中的函数才会被推到微任务中 );
5、第一轮结束,接着检查微任务队列,首先遇到then1, 输出 5,返回值data 为"async2的结果" ;
6、async1()中的await执行完毕,现在执行async1()后面的代码, 输出 3, 函数最后有返回值是Promise对象,可以用then方法指定下一步的操作,resolve处理的data为 "async2的结果",记为then2;
7、执行then2 输出 6,下一步输出data 输出 async2的结果;再检查微任务队列,发现已经清空
8、开始检查宏任务队列,执行 timeout1,输出 1
9、至此,所有的都队列都已清空,执行完毕

结果

1632626431(1).png

tips

JavaScript 是一门单线程语言,异步操作都是放到事件循环队列里面,等待主执行栈来执行的,并没有专门的异步执行线程。