JS 的事件循环 event loop

104 阅读4分钟

JS 的事件循环 event loop

事件循环⼜叫做消息循环,是浏览器渲染主线程的⼯作⽅式。 在 Chrome 的源码中,它开启⼀个不会结束的 for 循环,每次循环从消息 队列中取出第⼀个任务执⾏,⽽其他线程只需要在合适的时候将任务加⼊到 队列末尾即可。 过去把消息队列简单分为宏队列和微队列,这种说法⽬前已⽆法满⾜复杂的 浏览器环境,取⽽代之的是⼀种更加灵活多变的处理⽅式。 根据 W3C 官⽅的解释,每个任务有不同的类型,同类型的任务必须在同⼀ 个队列,不同的任务可以属于不同的队列。不同任务队列有不同的优先级, 在⼀次事件循环中,由浏览器⾃⾏决定取哪⼀个队列的任务。但浏览器必须 有⼀个微队列,微队列的任务⼀定具有最⾼的优先级,必须优先调度执⾏。

———————————————— 版权声明:本文为CSDN博主「JackieDYH」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 原文链接:blog.csdn.net/JackieDYH/a…

单线程即任务是串行的,后一个任务需要等待前一个任务的执行,这就可能出现长时间的等待。但由于类似ajax网络请求、setTimeout时间延迟、DOM事件的用户交互等,这些任务并不消耗 CPU,是一种空等,资源浪费,因此出现了异步。通过将任务交给相应的异步模块去处理,主线程的效率大大提升,可以并行的去处理其他的操作。当异步处理完成,主线程空闲时,主线程读取相应的callback,进行后续的操作,最大程度的利用CPU。此时出现了同步执行和异步执行的概念,同步执行是主线程按照顺序,串行执行任务;异步执行就是cpu跳过等待,先处理后续的任务(CPU与网络模块、timer等并行进行任务)。由此产生了任务队列与事件循环,来协调主线程与异步模块之间的工作。

首先把JS执行代码操作 分为主线程任务队列,任何一段js代码的执行都可以分为以下几个步骤:

  • 步骤一: 主线程读取JS代码,此时为同步环境,形成相应的堆和执行栈;
  • 步骤二: 当主线程遇到异步操作的时候,将异步操作交给对应的API进行处理;
  • 步骤三: 当异步操作处理完成,推入任务队列中
  • 步骤四: 主线程执行完毕后,查询任务队列,取出一个任务,并推入主线程进行处理
  • 步骤五: 重复步骤二、三、四

其中常见的异步操作有:ajax请求,setTimeout,还有类似onclik事件等

任务队列

同步和异步任务分别进入不同的执行环境,同步的进入主线程,即主执行栈,异步的进入任务队列

任务队列 的类型:

任务队列分为 宏任务(macrotask queue) 和 微任务(microtask queue)

宏任务主要包含:script( 整体代码)、setTimeout、setInterval、I/O、UI 交互事件、setImmediate(Node.js 环境)

微任务主要包含:Promise、MutationObserver、process.nextTick(Node.js 环境)

console.log('1, time = ' + new Date().toString()) // 1.进入主线程,执行同步任务,输出1
setTimeout(macroCallback, 0)// 2. 加入宏任务队列 // 7.开始执行此定时器宏任务,调用macroCallback,输出4
new Promise(function (resolve, reject) {//3.加入微任务队列
console.log('2, time = ' + new Date().toString())//4.执行此微任务中的同步代码,输出2
resolve()
console.log('3, time = ' + new Date().toString())//5.输出3
}).then(microCallback)// 6.执行then微任务,调用microCallback,输出5
//函数定义
function macroCallback() {
  console.log('4, time = ' + new Date().toString())
}

function microCallback() {
  console.log('5, time = ' + new Date().toString())
}

image.png

再来分析一个简单的例子:

console.log('0');
setTimeout(() => {
    console.log('1');
    new Promise(function(resolve) {
        console.log('2');
        resolve();
    }).then(()=>{
        console.log('3');
    })
    new Promise(resolve => {
        console.log('4');
        for(let i=0;i<9;i++){
            i == 7 && resolve();
        }
        console.log('5');
    }).then(() => {
        console.log('6');
    })
})

  • 进入主线程,检测到log为普通函数,压入执行栈,输出0;
  • 检测到setTimeOut是特殊的异步方法,交给其他模块处理,其回调函数加入 宏任务(macrotask)队列;
  • 此时主线程中已经没有任务,开始从任务队列中取;
  • 发现微任务队列为空,则取出宏任务队列首项,也就是刚才的定时器的回调函数;
  • 执行其中的同步任务,输出1;
  • 检测到promise及其resolve方法是一般的方法,压入执行栈,输出2,状态改变为resolve;
  • 检测到这个promise的then方法是异步方法,将其回调函数加入 微任务队列;
  • 紧接着又检测到一个promise,执行其中的同步任务,输出4,5,状态改变为resolve;
  • 然后将它的then异步方法加入微任务队列;
  • 执行微任务队列首项,也就是第一个promise的then,输出3;
  • 再取出微任务队列首项,也就是第二个promise的then,输出6;
  • 此时主线程和任务队列都为空,执行完毕;