重学前端|重学前端——事件循环

事件循环 js是单线程的 为什么要有事件循环机制哩?假设js只是单线程的 碰到同步任务还好,按照顺序依次执行,如果异步任务那种耗时很长的任务,比如发请求,那么这个js任务就卡住了 只能等到请求返回成功才能执行,这样效率就太低了。事件循环机制是为了解决这样的问题,让同步任务能跳过异步任务 并发的执行,等到异步任务完成后回调放入任务队列等待主线程调用执行。

  • 同步任务:在执行栈中按照先进后出的顺序执行,一个执行完了 再轮到下一个执行
  • 异步任务:不阻碍主线程的任务执行
  • 栈 先进后出,可以想像成一头被堵住的管道,只能从一头放入拿出,那先放入的压在下面,只能等上面的拿出后才能拿出。 函数调用时形成调用栈 当调用一个函数时 被压入栈中,函数执行完从栈中移除,执行权交给下一个函数。
  • 堆 对象被分配在堆中,堆是一个大块内存存储区域
  • 【重学前端|重学前端——事件循环】队列 先进先出,可以想象成两头通畅的水管,一头流入那一头流出。 js 运行时包含一个待处理消息的消息队列,每个消息都关联一个回调
    Js 任务类型:
    • 宏任务:script代码 setTimeout setInterval setImediate I/O UIRendering
    • 微任务: Process.nextTick Promise Mutation.Observer
    js代码在执行时 都有一个执行栈,按照先进后出的顺序执行,同步任务执行完从执行栈中弹出,异步任务会把回调放在任务队列中,在异步完成后,判断执行栈是否为空,如果为空会把任务队列中的任务放入执行栈,开启一次事件循环。任务队列中的任务分为两种,微任务和宏任务,先宏后微,第一次整体js代码作为一个宏任务,遇到宏任务就放任务队列,遇到微任务放任务队列,当执行栈空了就从任务队列拿微任务进行处理,执行完所有的微任务。当所有微任务完成,就开启下一轮事件循环,执行宏任务,宏任务执行完执行微任务。
    下面例子,执行过程:
    console.log('console start'); setTimeout(function() { console.log('eventLoop2-宏任务1:setTimeout1'); new Promise(function(resolve) { console.log('promise2'); resolve() }).then(function() { console.log('eventLoop2-微任务1:then2'); }).then(function() { console.log('eventLoop2-微任务2:then22'); }) }) setTimeout(function() { console.log('eventLoop3-宏任务1:setTimeout2'); })new Promise(function(resolve) { console.log('promise'); resolve() }).then(function() { console.log('eventLoop1-微任务1:then'); }).then(function() { console.log('eventLoop1-微任务2:then1'); })console.log('console end'); // console start // promise // console end // eventLoop1-微任务1:then // eventLoop1-微任务2:then1 // eventLoop2-宏任务1:setTimeout1 // promise2 // eventLoop2-微任务1:then2 // eventLoop2-微任务2:then22 // eventLoop3-宏任务1:setTimeout2

    1. 同步代码放入执行栈 执行后弹出 输出 console start。整体代码作为宏任务放入宏任务队列
    2. 接着setTimeout放入执行栈 执行后弹出,setTime callback (eventLoop2-宏任务1) 放入宏任务任务队列
    3. 接着setTimeout放入执行栈 执行后弹出,setTime callback(eventLoop3-宏任务1)放入宏任务任务队列
    4. new Promise 放入执行栈执行后弹出,输出 promisepromise.then callback(eventLoop1-微任务1)放入微任务队列 promise.then.then callback(eventLoop1-微任务2)放入微任务队列
    5. 执行同步代码 输出console end 此时执行栈为空
    6. 开启第一次事件循环,从宏任务队列对头那任务,执行整体宏任务,询问微任务是否为空
    7. 微任务队列不为空执行微任务队列,promise.then callback(eventLoop1-微任务1)放入执行栈执行完退出,输出 eventLoop1-微任务1:then
    8. 继续询问微任务队列是否为空 不为空 promise.then.then callback(eventLoop1-微任务2)放入执行栈 执行完退出 输出 eventLoop1-微任务2:then
    9. 继续询问微任务队列是否为空 为空。开启下一轮事件循环
    10. 拿宏任务执行,把setTime callback (eventLoop2-宏任务1)放入执行栈 执行完退出,打印eventLoop2-宏任务1:setTimeout1,继续执行new Promise 输出primise2,把 promise2的回调eventLoop2-微任务1:then2eventLoop2-微任务2:then22放入微任务队列
    11. 宏任务执行完 继续询问微任务 ,执行微任务 打印 eventLoop2-微任务1:then2eventLoop2-微任务2:then22
    12. 微任务队列为空 开启第三轮事件循环 执行setTime callback(eventLoop3-宏任务1) 打印eventLoop3-宏任务1:setTimeout2
    setTimeout(()=> {},0) 不是立即执行 而是等到执行栈为空再执行
    setTimeout(()=> {},1000) 不是1秒后执行 1秒只是最快执行时间
? 事件循环和浏览器渲染
并不是每一次事件循环都伴随着一次浏览器渲染,由屏幕刷新频率 页面性能 等共同决定。浏览器会尽可能保证帧率的稳定,如果无法维持60fps会降低到30fps 。
  • requestAnimationFrame 在重新渲染前调用
  • setTimeout 浏览器会把几次任务合并
    setTimeout(() => { console.log("sto") requestAnimationFrame(() => console.log("rAF")) }) setTimeout(() => { console.log("sto") requestAnimationFrame(() => console.log("rAF")) })queueMicrotask(() => console.log("mic")) queueMicrotask(() => console.log("mic"))

  • requestIdleCallback 空闲调度算法 在屏幕渲染后执行,把一些计算量较大但没那么紧急的任务放到空闲时间去执行 不要影响浏览器中优先级高的任务。

    推荐阅读