说明
今天学习了关于event loop的内容,所以整理成文。
最近复看这部分笔记,新整理了下内容。而且越了解越发现这部分内容很深,所以还需要花时间再更深入系统的学习研究下!
牢记
- JS 是单线程运行 (即同一个时间只能做一件事)
- JS中存在异步执行
- JS的Event Loop是JS的执行机制(深入了解JS的执行,就等于深入了解JS里的event loop)
- 微任务、宏任务
既然JS是单线程的,只能在一条线程上执行,又是如何实现的异步呢?————通过的事件循环(event loop)。
JS的单线程是指一个浏览器进程中只有一个JS的执行线程,同一时刻内只会有一段代码在执行。
什么是 JS Event Loop
JS Event Loop 即事件循环,是运行在浏览器环境 / Node 环境中的一种消息通信机制,它是主线程之外的独立线程。当主线程内需要执行某些可能导致线程阻塞的耗时操作时(比如请求发送与接收响应、文件 I/O、数据计算)主线程会注册一个回调函数并抛给 Event Loop 线程进行监听,自己则继续往下执行,一旦有消息返回并且主线程空闲的情况下,Event Loop 会及时通知主线程,执行对应的回调函数获取信息,以此达到非阻塞的目的。
什么是执行栈
- 作用:存储 在代码执行期间 创建的所有执行环境。当我们执行JS代码时,JS引擎会为其生成对应的执行环境,并将执行环境推入执行栈中。
- 栈的顺序:先进后出。(队列:先进先出)
- 【爆栈】:栈可存放的执行环境是有限制的!当我们使用递归的时候,一旦存放了过多执行环境且没有得到释放的话,就会出现爆栈的问题。(如死循环。所以递归时需要在达到条件时就结束递归哟)
更多相关请查看我的文章《JS:4.2 执行环境及作用域》
浏览器中的 Event Loop
1. 把任务源分为:【牢记】
- macro-task(宏任务):包括整体代码script,setTimeout,setInterval,ajax(xhr),UI rendering,setImmediate ,I/O 等中的回调函数。
- micro-task(微任务):Promise.then()中的回调函数,process.nextTick(Node 独有),MutationObserver
2. 执行顺序【牢记】
<script>
,执行同步代码(这属于宏任务)- 若执行栈为空,则查询是否有微任务,若有则执行(如
promise.then()
的回调函数) - 如有必要会渲染页面
- 【下一轮 Event Loop】:执行宏任务中的异步代码(
setTimeout
、setInterval
、ajax
(xhr) 、...等中的回调函数)。
new Promise(function1)).then(function2)
:new Promise 的内部注册的回调function1是立即执行的, function2 才是异步微任务。- 注意:全局执行环境始终是存在于执行栈的,除非关闭应用程序(如关闭网页或浏览器)。所以此处说的’执行栈为空‘,指的是除了全局执行环境外,执行栈中再没有其他执行环境了。【我的理解,还待深入确认下;疑问???】
- 介于浏览器对两种不同任务源队列中回调函数的读取机制,造成了上述中的执行顺序问题。
3. 浏览器 Event Loop 的执行机制【牢记!!!】
- 初始状态:执行栈为空,
micro-task
为空,macro-task
里有且只有一个script
脚本(整体源代码)。 script
脚本执行:JS引擎拿到JS源代码后,会进行一系列的编译解析,最后生成可执行的代码。JS引擎生成一个全局执行环境(script
任务的),里面包含变量对象(存储当前执行环境里的所有变量和函数)、作用域等。这个执行环境被生成以后,就会被推入js的执行栈,代码以同步的方式依次执行(一行行执行)。在执行的过程中,可能会产生新的macro-task
与micro-task
,它们会在特定的时机时分别被推入各自的任务队列里并等待被执行。- 比如
promise.then(callback)
里的回调函数是先被挂起,然后在promise状态变为已完成fullfilled后,才会被推入微任务队列里并等待被执行; - 比如
setTimeout(callback,time)
里的回调函数是先被挂起,等到第 time 毫秒后才被推入宏任务队列里并等待下一轮循环时被执行。
- 比如
- script 脚本出队:同步代码执行完了,
script
脚本会被移出macro-task
(这个过程本质上是宏任务队列的执行和出队的过程)。【疑问?全局执行环境也要在此时出栈么??】 微任务队列执行:上一步已经将
script
宏任务执行并出队了,这时候执行栈为空,Event Loop 会去micro-task
中将微任务推入主线程执行。注意:这里的微任务的执行方式和宏任务的执行方式有个很重要的区别,就是:宏任务是一个一个执行,而微任务是一队一队执行的。也就是说,执行一个宏任务,要执行一队的微任务。(注意:在执行微任务的过程中,仍有可能有新的微任务插入
micro-task
, 那么这种情况下,Event Loop 仍然需要将本次 Tick (循环) 下的微任务拿到主线程中执行完毕)。【多理解】[练习:下方‘非常典型的示例’eg1]- 浏览器执行渲染操作,更新界面。
- 检查是否存在 Web worker 任务,如果有,则对其进行处理。
- 上述过程循环往复,直到两个队列都清空。
4. 回调函数是什么时候被推入宏任务或微任务队列的?
JS的异步是通过回调函数实现的。异步任务必须指定回调函数,当主线程开始执行异步任务,就是执行对应的回调函数。
为了巩固和加强理解,请练习本文‘典例’中的eg1、eg2、eg3。
4.1 setTimeout(callback, milliseconds)
的回调函数
setTimeout(function(){
console.log('执行了');
},3000);
我们一般说: "3秒后,会执行setTimeout里的那个函数"。但是这种说并不严谨,准确的解释是:
setTimeout 的回调函数先被挂起,3 秒后,setTimeout 里的回调函数才会被推入宏任务的 event queue(事件队列。并且等待下一轮的执行;并不是立即执行);而event queue 里的任务,只有在执行栈空闲(上一轮宏任务、微任务都执行完)时才会执行。所以第二个参数仅仅表示最少延迟时间,而非确切的等待被执行时间。
所以只有下面两个条件同时满足时,才会3秒后就执行该函数:
- 3秒后
- 执行栈空闲
但如果执行栈中执行内容很多,执行时间超过了3秒,比如执行了10秒,那么这个函数只能10秒后执行了。
MDN:《window.setTimeout》
4.2 promise.then(successCallback, failureCallback)
的回调函数
- MDN:Promise 时序:
为了避免意外,即使是一个已经变成 resolve 状态的 Promise,传递给 then() 的函数也总是会被异步调用(被推入了微任务队列中)。resolve()
:将Promise对象的状态从「进行中」变成「已成功」(即从pending变为fulfilled)。- 【重点】只有在上一个操作执行完成之后(即 promise 的状态变为 fullfilled 了),才会开始下一个的操作,即 promise 才会调用 then 函数;此时 then 里面的回调函数才会被推入微任务队列中,并等待被执行。
例子:
const wait = ms => new Promise(resolve => setTimeout(()=>{
resolve();
console.log(6);
}, ms));
// a
wait(1000).then(() => console.log(5)); // 这个then的回调函数并不会在wait(1000)执行后就被推入微任务队列!而是先被挂起,等到wait(1000)的promise状态变成fullfilled后,才会被推入微任务队列并等待被执行。
// b
wait(0).then(() => console.log(4));
// c
Promise.resolve().then(() => console.log(2)).then(() => console.log(3));
console.log(1);
// 输出结果:1, 2, 3, 6, 4, 6, 5
我的理解:
- a处:首先 wait(1000) 函数被推入执行栈中,JS引擎执行里面Promise中的代码。遇到了setTimeout,则setTimeout的回调函数将被挂起,并在1秒后才被推入宏任务的事件队列中并且等待下一次执行。而 5 注册的回调函数被挂起,等待 wait(1000) 的 promise 执行完成。
- b处:wait(0) 函数被推入执行栈中,...,setTimeout的回调函数将被挂起,并在0秒后被推入宏任务的事件队列中并且等待下一次执行。而 4 注册的回调函数被挂起,等待 wait(0) 的 promise 执行完成。
- c处:是一个已经变成 resolve 状态的 Promise了,2 注册的回调函数被挂起后又被推入了微任务的事件队列中,等待被调用执行。
- 输出 1。执行栈为空了,则检查微任务队列中是否有内容。所以输出2。此时
Promise.resolve().then(() => console.log(2))
的操作已经完成,状态已经为fullfilled,于是 3 注册的回调函数被挂起后又被推入微任务队列中,被执行,所以输出3。此时微任务队列也已经被执行完了。完成了第一轮循环。- 【第二轮循环开始】从宏任务队列中抓取出一个任务:wait(0)对应的setTimeout的回调函数被执行,所以输出6,wait(0) 的 promise 状态变为 fullfilled,于是 4 注册的回调被推入微任务队列中。执行栈又为空了,执行微任务中的内容,所以输出 4。
- 【第三轮循环开始】从宏任务队列中抓取出一个任务:wait(1000)对应的setTimeout的回调函数被执行,所以输出6,wait(1000) 的 promise 状态变为 fullfilled,于是 5 注册的回调被推入微任务队列中。执行栈又为空了,执行微任务中的内容,所以输出 5。
Promise 的多个链式 then() 是怎样执行的【必须理解】
典例:
new Promise((resolve) => {
resolve();
})
.then(() => console.log(1))
.then(() => console.log(2))
.then(() => console.log(3))
new Promise((resolve) => {
resolve();
})
.then(() => console.log(4))
.then(() => console.log(5))
.then(() => console.log(6))
// 执行结果:1 4 2 5 3 6
- Promise 多个 then() 链式调用,并不是连续的创建了多个微任务并推入微任务队列,因为 then() 的返回值必然是一个 Promise,而后续的 then() 是上一步 then() 返回的 Promise 的回调;
- 传入 Promise 构造器的执行器函数内部的同步代码执行到 resolve(),将 Promise 的状态改变为
<resolved>: undefined
, 然后 then 中传入的回调函数 console.log('1') 作为一个微任务被推入微任务队列; - 第二个 then() 中传入的回调函数 console.log('2') 此时还没有被推入微任务队列,只有上一个 then() 中的 console.log('1') 执行完毕后,console.log('2') 才会被推入微任务队列。
- 继续执行本轮的宏任务代码,执行第二个Promise里的操作,执行到 resolve(),将 Promise 的状态改变为
<resolved>: undefined
, 然后 then 中传入的回调函数 console.log('4') 作为一个微任务被推入微任务队列; - 本轮的宏任务已经执行完毕了,开始执行微任务。先执行微任务队列里的第一个微任务: console.log(1) 所在的回调函数,执行完成后,返回一个状态为已完成的promise,则console.log(2)所在的回调函数作为一个微任务被推入微任务队列(在console.log('4') 所在回调函数的后面);开始执行微任务队列里的第二个微任务...
摘抄整理自《令人费解的 async/await 执行顺序》
5. 浏览器渲染时机
在上面浏览器 Event Loop 的执行机制中,有很重要的一块内容,就是浏览器的渲染时机,浏览器会等到当前的 micro-task
为空的时候,进行一次重新渲染。所以如果你需要在异步的操作后重新渲染 DOM
最好的方法是:将它包装成 micro
任务,这样 DOM
渲染将会在本次 Tick
(循环) 内就完成。
6. 宏任务与微任务,哪个先执行?
这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script
,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。
即:Event Loop 抓取回调的逻辑是先执行宏任务,再执行微任务,再执行宏任务。。。以此循环。
本质上来说,当前执行栈里的代码都属于宏任务,于是等待执行栈清空,宏任务执行完成,浏览器去 microtask
里抓取微任务来执行,除非 microtask
里没有,才会去 macrotask
抓取任务执行。
7. 其他理解分析
导图1:
分析:当我们执行 JS 代码的时候其实就是往执行栈中放入函数的执行环境,那么遇到异步代码的时候该怎么办?其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
导图2:
导图3:
详细解释:1.执行栈【具体见下面的 "执行环境+执行栈+作用域链+函数中局部变量的正常生命周期" 内容】
在 js 代码执行过程中,会生成对应的执行环境。每个执行环境都有一个变量对象,用于存放当前执行环境中的变量和函数。这个执行环境被生成以后,就会被推入 JS 的执行栈。一旦执行完成,那么这个执行环境就会被执行栈弹出并销毁,执行栈的指针下移,执行权交回给原来的执行环境。那个执行环境里相关的变量会脱离执行环境,在下一轮垃圾收集
到来的时候,这些变量占据的内存就能得以释放。【这是回顾之前学习滴哟~】这个执行栈,也可以理解为JavaScript的单一线程,所有代码都跑在这个里面,以同步的方式依次执行,或者阻塞,这就是同步场景。
2.异步【必须理解】
那么异步场景呢?显然就需要一个独立于“执行栈”之外的容器,专门管理这些异步的状态,于是在“主线程”、“执行栈”之外,有了一个Task
的队列结构,专门用于管理异步逻辑。
所有异步操作的回调,都会暂时被塞入这些队列。
Event Loop 处在两者之间,扮演一个大管家的角色,它会以一个固定的时间间隔不断轮询,当它发现主线程空闲,就会去到Task
队列里拿一个异步回调,把它塞入执行栈中执行;一段时间后,主线程执行完成,弹出这个异步回调的执行环境,再次空闲,Event Loop 又会执行同样的操作...依次循环,于是构成了一套完整的事件循环运行机制。上图是笔者在 Google 上找的,比较简洁地描绘了整个过程,只不过其中多了 heap (堆)的概念,堆和栈,简单来说,堆是留给开发者分配的内存空间,而栈是原生编译器要使用的内存空间,二者独立。
执行环境+执行栈+作用域链+函数中局部变量的正常生命周期【必须理解】
具体查看我的文章《JS:4.2 执行环境及作用域》中的”执行环境+执行栈+作用域链+函数中局部变量的正常生命周期【必须理解】“
关于 async 和 await
查看我的文章《async 和 await:让异步编程更简单》
典例
eg1、eg2、eg3 都要熟悉哟!
eg1(我自己写的):
setTimeout+Promise+ then链式调用
setTimeout(function t1() {
console.log("t1 定时器开始啦");
new Promise(function p3(resolve) {
console.log("p3");
return resolve();
}).then(function p3then() {
console.log("p3 then");
});
}, 0);
new Promise(function p1(resolve) {
console.log("p1 马上执行for循环啦");
for (var i = 0; i < 10000; i++) {
i === 99 && resolve();
}
}).then(function p1then() {
console.log("p1 then");
new Promise(function p2(resolve) {
console.log("p2");
return resolve();
}).then(function p2then() {
console.log("p2 then");
Promise.resolve().then(function p4then() {
console.log("P4 then");
setTimeout(function t3() {
console.log("t3");
}, 100);
});
});
setTimeout(function t2() {
console.log("t2 执行then函数里的timeout函数");
}, 0);
});
console.log("over 代码执行结束");
// 执行结果:
// p1 马上执行for循环啦,over 代码执行结束,p1 then,p2,p2 then,P4 then,t1 定时器开始啦, p3,p3 then,t2 执行then函数里的timeout函数,t3
我的分析:
【深入理解】这里的微任务的执行方式和宏任务的执行方式有个很重要的区别,就是:宏任务是一个一个执行,而微任务是一队一队执行的。也就是说,执行一个宏任务,要执行一队的微任务。(注意:在执行微任务的过程中,仍有可能有新的微任务插入 micro-task
, 那么这种情况下,Event Loop 仍然需要将本次 Tick (循环) 下的微任务拿到主线程中执行完毕。—— 具体可查看图中的第5、6步)。
eg2:【分析的特别好】
async/await + setTimeout + Promise
来源此文
console.log('script start');
async function async1() {
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
// 上面的 await 可能不太好理解,我换成了另一种写法
function async1() {
async2().then(res => {
console.log('async1 end')
})
}
function async2() {
console.log('async2 end')
return Promise.resolve(undefined);
}
// 或等同于
function async2(){
return new Promise((resolve, reject) => {// 返回一个 Promise 对象
console.log('async2 end')
resolve();
})
}
async1();
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
}).then(function() {
console.log('promise1') // 这个回调函数中并没有包含其他异步操作,所以执行完这一行后这个回调函数的promise状态就变为了fullfilled啦,则将其后then()中的回调函数又推入微任务队列中
}).then(function() {
console.log('promise2')
})
console.log('script end')
// log 打印顺序:script start -> async2 end -> Promise -> script end -> async1 end -> promise1 -> promise2 -> setTimeout
详细分析:
- script 脚本开头碰到了 console.log 于是打印 script start
- 解析至 async1() ,async1 执行环境被推入执行栈,解析引擎进入 async1 内部
- 发现 async1 内部调用了 async2,于是继续进入 async2,并将 async2 执行环境推入执行栈
- 碰到 console.log,于是打印 async2 end
- async2 函数执行完成,弹出执行栈,并返回了一个 Promise.resolve(undefined),此时,由于 Promise 已经变成 resolve 状态,于是async1 then 注册的回调被推入 microtask
- 解析至 setTimeout,等待 0ms 后将其回调推入 macrotask
- 继续执行,直到碰到了 Promise,new Promise 的内部注册的回调是立即执行的,解析进入注入函数的内部,碰到 console.log,于是打印 'Promise',再往下,碰到了 resolve,将第一个 then 中的回调函数推入 micro-task ,然后碰到了第二个 then ,继续将其中的回调函数推入 micro-task。
- 执行到最后一段代码,打印 script end
- 自此,第一轮 Tick 中的一个宏任务执行完成,开始执行微任务队列,通过前面的分析可以得知,目前 micro-task 中有三个任务,依次为:console.log('async 1')、console.log('promise1')、console.log('promise2')于是 Event Loop 会将这三个回调依次取到主线程执行,控制台打印:async1、promise1、promise2
- 自此,micro-task 为空,浏览器开始重新渲染(如果有 DOM 操作的话)
- Event Loop 再次启动一个新的 Tick ,从宏任务队列中拿出一个(唯一的一个)宏任务执行,打印出:setTimeout
提问:为什么连续打印promise1和promise2?
答:这个有一个重要的原因:
- 浏览器中的 Event Loop 机制是先执行宏任务,再执行微任务的。
- 当浏览器执行完一个宏任务,会去执行微任务队列中的所有微任务,然后再执行宏任务队列中的下一个宏任务,再去执行微任务队列中的所有微任务...以此循环。
这就能够解释这个问题了:因为 then 返回的是一个新的 Promise,如果链式调用的话(且回调中无其他宏任务回调函数),就会不断往微任务队列中推入新的微任务,浏览器必须要清空微任务队列中的所有任务,才会去执行下一个宏任务。
eg3 【then 链式调用】
async/await + setTimeout + Promise
我根据 eg2 改编的,主要是为了更加深入理解 promise.then() 的回调函数何时被推入微任务队列的。
console.log('script start');
async function async1() {
await async2();
await async2();
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2 end');
}
提醒【很重要】:下面第一个async2()后的四个then的回调,并不是在async2()正常执行完成后就一次性推入微任务队列的,此时只有第二个async2()所在的回调函数被推入微任务队列!所以注意:promise.then()的回调函数,必须等到前一个 promise 的状态变为 fullfilled 后,才会被推入(push进)微任务队列。
// 上面的 await 可能不太好理解,我换成了另一种写法
function async1() {
return async2()
.then(()=>async2()) // 当第一个async2()状态变为fullfilled后,这个then()中的回调函数才会被push进微任务队列中。
.then(()=>async2())
.then(()=>async2())
.then(()=>console.log('async1 end'));// 当其前的这个 promise 变为fullfilled后,这个then()中的回调函数才会被push进微任务队列中
}
function async2() {
console.log('async2 end')
return Promise.resolve(undefined);
}
async1().then(()=>{ // then中的回调函数先被挂起,只有 async1 内部的异步操作都执行完成后(状态变成 fullfilled),这个then()中的回调函数才会被push进微任务队列中
console.log('async1 then');
});
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')
// log 打印顺序:script start -> async2 end -> Promise -> script end -> async2 end -> promise1 -> async2 end -> promise2 -> async1 end -> async1 then -> setTimeout
关键需要理解:本文“4.2 promise.then(successCallback, failureCallback) 的回调函数” 在何时被推进微任务队列的内容。
eg4 面试题
const tasks = [];
const output = i => new Promise((resolve) => { setTimeout(() => { console.log(i); resolve(); }), 1000 * i; });
for (var i = 0; i < 5; i++) { tasks.push(output(i)); }
Promise.all(tasks).then(() => { setTimeout(() => { console.log(i); }), 1000; });
0 1 2 3 4 5
复杂点的题
const first = () =>
new Promise((resolve, reject) => {
console.log(3);
let p = new Promise((resolve, reject) => {
console.log(7);
setTimeout(() => {
// 宏任务
console.log(5);
resolve(6);
}, 0);
resolve(1); // p的状态结果(Promise的状态一旦改变就不会在改变了)
});
resolve(2);
// 微任务
p.then(arg => {
console.log(arg); // 1
});
});
// 微任务
first().then(arg => {
console.log(arg); // 2
});
console.log(4);
结果:3 7 4 1 2 5
分析:
宏任务:5
执行栈:3 7 4
微任务:1 2
谈谈setTimeout(function, milliseconds)
【需整理修改下】
注意区别下面两个例子:
【待研究】研究下是否是因为最小延迟时间导致的,MDN文档
我自己测试写的。发现 setTimeout被推入队列的时间不同,可能会影响最终指向结果。
setTimeout(function() {
console.log("定时器开始啦");// 宏任务1(2ms时被推入event table)
},2);
new Promise(function(resolve) {
resolve()
}).then(function() {
setTimeout(function() {
console.log("执行then函数里的timeout函数"); //宏任务2 (1ms后被推入event table)。执行此行代码时,可能总时长还<2ms,所以宏任务2先于宏任务1执行
},1);
});
// 执行then函数里的timeout函数
// 定时器开始啦
setTimeout(function() {
console.log("定时器开始啦");// 宏任务(1.1ms时被推入event table)
},1.1);
new Promise(function(resolve) {
resolve()
}).then(function() {
setTimeout(function() {
console.log("执行then函数里的timeout函数"); // 宏任务(1ms后被推入event table)执行此行代码时,可能总时长已经>1.1ms了,所以宏任务1先于宏任务2执行
},1);
});
// 定时器开始啦
// 执行then函数里的timeout函数
待研究整理
原文链接
console.log('本轮任务');
new Promise((resolve, reject) => {
resolve(3)
}).then(() => {
console.log('本轮微任务');
})
document.querySelector('div').addEventListener('click', () => { console.log('click'); })
document.querySelector('div').dispatchEvent(new Event('click'))
console.log('hello')
// 本轮任务 click hello 本轮微任务
所以题目里面的console.log('click') 这个回调应该是不会被加入到 macrotask queue 里面的, 是直接同步执行。【待确认】
另外在html规范中好像给出了一个关于浏览器event loop机制的伪代码,个人感觉把这几行代码理清楚了就能解决关于微任务以及task九成的问题。
eventLoop = {
taskQueues: {
events: [], // UI events from native GUI framework
parser: [], // HTML parser
callbacks: [], // setTimeout, requestIdleTask
resources: [], // image loading
domManipulation: []
},
microtaskQueue: [],
nextTask: function() {
// Spec says:
// "Select the oldest task on one of the event loop's task queues"
// Which gives browser implementers lots of freedom
// Queues can have different priorities, etc.
for (let q of taskQueues)
if (q.length > 0) return q.shift();
return null;
},
executeMicrotasks: function() {
if (scriptExecuting) return;
let microtasks = this.microtaskQueue;
this.microtaskQueue = [];
for (let t of microtasks) t.execute();
},
needsRendering: function() {
return vSyncTime() && (needsDomRerender() || hasEventLoopEventsToDispatch());
},
render: function() {
dispatchPendingUIEvents();
resizeSteps();
scrollSteps();
mediaQuerySteps();
cssAnimationSteps();
fullscreenRenderingSteps();
animationFrameCallbackSteps();
intersectionObserverSteps();
while (resizeObserverSteps()) {
updateStyle();
updateLayout();
}
paint();
}
}
while(true) {
task = eventLoop.nextTask();
if (task) task.execute();
eventLoop.executeMicrotasks();
if (eventLoop.needsRendering()) eventLoop.render();
}
其他
我的文章《JS:4.2 执行环境及作用域》
强烈推荐《10分钟理解JS引擎的执行机制》-初步理解
强烈推荐《[基础] 浅谈 JS Event Loop》-深入理解原理
参考文章:《Event Loop》
还需要深入理解下这篇文章里的内容:《事件循环机制-执行栈、调用栈》