浅谈JavaScript中的eventloop

面试被问到了JavaScript的事件循环。

答:JavaScript先执行代码中的同步部分,然后从任务循环里读取任务。这种读取执行的循环就是任务循环。

又问:宏任务和微任务有什么区别。

答:。。。

本着面试题目广而不深的原则看了一些面试题,都只是浅尝辄止的扫过,并没有深入理解。今天花了点时间看了一些eventloop的资料。学习需要翻阅很多资料,集众家之长。再结合自己的思考。像前端大佬阮一峰的技术博客关于eventloop,2013年版的他后来也承认是自己的理解有错误,后面重新写了一篇,但是后来重新写的一篇竟然没有对宏任务和微任务的解释。对此,小白表示不能理解。

首先贴上我觉得比较好的一篇技术博客。

https://segmentfault.com/a/1190000016278115

自我总结。

浏览器和node中的JavaScript对eventloop的实现方式相似但不相同。

EventLoop是一个技术模型在不同的地方有不同的实现。浏览器和NodeJS基于不同的技术实现了各自的Event Loop。

宏队列和微队列

宏队列

  • setTimeout
  • setInterval
  • setImmediate (Node独有)
  • requestAnimationFrame (浏览器独有)
  • I/O
  • UI rendering (浏览器独有)

微队列

  • process.nextTick (Node独有)
  • Promise
  • Object.observe
  • MutationObserver

那么浏览器对JavaScript代码的执行流程如下

  1. 执行全局Script同步代码,这些同步代码有一些是同步语句,有一些是异步语句(比如setTimeout等);
  2. 全局Script代码执行完毕后,调用栈Stack会清空;
  3. 从微队列microtask queue中取出位于队首的回调任务,放入调用栈Stack中执行,执行完后microtask queue长度减1;
  4. 继续取出位于队首的任务,放入调用栈Stack中执行,以此类推,直到直到把microtask queue中的所有任务都执行完毕。注意,如果在执行microtask的过程中,又产生了microtask,那么会加入到队列的末尾,也会在这个周期被调用执行;
  5. microtask queue中的所有任务都执行完毕,此时microtask queue为空队列,调用栈Stack也为空;
  6. 取出宏队列macrotask queue中位于队首的任务,放入Stack中执行;
  7. 执行完毕后,调用栈Stack为空;
  8. 重复第3-7个步骤;

我觉得这个流程是理解这篇博客的核心,理解了这个流程任何代码的输出均信手拈来。(至少我是这样)

我对流程的理解如下

  1. 执行代码的同步部分
  2. 把微任务队列里的任务全部执行
  3. 执行宏任务的第一个任务(执行完有可能产生微任务),如果产生了微任务,将微任务推进微任务队列
  4. 重复2-4

这个重复的过程,就是我理解的eventloop。

理解了这个,那么任何代码的执行顺序都不再是问题

示例

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})

setTimeout(() => {
  console.log(6);
})

console.log(7);

如果要执行这段代码,我们首先假设自己是一个浏览器。

(我是一个浏览器)(我是一个浏览器)(我是一个浏览器)(我是一个浏览器)(我是一个浏览器)(我是一个浏览器)


执行第一步 

console.log(1);
主线程宏队列微队列
console.log(1)

输出 1

执行第二步 

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});
主线程宏队列微队列
第一个setTimeoutconsole.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });

优先执行同步部分,setTimeout是宏任务所以这里不会有任何输出,宏任务我把它的回调函数放到宏队列,至此,输出仍然是1,

执行第三步

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})
主线程宏队列微队列
Promiseconsole.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
promise

promise是微任务,我把它放进微队列,这里有一个注意点,promise的构造部分是同步代码,所以会执行。此时的输出为1,4

执行第四步

setTimeout(() => {
  console.log(6);
})
主线程宏队列微队列
第二个setTimeoutconsole.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
promise
console.log(6);

执行到第二个setTimeOut,宏任务,我把它的回调函数放进宏队列。

执行第五步

console.log(7);
主线程宏队列微队列
console.log(7)console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
promise
第二个setTimeout的回调

这是同步代码,直接执行。此时的输出为1,4,7,好了,同步代码执行完毕,按照流程,我要执行微队列里的任务了,我要把微队列的任务一次拿到主线程中执行。因为众所周知,线程才是执行任务的基本单位。此时任务变成这样

主线程宏队列微队列
promiseconsole.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
console.log(6);

执行promise

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
})

promise执行resolve(5),console.log(5),此时输出变成1,4,7,5,主线程执行完promise之后空了,这时候微队列也空了,读取宏队列的第一个任务。也就是第一个setTimeout的回调

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});
主线程宏队列微队列
console.log(2)console.log(6);Promise.resolve().then(() => {
    console.log(3)
  });

执行主线程的代码,输出变成1,4,7,5,2,主线程空了,执行微队列

主线程宏队列微队列
Promise.resolve().then(() => {
    console.log(3)
  });
console.log(6);

输出变成1,4,7,5,2,3,执行之后主线程和微队列都空了,再读取宏队列,console.log(6)

主线程宏队列微队列
console.log(6);

执行主线程中的任务输出变成1,4,7,5,2,3,6。至此三个队列都空了。浏览器工作顺利完成!我们看看真实情况的输出。

膨胀了之后来一个进阶题

console.log(1);

setTimeout(() => {
  console.log(2);
  Promise.resolve().then(() => {
    console.log(3)
  });
});

new Promise((resolve, reject) => {
  console.log(4)
  resolve(5)
}).then((data) => {
  console.log(data);
  
  Promise.resolve().then(() => {
    console.log(6)
  }).then(() => {
    console.log(7)
    
    setTimeout(() => {
      console.log(8)
    }, 0);
  });
})

setTimeout(() => {
  console.log(9);
})

console.log(10);

直接看出答案,同步部分1,4,10,再执行微队列里的Promise,输出5,6,7,依次执行三个setTimeout,第一个输出2,3,第二个输出8.第三个输出9。

全部输出为1,4,10,5,6,7,2,3,8,9。

Node中的EventLoop

node中的Eventloop相对而言比浏览器端复杂一点点。因为有node中才可以使用的Process.nextTick

简而言之,node中的微队列有两个,一个是Next Tick Queue微队列,它是放Process.nextTick的回调,还有一个Other Microtask Queue,它是放promise的。

node在执行的时候,首先会检查Next Tick Queue,再检查Other Microtask Queue,然后就和浏览器端的差不多。

总结代码的执行顺序

在Node.js中

代码同步部分--->Process.nextTick()--->promise--->setTimeout--->setInterval--->setImmediate

老规矩 代码测试

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')
    })
})

new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(function() {
    console.log('8')
})
process.nextTick(function() {
  console.log('6');
})

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,process.nextTick输出6,promise输出7,8,第一个setTimeout输出2,4,3,5,第二个promise输出9,11,10,12

最终输出1,6,7,8,2,4,3,5,9,11,10,12

perfect!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值