Javascript 宏任务、微任务、事件循环(EventLoop)

161 阅读6分钟

Javascript 是一门单线程语言

宏任务、微任务、事件循环

事件循环事件循环、宏任务、微任务的关系
image.pngimage.png
  • 同步和异步任务分别进入不同的执行"场所",同步的进入主线程,异步的进入Event Table并注册函数
  • 当指定的事情完成时,Event Table会将这个函数移入Event Queue
  • 主线程内的任务执行完毕为空,会去Event Queue读取对应的函数,进入主线程执行
  • 上述过程会不断重复,也就是常说的Event Loop(事件循环)

除了广义的同步任务和异步任务,任务还有更精细的定义:

  • macro-task:宏任务
  • micro-task:微任务

不同类型的任务会进入对应的 Event Queue(宏任务队列、微任务队列)

宏任务 macro-task

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

微任务 micro-task

Promise.then(非new Promise)
await
Object.observe
MutationObserver
process.nextTick(Node.js 环境)

事件循环

事件循环的具体流程如下:

  1. 从宏任务队列中,按照入队顺序,找到第一个执行的宏任务,放入调用栈,开始执行;
  2. 执行完该宏任务下所有同步任务后,即调用栈清空后,该宏任务被推出宏任务队列,然后微任务队列开始按照入队顺序,依次执行其中的微任务,直至微任务队列清空为止
  3. 当微任务队列清空后,一个事件循环结束;
  4. 接着从宏任务队列中,找到下一个执行的宏任务,开始第二个事件循环,直至宏任务队列清空为止。

注意:

  • setTimeout 属于宏任务
  • Promise 本身是同步的立即执行函数,Promise.then 属于微任务
  • async方法执行时,遇到await会立即执行表达式,表达式之后的代码放到微任务执行
  • 当我们第一次执行的时候,解释器会将整体代码script放入宏任务队列中,因此事件循环是从第一个宏任务开始的;
  • 如果在执行微任务的过程中,产生新的微任务添加到微任务队列中,也需要一起清空;微任务队列没清空之前,是不会执行下一个宏任务的。

相关代码执行顺序问题

Q1:以下代码执行结果

console.log('1')

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

new Promise((resolve) => {
  console.log('3')
  resolve()
})
.then(() => { //第1个then
   console.log('4')
})
.then(() => { //第2个then
   console.log('5')
})

console.log('6')

// 结果:1、3、6、4、5、2

// 整个script入宏任务队列
// 输出1 -> setTimeout入宏任务队列 -> 输出3 -> 第1个then入微任务队列 ->  输出6 
// 输出4 -> 第2个then入微任务队列 -> 输出5 
// 微任务队列清空,执行下一个宏任务setTimeout 输出2

Q2:以下代码执行结果

console.log('1');

setTimeout(()=>{ //setTimeout1
   console.log('2')
},0)

new Promise((resolve, reject)=>{
    console.log('3')
    resolve()
}).then(()=>{ //then1
    console.log('4')
    setTimeout(()=>{ //setTimeout2
       console.log('5')
    },0)
    new Promise((resolve, reject)=>{
       console.log('6')
       resolve()
    }).then(()=>{ //then2
       console.log('7')
    })
})

console.log('8');

// 结果:1、3、8、4、6、7、2、5

// 整个script入宏任务队列
// 输出1 -> setTimeout1入宏任务队列 -> 输出3 -> then1入微任务队列 ->  输出8 
// 执行微任务队列
// 输出4 -> setTimeout2入宏任务队列 -> 输出6 -> then2入微任务队列
// 输出7 ,微任务队列清空
// 执行下一个宏任务setTimeout1 输出2
// 执行下一个宏任务setTimeout2 输出5

Q4:以下代码执行结果

console.log('1')

setTimeout(()=>{ //setTimeout1
    console.log('2')
    setTimeout(()=>{ //setTimeout2
        console.log('3')
    },0)
    new Promise((resolve, reject)=>{
        console.log('4')
        resolve()
    }).then(()=>{ //then1
        console.log('5')
    })
},0)

new Promise((resolve, reject)=>{
    console.log('6')
    resolve()
}).then(()=>{ //then2
    console.log('7')
})

console.log('8')

// 结果:1、6、8、7、2、4、5、3

// 整个script入宏任务队列
// 输出1 -> setTimeout1入宏任务队列 -> 输出6 -> then2入微任务队列 ->  输出8 
// 执行微任务队列 then2,输出7 ,微任务队列清空
// 执行宏任务setTimeout1,输出2 -> setTimeout2入宏任务队列 -> 输出4 -> then1入微任务队列 
// 执行微任务队列 then1,输出5 ,微任务队列清空
// 执行宏任务setTimeout2,输出3

Q3:以下代码执行结果

async function async1() {
    console.log('1');
    await async2(); 
    console.log('2');
}
async function async2() {
    console.log('3');
}

console.log('4');
setTimeout(() => {
    console.log('5');
}, 0);
async1().then(function () { //then1
    console.log('6');
});
new Promise(function (reslove) {
    console.log('7');
    reslove();
}).then(function () { //then2
    console.log('8');
})
console.log('9');

// 结果:4、1、3、7、9、2、8、6、5

// 整个script入宏任务队列
// 输出4  -> setTimeout入宏任务队列 -> async1 输出1 -> async2 输出3 await后面加入微任务队列 -> 输出7 -> then2入微任务队列 -> 输出9
// 执行await后面,输出2 -> then1入微任务队列
// 执行then2,输出8
// 执行then1,输出6,微任务队列清空
// 执行setTimeout,输出5

Q5:以下代码执行结果

console.log('1');

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

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

// 结果:1,7,6,8,2,4,3,5,9,11,10,12
// 注意,node环境下的事件监听依赖与前端环境不完全相同,输出顺序可能会有误差

Q6:以下代码执行结果及思考

for(var i=0;i<5;i++){ //var
    setTimeout(function() {
      console.log(i)
    }, 1000)
}
// 结果:5,5,5,5,5

如何输出: 0,1,2,3,4 ?

// 闭包自执行实现
for(var i=0;i<5;i++){
    (function(j){
        setTimeout(function() {
          console.log(j)
        }, 1000)
    })(i)
}

// 函数实现
function output(i){
    setTimeout(function() {
          console.log(i)
    }, 1000)
}
for(var i=0;i<5;i++){
    output(i)
}

// let
for(let i=0;i<5;i++){
    setTimeout(function() {
      console.log(i)
    }, 1000)
}

也可以思考如何输出 0 1 2 3 4 5

Q7:以下代码执行结果(不是很明白的一个问题)

<div class="outer">
  <div class="inner">点击一下试试</div>
</div>

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('setTimeout');
  }, 0);
  Promise.resolve().then(function() {
    console.log('new Promise');
  });
  outer.setAttribute('data-random', Math.random());
}

inner.addEventListener('click', onClick);
outer.addEventListener('click', onClick);

//页面点击触发事件

结果:

click          //inner的click
new Promise    //inner的promise
mutate
click          //outer的click
new Promise    //outer的promise
mutate
setTimeout     //inner的timeout
setTimeout     //outer的timeout

作者理解的,忘了原地址了 修改

var outer = document.querySelector('.outer');
var inner = document.querySelector('.inner');

const onInnerClick = (e) => {
  console.log('inner cilcked');
  setTimeout(function() {
    console.log('inner timeout');
  }, 0);
  Promise.resolve().then(function() {
    console.log('inner promise');
  });
}

const onOuterClick = (e) => {
  console.log('outer clicked');
  setTimeout(function() {
    console.log('outer timeout');
  }, 0);
  Promise.resolve().then(function() {
    console.log('outer promise');
  });
}

inner.addEventListener('click', onInnerClick);
outer.addEventListener('click', onOuterClick);

inner.click();

修改后结果:

inner cilcked
outer clicked
inner promise
outer promise
inner timeout
outer timeout

作者理解的,忘了原地址了

造成以上巨大差异的原因是,手动点击,不是通过函数进入执行栈的方式触发点击事件的回调,所以inner 的回调执行完了主线程中的执行栈就是空的可以直接执行队列中任务,然后事件冒泡导致的回调函数才被推入栈运行;而 click 方法的点击则是通过将 click 推入栈中执行来达到的,inner 的点击回调执行完了之后 click 方法并没有被弹出栈,而是直接执行冒泡的下一个回调,由于下一个回调有一个重复的 属性设置 这是不会重复触发 MutationObserver的所以 mutate 的输出只会有一个。等所有的冒泡回调被执行完毕 click 函数才会被弹出栈。

参考:

juejin.cn/post/706158…

推荐一个可以在线看代码流程的网站:loupe

视频学习:到底什么是 Event Loop 呢?

这一次,彻底弄懂 JavaScript 执行机制

做一些动图,学习一下 EventLoop

深入解析你不知道的 EventLoop 和浏览器渲染、帧动画、空闲回调(动图演示)