扒下JS的“底裤”之任务管理 :事件循环

201 阅读3分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情

任务管理

JavaScript 语言的一大特点就是单线程,也就是说同一个时间只能处理一个任务。为了协调事件、用户交互、脚本、UI 渲染和网络处理等行为,防止主线程的不阻塞,(事件循环)Event Loop 的方案应用而生。

JavaScript 处理任务是在等待任务、执行任务 、休眠等待新任务中不断循环中,也称这种机制为事件循环。

  • 主线程中的任务执行完后,才执行任务队列中的任务
  • 有新的任务到来时会将其放入队列,采取先进先出的执行策略执行队列中的任务
  • 比如多个setTimeout同时到时间了,就要依次执行

任务包括 script(整体代码)、 setTimeout、setInterval、DOM 渲染、DOM 事件、Promise、XMLHTTPREQUEST 等

原理分析

宏任务与微任务

console.log(1) 
setTimeout(function(){
    console.log(5);
},0) 
Promise.resolve().then(function(){
    console.log(3);
}).then(function(){
    console.log(4);
})

console.log(2);

// 1 2 3 4 5

执行过程

  1. 先执行console.log(1) 输出1
  2. 执行setTimeout宏任务 将function(){console.log(5);},0 放入宏任务队列。
  3. 执行Promise微任务将 console.log(3);放入微任务队列
  4. 执行同步代码console.log(2);输出2
  5. 主线程任务完成,微任务开始传入主进程
  6. 通过事件循环遍历微任务队列,将刚才放入的 Promise.then 微任务读取到主线程执行,然后输出 3
  7. 之后又执行 promse.then 产生新的微任务,并放入微任务队列
  8. 主线程任务执行完毕
  9. 再次事件循环遍历微任务队列,读取到 promise2 微任务放入主线程执行,然后输出4
  10. 主线程任务执行完毕
  11. 此时微任务队列已经无任务,然后从宏任务队列中读取到 setTimeout 任务并加入主线程,然后输出 5

脚本加载

引擎在执行任务时不会进行 DOM 渲染,所以如果把script 定义在前面,要先执行完任务后再渲染 DOM,建议将script 放在 BODY 结束标签前。

定时器

定时器会放入异步任务队列,也需要等待同步任务执行完成后执行。注意是宏任务消息队列

HTML 标准规定最小时间不能低于 4 毫秒,有些异步操作如 DOM 操作最低是 16 毫秒,总之把时间设置大些对性能更好。

定时器会在等待时间完成后将任务放入宏任务队列,只有当主进程的任务执行完毕之后才会将这个任务放入主进程执行。

setTimeout(()=>{
    console.log(2);
},1000)

function fn() {
    let i = 0;
    console.log(1);
    while(i < 99999999) {
        i++;
    }
}

fn(); 
// 1 2

这里主线程先执行setTimeout,等待1s后将console.log(2);放入宏任务队列,等待期间同时执行同步代码 fn函数,输出1 。在等待1s后将console.log(2);放入宏任务队列,这时主进程的同步代码还未完成,宏任console.log(2);继续待在宏任务队列。等主进程同步代码执行完成之后,将console.log(2);从宏任务队列中弹出到主进程执行。输出2。

这是对定时器的说明,其他的异步操作如事件、XMLHTTPREQUEST 等逻辑是一样的

微任务

微任务一般由用户代码产生,微任务较宏任务执行优先级更高,Promise.then 是典型的微任务,实例化Promise 时执行器代码是同步的,然而实例函数 then 注册的回调函数是异步微任务的。

任务的执行顺序是同步任务、微任务、宏任务所以下面执行结果是 1、2、3、4

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

new Promise((resolve) => {
    console.log(1)
     resolve()
}).then((_) => console.log(3))

console.log(2);

// 1 2 3 4

执行过程

  1. 执行脚本
  2. 执行setTimeout异步函数,将 console.log(4);放入宏任务队列
  3. 执行Promise函数,执行执行器函数代码(是同步的)直接输出1
  4. 执行Promise.then将console.log(3))放入微任务
  5. 执行同步代码console.log(2);输出2
  6. 主进程任务完成
  7. 将微任务console.log(3))放入主进程执行输出3
  8. 主进程任务完成
  9. 将宏任务 console.log(4);放入主进程执行输出4