头图

由于JS设定之初,设计者并不希望过于复杂,所以将JS设置成单线程语言,不过由于后来语言发展,单线程反而成为历史遗留问题,不过这并不本文想讲解的内容。。。好了,开始正题。

我们都知道JS对面是从上至下,从左到右的顺序执行的。但是,这是在代码块内没有异步代码的情况下。一旦存在异步代码,就需要考虑JS的循环机制(又称浏览器循环机制)。理解js的事件循环机制,能够很大程度的帮我们更深层次的理解平时遇到的一些很疑惑的问题。

我们这里的JS 分为浏览器端的JS和服务器端的JS(如NodeJS),以下内容暂时只讲浏览器端JS的循环机制。

我们先来说一下概念,以下图说明:
事件循环机制草图
在浏览器的执行环境里:
⑴一个主线程,线程内包含堆栈;
⑵还有WebAPs,包含DOM操作、ajax请求以及setTimeout异步操作等;当在执行过程中遇到一些类似于setTimeout等异步操作的时候,会交给浏览器的其他模块(以webkit为例,是webcore模块)进行处理。对于图中WebAPIs提到的三种API,webcore分别提供了DOM Binding、network、timer模块来处理底层实现。
⑶队列其实是回调函数队列(网上不少人设定为任务队列)。不存放宏任务,微任务所有代码(包含关键字的代码),只存放其中包含的代码段(即回调函数)。但分宏任务的回调函数队列和微任务回调函数队列,以下简化为宏任务队列和微任务队列。

JS每句代码为一个任务,任务分为同步任务和异步任务。异步任务又分为宏任务(Macro Task)和微任务(Micro Task),微任务优先级高于宏任务。
同任务优先级高低从左至右排列如下:

  1. 宏任务: script(整体代码), setTimeout, setInterval, setImmediate(Nodejs,v8环境), I/O, UI rendering
  2. 微任务: process.nextTick(Nodejs,v8环境), Promises, Object.observe, MutationObserver
    async await是ES7内容,本质上是promise的语法糖,优先级与promise相同。
    PS:如果面试官问的是浏览器而不是NodeJS,注意说优先级时提一嘴运行环境。

然后再来说一下JS执行引擎如何处理JS。
首先,JS会从上至下,从左至右读取每行代码,每读取一次代码,都会将该代码块区分为同步任务还是异步任务,是异步任务的话会区分为宏任务还是微任务。若为同步任务,则放置于栈(执行栈)内执行,若为异步任务,则存放到WebAPIs中,等到时间达到指定时间时,会存入宏任务或微任务队列中。执行栈内函数执行完毕,然后会去微任务队列取任务,直到微任务队列清空。然后检查宏队列,去宏任务队列取任务,并且每一个宏任务执行完毕都会去微队列跑一遍,看看有没有新的微任务,有的话再把微任务清空。这样依次循环,直到全部任务执行完毕。

提升点:

  1. 如果在执行任务时加入了一个微任务,此时在处理宏任务,则会先处理完当前宏任务再去微任务队列处理新的微任务。
  2. 如果在执行任务时加入了一个宏任务,此时在处理微任务,会处理完微任务队列的所有微任务再去处理宏任务。如果在处理宏任务,则会在宏任务队列最后一个才处理这个昕加入的宏任务。

举个例子:

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

 let promise = new Promise(resolve => {
   console.log(2);
   resolve();
 }).then(data => {
   console.log(3);
   let promises = new Promise(resolve => {
     console.log(4);
     resolve();
   }).then(data => {
      console.log(5);
   });
 });
  let promisess = new Promise(resolve => {
   console.log(6);
   resolve();
 }).then(data => {
   console.log(7);
   let promisesss = new Promise(resolve => {
     console.log(8);
     resolve();
   }).then(data => {
      console.log(9);
   });
 });   
 console.log(10);

其结果为:1,2,6,10,3,4,7,8,5,9,setTimeOut
解析:
首先执行同步代码,即执行出来1,2,6,10;然后执行promise里then的回调函数(因为promise为微任务,settimeout为宏任务,所以先执行promise里的微任务),同为微任务情况下,从上至下执行出3,4,7,8;现在执行环境里还剩嵌套在promise的then微任务和settimeout宏任务,所以继续执行嵌套在promise的then微任务,得出5,9;最后因为环境只剩下一个宏任务,所以执行最后宏任务得出setTimeOut。

2021年6月13号更新

宏任务在DOM渲染后执行,如settimeout,而微任务在DOM渲染前执行,如promise,async/await。

在event loop 中,
1.每次call stack清空(即每次轮询结束),即同步任务执行完,
2.先执行微任务队列的微任务,微任务队列内的微任务执行完之后,
3.检查DOM结构,如果DOM结构改变,则重新渲染DOM,
4.DOM重新渲染后或无DOM结构更新则执行宏任务队列内的宏任务,每执行完一个宏任务,就去检查微任务队列内是否有微任务,若有则执行微任务队列的微任务,再检查DOM结构是否需要更新操作,再去执行宏任务队列的内宏任务,循环下去直到微任务和宏任务都执行完毕。

2022年3月27号更新

async function async1() {
console.log("async1 start");

await async2();

console.log("async1 end");

}

async function async2() {
console.log("async2");
await async3();
console.log("async2 end");
}
async function async3() {
console.log("async3");
}
console.log("script start");

setTimeout(function() {
// setTimeout放入event-loop中的macro-tasks队列,暂不执行

console.log("setTimeout");

}, 0);

async1();

new Promise(function(resolve) {
console.log("promise1");

resolve();

}).then(function() {
console.log("promise end");

});

console.log("script end");

其运行结果为:

script start
async1 start
async2
async3
promise1
script end
async2 end
promise end
async1 end
setTimeout

注:若题目内包含了ajax或者axios异步请求,其then函数与setTimeout的回调函数的内容谁先执行取决于异步请求数据返回的时间和setTimeout的延迟时间哪个更快,无法直接确定执行顺序。

2022年8月17日更新

console.log('1');

setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})

setTimeout(function() {
    console.log('9');
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})

执行顺序为:1,7,8,2,4,5,9,11,12

注:宏任务是比微任务先执行的。第一轮事件循环中,最先开始检查宏队列有无,无则该轮不执行宏任务,因为同步代码还没执行,所以就算写了宏任务也只会在第二轮检查出并执行,进而因为微任务的该轮全部执行的特点使得第一轮事件循环中看起来是微任务先比宏任务执行,但是从第二轮开始就宏任务先执行,而且微任务的创建也几乎只能从宏任务中来,因为第一轮同步代码都基本执行完了。


爱吃鸡蛋饼
55 声望8 粉丝