JS Event Loop 汇总

110 阅读14分钟

事件任务类型

  1. 知识点
  • 宏任务有:

    • <script>标签中的运行代码
    • setTimeout、setInterval的回调函数
    • 事件触发的回调函数,例如DOM EventsI/OrequestAnimationFrame、Ajax、UI交互等

注:详细来说requestAnimationFrame不属于宏任务或者微任务,每次 loop 结束发现需要渲染,在渲染之前执行的一个回调函数

  • 微任务有:

    • Promise的回调函数:then、catch、finally
    • MutationObserver:使用方式
    • queueMicrotask:使用方式
    • process.nextTick:Node独有
  1. 题目
async function async1() {
    console.log('1');
    await async2();
    console.log('2');
}

async function async2() {
    console.log('3');
}
requestAnimationFrame(()=>console.log(98))
console.log('4');
async1();

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

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

console.log('8');
  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');
// 执行顺序问题,考察频率挺高的,先自己想答案**
setTimeout(function () {
    console.log(1);
});
new Promise(function(resolve,reject){
    console.log(2)
    resolve(3)
}).then(function(val){
    console.log(val);
})
console.log(4);
const promise = new Promise((resolve, reject) => {
  console.log(1);
  resolve('success')
  console.log(2);
});
promise.then(() => {
  console.log(3);
});
console.log(4);
const first = () => (new Promise((resolve, reject) => {
  console.log(3);
  let p = new Promise((resolve, reject) => {
      console.log(7);
      setTimeout(() => {
          console.log(5);
          resolve(6);
          console.log(p)
      }, 0)
      resolve(1);
  });
  resolve(2);
  p.then((arg) => {
      console.log(arg);
  });

}));

first().then((arg) => {
  console.log(arg);
});
console.log(4);
const async1 = async () => {
  console.log('async1');
  setTimeout(() => {
      console.log('timer1')
  }, 2000)
  await new Promise(resolve => {
      console.log('promise1')
  })
  console.log('async1 end')
  return 'async1 success'
}
console.log('script start');
async1().then(res => console.log(res));
console.log('script end');
Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .catch(4)
  .then(res => console.log(res))
setTimeout(() => {
  console.log('timer2')
}, 1000)
const p1 = new Promise((resolve) => {
  setTimeout(() => {
      resolve('resolve3');
      console.log('timer1')
  }, 0)
  resolve('resovle1');
  resolve('resolve2');
}).then(res => {
  console.log(res)
  setTimeout(() => {
      console.log(p1)
  }, 1000)
}).finally(res => {
  console.log('finally', res)
})
console.log('script start');

setTimeout(() => {
  console.log('北歌');
}, 1 * 2000);

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


async function foo() {
  await bar()
  console.log('async1 end')
}
foo()

async function errorFunc () {
  try {
    await Promise.reject('error!!!')
  } catch(e) {
    console.log(e)
  }
  console.log('async1');
  return Promise.resolve('async1 success')
}
errorFunc().then(res => console.log(res))

function bar() {
  console.log('async2 end') 
}

console.log('script end');
console.log('1');

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

Promise.reject().then(() => {
  console.log('13');
}, () => {
  console.log('12');
})

new Promise((resolve) => {
  console.log('7');
  resolve();
}).then(() => {
  console.log('8')
})

setTimeout(() => {
  console.log('9');
  Promise.resolve().then(() => {
    console.log('10');
  })
  new Promise((resolve) => {
    console.log('11');
    resolve();
  }).then(() => {
    console.log('12')
  })
})
new Promise((resolve, reject) => {
  console.log(1)
  resolve()
})
.then(() => { // 微1-1
  console.log(2)
  new Promise((resolve, reject) => {
      console.log(3)
      setTimeout(() => { // 宏2
        reject();
      }, 3 * 1000);
      resolve() // TODO 注1
  })
    .then(() => { // 微1-2  TODO 注2
      console.log(4)
      new Promise((resolve, reject) => {
          console.log(5)
          resolve();
      })
        .then(() => { // 微1-4
          console.log(7)
        })
        .then(() => { // 微1-6
          console.log(9)
        })
    })
    .then(() => {  // 微1-5 TODO 注3
      console.log(8)
    })
})
.then(() => { // 微1-3
  console.log(6)
})

Promise.resolve()
  .then(() => {
    console.log('promise1');
    return new Promise((resolve, reject) => {
        setTimeout(() => {
          console.log('timer2')
          resolve()
        }, 0)
    })
      .then(async () => {
        await foo();
        return new Error('error1')
      })
      .then((ret) => {
        setTimeout(() => {
          console.log(ret);
          Promise.resolve()
          .then(() => {
            return new Error('error!!!')
          })
          .then(res => {
            console.log("then: ", res)
          })
          .catch(err => {
            console.log("catch: ", err)
          })
        }, 1 * 3000)
      }, err => {
        console.log(err);
      })
      .finally((res) => {
        console.log(res);
        throw new Error('error2')
      })
      .then((res) => {
        console.log(res);
      }, err => {
        console.log(err);
      })
  })
  .then(() => {
    console.log('promise2');
  })

function foo() {
  setTimeout(() => { 
    console.log('async1');
  }, 2 * 1000);
}

setTimeout(() => {
  console.log('timer1')
  Promise.resolve()
    .then(() => {
      console.log('promise3')
    })
}, 0)

console.log('start');
async function async1() {
    console.log('async1 start');
    new Promise(resolve => {
        try {
            throw new Error('error1');
        } catch (e) {
            console.log(e);
        }
        setTimeout(() => {
            // 宏3
            resolve('promise4');
        }, 3 * 1000);
    })
        .then(
            res => {
                // 微3-1
                console.log(res);
            },
            err => {
                console.log(err);
            }
        )
        .finally(res => {
            // 微3-2 // TODO注3
            console.log(res);
        });
    console.log(await async2()); // 微4-1  TODO-注1
    console.log('async1 end'); // 微4-2 // TODO-注2
}

function async2() {
    console.log('async2');
    return new Promise(resolve => {
        setTimeout(() => {
            // 宏4
            resolve(2);
        }, 1 * 3000);
    });
}

console.log('script start');

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

async1();

new Promise(resolve => {
    console.log('promise1');
    resolve();
})
    .then(() => {
        // 微1-2
        console.log('promise2');
        return new Promise(resolve => {
            resolve();
        }).then(() => {
            // 微1-3
            console.log('then 1-1');
        });
    })
    .then(() => {
        // 微1-4
        console.log('promise3');
    });

console.log('script end');

  1. 题目
console.log('script start')

async function async1() {
  await async2()
  console.log('async1 end')
}
async function async2() {
  console.log('async2 end') 
}
async1()

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

new Promise(resolve => {
  console.log('Promise')
  resolve()
})
  .then(function() {
    console.log('promise1')
  })
  .then(function() {
    console.log('promise2')
  })

console.log('script end')

注:### 关于73以下(金丝雀)版本和73版本的区别

  • 在老版本版本以下,先执行promise1promise2,再执行async1。每个await后都会跟一个promise进行包裹
  • 在73版本,先执行async1再执行promise1promise2

为什么settimeout有时候不准时?

而setTimeout() 的第二个参数(延时时间)只是告诉 JavaScript 再过多长时间把当前任务添加到任务队列中。如果任务队列是空的,并且主线程任务执行完毕了。那么添加的代码会立即执行;如果任务队列不是空的,那么它就要等前面的代码执行完了以后再执行。

HTML5标准规定了setTimeout()的第二个参数的最小值不得小于4毫秒,如果低于这个值,则默认是4毫秒

浏览器的 event loop

check

浏览器里面执行一个 JS 任务就是一个 event loop,每个 loop 结束会检查下是否需要渲染,是否需要处理 worker 的消息,通过这种每次 loop 结束都 check 的方式来综合渲染、JS 执行、worker 等,让它们都能在一个线程内得到执行(渲染其实是在别的线程,但是会和 JS 线程相互阻塞)。

这样就解决了渲染、JS 执行、worker 这三者的调度问题。

但是这样有没有问题?

我们会在任务队列中不断的放新的任务,这样如果有更高优的任务是不是要等所有任务都执行完才能被执行。如果是“急事”呢?

所以这样还不行,要给 event loop 加上“急事”处理的快速通道,这就是微任务 micro tasks。

micro tasks

任务还是每次取一个执行,执行完检查下要不要渲染,处理下 worker 消息,但是也给高优先级的“急事”加入了插队机制,会在执行完任务之后,把所有的急事(micro task)全部处理完。

这样,event loop 貌似就挺完美的了,每次都会检查是否要渲染,也能更快的处理 JS 的“急事”。

requestAnimationFrame

JS 执行完,开始渲染之前会有一个生命周期,就是 requestAnimationFrame,在这里面做一些计算最合适了,能保证一定是在渲染之前做的计算。

image.png

如果有人问 requestAnimationFrame 是宏任务还是微任务,就可以告诉他:requestAnimationFrame 是每次 loop 结束发现需要渲染,在渲染之前执行的一个回调函数,不是宏微任务。

event loop 的问题

上文聊过,虽然后面加入了 worker,但是主流的方式还是 JS 计算和渲染相互阻塞,这样就导致了一个问题:

每一帧的计算和渲染是有固定频率的,如果 JS 执行时间过长,超过了一帧的刷新时间,那么就会导致渲染延迟,甚至掉帧(因为上一帧的数据还没渲染到界面就被覆盖成新的数据了),给用户的感受就是“界面卡了”。

什么情况会导致帧刷新拖延甚至帧数据被覆盖(丢帧)呢?每个 loop 在 check 渲染之前的每一个阶段都有可能,也就是 task、microtask、requestAnimationFrame、requestIdleCallback 都有可能导致阻塞了 check,这样等到了 check 的时候发现要渲染了,再去渲染的时候就晚了。

所以主线程 JS 代码不要做太多的计算(不像安卓会很自然的起一个线程来做),要做拆分,这也是为啥 ui 框架要做计算的 fiber 化,就是因为处理交互的时候,不能让计算阻塞了渲染,要递归改循环,通过链表来做计算的暂停恢复。

除了 JS 代码本身要注意之外,如果浏览器能够提供 API 就是在每帧间隔来执行,那样岂不是就不会阻塞了,所以后来有了 requestIdeCallback。

requestIdleCallback

requestIdleCallback 会在每次 check 结束发现距离下一帧的刷新还有时间,就执行一下这个。如果时间不够,就下一帧再说。

如果每一帧都没时间呢,那也不行,所以提供了 timeout 的参数可以指定最长的等待时间,如果一直没时间执行这个逻辑,那么就算拖延了帧渲染也要执行。

这个 api 对于前端框架来说太需要了,框架就是希望计算不阻塞渲染,也就是在每一帧的间隔时间(idle时间)做计算,但是这个 api 毕竟是最近加的,有兼容问题,所以 react 自己实现了类似 idle callback 的fiber 机制,在执行逻辑之前判断一下离下一帧刷新还有多久,来判断是否执行逻辑。

总结

总之,浏览器里有 JS 引擎做 JS 代码的执行,利用注入的浏览器 API 完成功能,有渲染引擎做页面渲染,两者都比较纯粹,需要一个调度的方式,就是 event loop。

event loop 实现了 task 和 急事处理机制 microtask,而且每次 loop 结束会 check 是否要渲染,渲染之前会有 requestAnimationFrames 生命周期。

帧刷新不能被拖延否则会卡顿甚至掉帧,所以就需要 JS 代码里面不要做过多计算,于是有了 requestIdleCallback 的 api,希望在每次 check 完发现还有时间就执行,没时间就不执行(这个deadline的时间也作为参数让 js 代码自己判断),为了避免一直没时间,还提供了 timeout 参数强制执行。

防止计算时间过长导致渲染掉帧是 ui 框架一直关注的问题,就是怎么不阻塞渲染,让逻辑能够拆成帧间隔时间内能够执行完的小块。浏览器提供了 idelcallback 的 api,很多 ui 框架也通过递归改循环然后记录状态等方式实现了计算量的拆分,目的只有一个:loop 内的逻辑执行不能阻塞 check,也就是不能阻塞渲染引擎做帧刷新。所以不管是 JS 代码宏微任务、 requestAnimationCallback、requestIdleCallback 都不能计算时间太长。这个问题是前端开发的持续性阵痛。

nodejs应用

nodejs线程

  • node是单线程也指的是主线程是单线程的
  • node进程创建了哪些线程
    • Javascript 执行主线程

    • watchdog 监控线程用于处理调试信息

    • v8 task scheduler 线程用于调度任务优先级,加速延迟敏感任务执行

    • 4 个 v8 线程 主要用来执行代码调优与 GC 等后台任务;以及用于异步 I / O 的 libuv 线程池。

nodejs事件循环

浏览器的 Event Loop 只分了两层优先级,一层是宏任务,一层是微任务。但是宏任务之间没有再划分优先级,微任务之间也没有再划分优先级。

而 Node.js 宏任务之间也是有优先级的,比如定时器 Timer 的逻辑就比 IO 的逻辑优先级高,因为涉及到时间,越早越准确;而 close 资源的处理逻辑优先级就很低,因为不 close 最多多占点内存等资源,影响不大。

于是就把宏任务队列拆成了五个优先级:Timers、Pending、Poll、Check、Close。

image.png 解释一下这五种宏任务:

Timers Callback: 涉及到时间,肯定越早执行越准确,所以这个优先级最高很容易理解。

Pending Callback:处理网络、IO 等异常时的回调,有的系统会等待发生错误的上报,所以得处理下。

Poll Callback:处理 IO 的 data,网络的 connection,服务器主要处理的就是这个。

Check Callback:执行 setImmediate 的回调,特点是刚执行完 IO 之后就能回调这个。

Close Callback:关闭资源的回调,晚点执行影响也不到,优先级最低。

image.png 还有一点不同要特别注意:

Node.js 的 Event Loop 并不是浏览器那种一次执行一个宏任务,然后执行所有的微任务,而是执行完一定数量的 Timers 宏任务,再去执行所有微任务,然后再执行一定数量的 Pending 的宏任务,然后再去执行所有微任务,剩余的 Poll、Check、Close 的宏任务也是这样。

其实按照优先级来看很容易理解:

假设浏览器里面的宏任务优先级是 1,所以是按照先后顺序依次执行,也就是一个宏任务,所有的微任务,再一个宏任务,再所有的微任务。

image.png 而 Node.js 的 宏任务之间也是有优先级的,所以 Node.js 的 Event Loop 每次都是把当前优先级的所有宏任务跑完再去跑微任务,然后再跑下一个优先级的宏任务。 image.png

也就是是一定数量的 Timers 宏任务,再所有微任务,再一定数量的 Pending Callback 宏任务,再所有微任务这样。

为什么说是一定数量呢?

因为如果某个阶段宏任务太多,下个阶段就一直执行不到了,所以有个上限的限制,剩余的下个 Event Loop 再继续执行。

除了宏任务有优先级,微任务也划分了优先级,多了一个 process.nextTick 的高优先级微任务,在所有的普通微任务之前来跑。 所以,Node.js 的 Event Loop 的完整流程就是这样的:

  • Timers 阶段:执行一定数量的定时器,也就是 setTimeout、setInterval 的 callback,太多的话留到下次执行
  • 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
  • Pending 阶段:执行一定数量的 IO 和网络的异常回调,太多的话留到下次执行
  • 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
  • Idle/Prepare 阶段:内部用的一个阶段
  • 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
  • Poll 阶段:执行一定数量的文件的 data 回调、网络的 connection 回调,太多的话留到下次执行。如果没有 IO 回调并且也没有 timers、check 阶段的回调要处理,就阻塞在这里等待 IO 事件
  • 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
  • Check 阶段:执行一定数量的 setImmediate 的 callback,太多的话留到下次执行。
  • 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务
  • Close 阶段:执行一定数量的 close 事件的 callback,太多的话留到下次执行。
  • 微任务:执行所有 nextTick 的微任务,再执行其他的普通微任务

还有一个特别要注意的点,就是 poll 阶段:如果执行到 poll 阶段,发现 poll 队列为空并且 timers 队列、check 队列都没有任务要执行,那么就阻塞的等在这里等 IO 事件,而不是空转。 这点设计也是因为服务器主要是处理 IO 的,阻塞在这里可以更早的响应 IO。

完整的event loop

image.png

process.nextTick、setImmediate、setTimeout对比

process.nextTick

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

new Promise((resolve=>{
  resolve(2)
})).then((res)=>{
  console.log('promise',res);
})
process.nextTick(()=>{
  console.log(1);
  process.nextTick(()=>{console.log(3);});
});
/*
1
3
promise 2
TIMEOUT FIRED
*/
复制代码

setImmediate

setImmediate(()=> {
  console.log(1);
  setImmediate(()=>{console.log(2);});
});

setTimeout(()=> {
  console.log('TIMEOUT FIRED');
}, 0);
复制代码

这个结果不固定,同一台机器测试结果也有两种:

// TIMEOUT FIRED =>1 =>2
或者
//  1=>TIMEOUT FIRED=>2
复制代码
复制代码
  1. 事件队列进入timer,性能好的 小于1ms,则不执行回调继续往下。若此时大于1ms, 则输出 TIMEOUT FIRED 就不输出步骤3了。
  2. poll阶段任务为空,存在setImmediate 直接进入setImmediate 输出1
  3. 然后再次到达timer 输出 TIMEOUT FIRED
  4. 再次进入check 阶段 输出 2

原因在于setTimeout 0 node 中至少为1ms,也就是取决于机器执行至timer时是否到了可执行的时机。

做个对比就比较清楚了:

setImmediate(()=> {
  console.log(1);
  setImmediate(()=>{console.log(2);});
});

setImmediate(()=>{console.log(4);});
setTimeout(()=> {
  console.log('TIMEOUT FIRED');
}, 20);
// 1=>2=>TIMEOUT FIRED
复制代码

此时间隔时间较长,timer阶段最后才会执行,所以会先执行两次check,出处1,2 下面再看个例子 poll阶段任务队列

var fs = require('fs')

fs.readFile('./yarn.lock', () => {
    setImmediate(() => {
        console.log('1')
        setImmediate(() => {
            console.log('2')
        })
    })
    setTimeout(() => {
        console.log('TIMEOUT FIRED')
    }, 0)
    
})
// 结果确定:
// 输出始终为1=>TIMEOUT FIRED=>2
复制代码
复制代码
  1. 读取文件,回调进入poll阶段
  2. 当前无任务队列,直接check 输出1 将setImmediate2加入事件队列
  3. 接着timer阶段,输出TIMEOUT FIRED
  4. 再次check阶段,输出2 结论: 

process.nextTick(),效率最高,消费资源小,但会阻塞CPU的后续调用; 

setTimeout(),精确度不高,可能有延迟执行的情况发生,且因为动用了红黑树,所以消耗资源大;

setImmediate(),消耗的资源小,也不会造成阻塞,但效率也是最低的。

  • 题目:
console.log('start')
setTimeout(() => {
  console.log('timer1')
  Promise.resolve().then(function() {
    console.log('promise1')
  })
}, 0)
setTimeout(() => {
  console.log('timer2')
  Promise.resolve().then(function() {
    console.log('promise2')
  })
}, 0)
Promise.resolve().then(function() {
  console.log('promise3')
})
console.log('end')

如果node版本为v11.x, 其结果与浏览器一致。

start
end
promise3
timer1
promise1
timer2
promise2
复制代码

具体详情可以查看《又被node的eventloop坑了,这次是node的锅》。

如果v10版本上述结果存在两种情况:

  • 如果time2定时器已经在执行队列中了
start
end
promise3
timer1
timer2
promise1
promise2复制代码
  • 如果time2定时器没有在执行对列中,执行结果为
start
end
promise3
timer1
promise1
timer2
promise2复制代码
  • Process.nextTick()

    process.nextTick()虽然它是异步API的一部分,但未在图中显示。这是因为process.nextTick()从技术上讲,它不是事件循环的一部分。

    • process.nextTick()方法将 callback 添加到next tick队列。 一旦当前事件轮询队列的任务全部完成,在next tick队列中的所有callbacks会被依次调用。

    换种理解方式:

    • 当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

例子

let bar;

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

setImmediate(() => {
  console.log('setImmediate');
})
function someAsyncApiCall(callback) {
  process.nextTick(callback);
}

someAsyncApiCall(() => {
  console.log('bar', bar); // 1
});

bar = 1;复制代码

在NodeV10中上述代码执行可能有两种答案,一种为:

bar 1
setTimeout
setImmediate复制代码

另一种为:

bar 1
setImmediate
setTimeout复制代码

无论哪种,始终都是先执行process.nextTick(callback),打印bar 1

事件流和事件循环

document.body.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(1))
  console.log(2);
}, false);
document.body.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(3))
  console.log(4);
}, false);
document.body.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(1))
  console.log(2);
}, false);
document.body.addEventListener('click', () => {
  Promise.resolve().then(() => console.log(3))
  console.log(4);
}, true);

document.body.addEventListener('click', () => {
  setTimeout(() => {
        console.log(1);
      })
  console.log(2);
}, false);
document.body.addEventListener('click', () => {
  setTimeout(() => {
        console.log(3);
      })
  console.log(4);
}, true);