小白也能看懂的javascript的事件循环机制

·  阅读 153
小白也能看懂的javascript的事件循环机制

1.JavaScript是单线程

javascript的一个大特点就是单线程,也就是说同一个时间只能做一件事,这样能够避免一个线程在更新DOM节点,而另外一个却在删除该DOM节点。

2.JavaScript的异步

由于javascript是单线程,全部代码只能自上而下执行,但这样子又会有很多的麻烦。如果上一行代码解析时间长,会对下面的代码造成阻塞,这将导致很差的用户体验。所以聪明的程序员想到了将任务分为同步任务异步任务

  • 任务进入执行栈时先判断是同步任务还是异步任务,同步任务直接进入主线程,而异步任务放到Event Table
  • 等到主线程任务完成,会去Event Queue读取对于的函数,进入主线程执行。
  • 这些过程不断重复,就是所谓的Event Loop(事件循环)。

3.什么算是异步任务

  • 定时器都是异步操作
  • 事件绑定都是异步操作
  • AJAX中一般我们都采取异步操作
  • 回调函数可以理解为异步(不是严谨的异步操作)
console.log('开始')
setTimeout(()=>{
    console.log('执行中...')
},2000)
console.log('结束')
复制代码

运行结果:开始 结束 执行中...

  • console.log('开始')作为同步任务,直接放入主线程;
  • setTimeout()属于异步任务,放入Event Table,并注册匿名函数。当两秒钟后才被放入Event Queue
  • console.log('结束')属于同步任务,也被放入主线程。
  • 如果主线程的任务在两秒钟内执行完成,也不会立即执行setTimeout()的任务,因为只有两秒钟后注册的匿名函数才会被推到Event Queue中,才能进入主线程执行。还有就是主线程的任务在两秒钟内没有执行完成,setTimeout注册的函数被推到到Event Queue也不会被主线程立即调用。
setTimeout(() => {
    console.log('fine')
},2000)

sleep(10000)
console.log('ok');
复制代码

运行结果:ok fine

  • console.log('fine')进入Event Table并注册,计时开始。
  • 执行sleep函数;
  • 3秒到了,setTimeout完成,task()进入Event Queue,但是sleep也太慢了吧,还没执行完,只好等着。 sleep终于执行完了 -console.log('ok')再执行;
  • task()终于从Event Queue进入了主线程执行。

4.宏任务与微任务

我们先来看一个例子:

setTimeout(()=>{
     console.log('开始执行')
 });
 
 new Promise((resolve)=>{
     resolve(console.log('我在哪'));
 }).then(function(){
     console.log('我是then函数')
 });
 
 console.log('终于结束了');
复制代码

按照上面的规则,我们想的执行结果应该为:我在哪 终于结束了 开始执行 我是then函数

但是实际上运行的结果却是: 我在哪 终于结束了 我是then函数 开始执行

WTH,难道是异步任务的执行顺序,不是前后顺序,而是另有规定?事实上,按照异步和同步的划分方式,并不准确。 除了广义上的同步任务与异步任务,我们对任务还有更加精细的定义:

  • macro-task(宏任务):包括整体代码script,setTimeout,setInterval
  • micro-task(微任务):Promise,process.nextTick

按照上面对任务更加精细的分配,JS的执行机制是:

  • 执行一个宏任务,过程中如果遇到微任务,则将微任务放到微任务的"事件队列"里;
  • 当前宏任务执行完毕,查看微任务的"事件队列",并将事件队列里的全部微任务执行完毕。
  • 进入宏任务"事件队列",继续以上的步骤。
console.log("begin");

setTimeout(()=>{
    console.log('计时器1');
    process.nextTick(function() {
        console.log('nextTick1');
    })
})

new Promise(function(resolve) {
    resolve(console.log('outterP'));
}).then(function() {
    console.log('outterT');
})

setTimeout(()=>{
    console.log('计时器2');
    process.nextTick(function() {
        console.log('nextTick2');
    })
    new Promise(function(resolve) {
    resolve(console.log('innerP'));
}).then(function() {
    console.log('innerT');
})
})

console.log('end');

复制代码

第一轮事件循环:

  • 整个script作为第一个宏任务进入主线程,遇到console.log,打印出begin;
  • 遇到第一个setTimeout,则里面的回调函数放到宏任务的Event Queue中;
  • 遇到new promise,直接执行,输出outterP,then函数则分配到微任务·Event Queue·中;
  • 遇到第二个·setTimeout·,则里面的回调函数放到宏任务的Event Queue中;
  • 再次遇到console.log,则打印出end;
  • 接着执行微任务Event Queue,执行console.log打印出outterT

第一轮事件循环输出:begin outterP end outterT;

第二轮事件循环:

  • 先到宏任务队列里面找到第一个宏任务;
  • 遇到console.log 打印输出 计时器1
  • 遇到process.nextTick,则放到微任务Event Queue中;
  • 第二轮宏任务执行完毕,接着执行微任务Event Queue,执行console.log打印出nextTick1

第二轮事件循环输出:计时器1 nextTick1;

第三轮事件循环:

  • 进入宏任务队列,找到第一个宏任务(前面的宏任务执行的时候就出队了,所以又是找第一个)
  • 遇到console.log,打印出计时器2
  • 遇到process.nextTick,将其回调函数放到微任务Event Queue
  • 遇到new promise 输出innerP,将then的回调函数放到微任务Event Queue
  • 第三轮宏任务执行完毕,接着执行微任务队列,输出nextTick2innerT;

第三轮事件循环输出:计时器2 innerP nextTick2 innerT

所以完整的输出为:begin outterP end outterT 计时器1 nextTick1 计时器2 innerP nextTick2 innerT

参考链接:

解读 JavaScript 之引擎、运行时和堆栈调用

JS中的任务队列

宏任务和微任务都有哪些?

分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改