写在前面
之前做一个十万数据图表 (目前荣登三十万) 时遇到一个问题,怎么样能让 Echarts 不那么卡顿。真的就是画着画着内存就溢出了,试了无数种方法。其中有一种就是使用定时器,每十秒增加2000条数据,然后开始绘制,发现还是卡。这就是垫话了,顺便给大家出道题,setTimeout中递归自己,浏览器会出现什么样的效果?Promise的then中递归自己,浏览器又会出现什么样的效果?
基础概念
简单版: 运行时对 JavaScript 脚本的调度方式就叫做事件循环。
详细版: 主线程事件执行完毕后从任务队列中读取异步事件,并将其放入调用栈中执行,这个过程是循环不断的,所以整个的这种运行机制被称为 Event Loop(事件循环)。
一句话描述该技术的用途
简单版: 提供了异步编程的可能性。
绕口版: 改善因 JavaScript 单线程的特性而导致的没必要的程序阻塞问题 ( 事件协调 / 用户交互 / 脚本 / 渲染 / 网络 ... )。
- 事件协调: 定时器 Promise ...
- 用户交互: click onScroll ...
- 脚本: JavaScript 脚本执行
- 渲染: 解析 DOM / CSS ...
- 网络: HTTP 请求
核心概念或运作流程
浏览器是多线程的,譬如:
- 定时器触发线程
- HTTP异步线程
- EventLoop 处理线程
事件循环机制
- 事件循环机制从宏任务开始
- 第一个执行的宏任务是一个匿名函数,该函数内部是当前 JavaScript 整体代码
- 将任务放置于主线程上按从上到下的顺序依次进入调用栈执行
- 2.1 同步任务压栈直接执行,然后弹栈
- 2.2 异步任务压栈,将任务的函数体添加至任务队列,然后弹栈
- 当主线程上的任务都执行完毕,按照先进先出的原则读取任务队列中可执行的任务,将出队列的任务放置到主线程上
- 3.1 优先微任务队列 ( Microtask Queue ),例如 Promise / MutationObserver
- 3.2 其次执行消息队列 ( Message Queue ),例如 setTimeout / setInterval
- 无论微任务宏任务,其内部肯定也存在同步任务和异步任务,因此重复2-3
考点分析
- 事件循环流程
- 同步异步执行的时机
- async/await 执行顺序
- 递归异步任务时浏览器会发生什么
考题分析
题·一
console.log(0)
setTimeout(() => { // timer1
console.log(6)
}, 3000)
console.log(1)
setTimeout(() => { // timer2
console.log(4)
}, 1000)
Promise.resolve(3) // promise
.then(res => {
console.log(res)
setTimeout(() => { // timer3
console.log(5)
}, 1000)
})
console.log(2)
/*
0 1 2 3 4 5 6
*/
分析
- 整体代码进入主线程
console.log(0)
进入调用栈,输出 0,弹栈- timer1 进入调用栈,这是一个宏任务,将
console.log(6)
推入定时器触发线程并开始倒计时,等待3秒后放入消息队列 console.log(1)
进入调用栈,输出 1,弹栈- timer2 进入调用栈,这是一个宏任务,将
console.log(4)
推入定时器触发线程并开始倒计时,等待1秒后放入消息队列 - promise 进入调用栈
- 执行
Promise.resolve(3)
- then 是一个微任务,将
console.log(res)
和 timer3 打包推入微任务队列
- 执行
- promise 弹栈
console.log(2)
进入调用栈,输出 2,弹栈- 主线程代码执行完毕,微任务队列存在任务
- 从微任务队列中读取
console.log(res)
和 timer3 的 chunk,将其放入主线程 console.log(3)
进入调用栈,输出 3,弹栈- timer3 进入调用栈,这是一个宏任务,将
console.log(5)
推入定时器触发线程并开始倒计时,等待1秒后放入消息队列 - 主线程代码执行完毕,微任务队列不存在任务,消息队列存在任务
- 从消息队列中读取可执行任务
- 等待 timer2 倒计时完毕,进入主线程
console.log(4)
进入调用栈,输出 4,弹栈- 等待 timer1 倒计时完毕,进入主线程
console.log(5)
进入调用栈,输出 5,弹栈- 等待 timer1 倒计时完毕,进入主线程
console.log(6)
进入调用栈,输出 6,弹栈
题·二
setTimeout(() => { // timer1
console.log(1)
}, 20)
setTimeout(() => { // timer2
console.log(2)
}, 10)
setTimeout(() => { // timer3
console.log(3)
}, 20)
setTimeout(() => { // timer4
console.log(4)
}, 10)
/*
2 4 1 3
*/
分析
- timer1 进入调用栈,这是一个宏任务,将
console.log(1)
推入定时器触发线程并开始倒计时,等待20毫秒后放入消息队列 - timer2 进入调用栈,这是一个宏任务,将
console.log(2)
推入定时器触发线程并开始倒计时,等待10毫秒后放入消息队列 - timer3 进入调用栈,这是一个宏任务,将
console.log(3)
推入定时器触发线程并开始倒计时,等待20毫秒后放入消息队列 - timer4 进入调用栈,这是一个宏任务,将
console.log(4)
推入定时器触发线程并开始倒计时,等待10毫秒后放入消息队列 - 主线程代码执行完毕,微任务队列不存在任务,消息队列存在任务
- 定时器触发线程中执行完的先后顺序为 timer2 -> timer4 -> timer1 -> timer3
- 因此依次输出 2 4 1 3
题·三
setTimeout(() => { // timer1
console.log(1)
}, 20)
setTimeout(() => { // timer2
console.log(2)
}, 10)
console.time('loop')
for (let i = 0; i < 99999999; i++) {
// do something
}
console.timeEnd('loop') // 82ms
setTimeout(() => { // timer3
console.log(3)
}, 20)
setTimeout(() => { // timer4
console.log(4)
}, 10)
/*
2 1 4 3
*/
分析
- timer1 进入调用栈,这是一个宏任务,将
console.log(1)
推入定时器触发线程并开始倒计时,等待20毫秒后放入消息队列 - timer2 进入调用栈,这是一个宏任务,将
console.log(2)
推入定时器触发线程并开始倒计时,等待10毫秒后放入消息队列 - for 循环体进入调用栈,执行了82毫秒,弹栈
- 在 for 循环执行的82毫秒中,定时器触发线程中的 timer1 和 timer2 已经倒计时完毕,并按照 timer2 -> timer1 的顺序放入消息队列
- timer3 进入调用栈,这是一个宏任务,将
console.log(3)
推入定时器触发线程并开始倒计时,等待20毫秒后放入消息队列 - timer4 进入调用栈,这是一个宏任务,将
console.log(4)
推入定时器触发线程并开始倒计时,等待10毫秒后放入消息队列 - 主线程代码执行完毕,微任务队列不存在任务,消息队列存在任务
- 定时器触发线程中执行完的先后顺序为 timer2 -> timer1 -> timer4 -> timer3
- 因此依次输出 2 1 4 3
题·四
async function async1() {
console.log('async1 start')
async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 call')
}
console.log(1)
setTimeout(() => { // timer
console.log('setTimeout')
})
async1()
new Promise(resolve => { // promise
console.log('promise')
resolve()
}).then(() => {
console.log('then')
})
console.log(2)
/*
1
async1 start
async2 call
async1 end
promise
2
then
setTimeout
*/
分析
- 先声明了 async1 和 async2 两个函数
console.log(1)
进入调用栈,输出 1,弹栈- timer 进入调用栈,这是一个宏任务,将
console.log(setTimeout)
推入定时器触发线程并开始倒计时,等待4毫秒后放入消息队列 - async1() 进入调用栈
console.log(async1 start)
进入调用栈,输出 async1 start,弹栈- async2() 进入调用栈
console.log(async2 call)
进入调用栈,输出 async2 call,弹栈
- async2() 弹栈
console.log(async1 end)
进入调用栈,输出 async1 end,弹栈
- async1() 弹栈
- promise 进入调用栈
console.log(promise)
进入调用栈,输出 promise,弹栈resolve()
进入调用栈,执行,弹栈- then 是一个微任务,将
console.log(then)
推入微任务队列
- promise 弹栈
console.log(2)
进入调用栈,输出 2,弹栈- 主线程代码执行完毕,微任务队列存在任务
- 从微任务队列中读取
console.log(then)
console.log(then)
进入调用栈,输出 then,弹栈- 主线程代码执行完毕,微任务队列不存在任务,消息队列存在任务
- 等待 timer 倒计时完毕,进入主线程
console.log(setTimeout)
进入调用栈,输出 setTimeout,弹栈
题·五
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 call')
}
console.log(1)
setTimeout(() => {
console.log('setTimeout')
})
async1()
new Promise(resolve => {
console.log('promise')
resolve()
}).then(() => {
console.log('then')
})
console.log(2)
/*
1
async1 start
async2 call
promise
2
async1 end
then
setTimeout
*/
分析
- 先声明了 async1 和 async2 两个函数
console.log(1)
进入调用栈,输出 1,弹栈- timer 进入调用栈,这是一个宏任务,将
console.log(setTimeout)
推入定时器触发线程并开始倒计时,等待4毫秒后放入消息队列 - async1() 进入调用栈
console.log(async1 start)
进入调用栈,输出 async1 start,弹栈await async2()
进入调用栈- async2() 进入调用栈
console.log(async2 call)
进入调用栈,输出 async2 call,弹栈
- async2() 弹栈
- async2() 进入调用栈
await
之后的内容类似 promise 中的 then,因此是个微任务,将console.log(async1 end)
推入微任务队列await async2()
弹栈
- async1() 弹栈
- promise 进入调用栈
console.log(promise)
进入调用栈,输出 promise,弹栈resolve()
进入调用栈,执行,弹栈- then 是一个微任务,将
console.log(then)
推入微任务队列
- promise 弹栈
console.log(2)
进入调用栈,输出 2,弹栈- 主线程代码执行完毕,微任务队列存在任务
- 从微任务队列中读取
console.log(async1 end)
console.log(async1 end)
进入调用栈,输出 async1 end,弹栈- 主线程代码执行完毕,微任务队列存在任务
- 从微任务队列中读取
console.log(then)
console.log(then)
进入调用栈,输出 then,弹栈- 主线程代码执行完毕,微任务队列不存在任务,消息队列存在任务
- 等待 timer 倒计时完毕,进入主线程
console.log(setTimeout)
进入调用栈,输出 setTimeout,弹栈
题·六
<button id="btn">按钮</button>
<script>
const button = document.getElementById('btn')
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask-1'))
console.log('Listener-1')
})
button.addEventListener('click', () => {
Promise.resolve().then(() => console.log('Microtask-2'))
console.log('Listener-2')
})
button.click()
/*
// 自动触发
Listener-1
Listener-2
Microtask-1
Microtask-2
// 手动点击
Listener-1
Microtask-1
Listener-2
Microtask-2
*/
</script>
分析 —— 自动触发
button.click()
进入调用栈Promise.resolve().then(() => console.log('Microtask-1'))
进入调用栈- 执行
Promise.resolve()
- then 是一个微任务,将
console.log(Microtask-1)
推入微任务队列
- 执行
Promise.resolve().then(() => console.log('Microtask-1'))
弹栈console.log('Listener-1')
进入调用栈,输出 Listener-1,弹栈Promise.resolve().then(() => console.log('Microtask-2'))
进入调用栈- 执行
Promise.resolve()
- then 是一个微任务,将
console.log(Microtask-2)
推入微任务队列
- 执行
Promise.resolve().then(() => console.log('Microtask-2'))
弹栈console.log('Listener-2')
进入调用栈,输出 Listener-2,弹栈- 主线程代码执行完毕,微任务队列存在任务
- 从微任务队列中读取
console.log('Microtask-1')
console.log('Microtask-1')
进入调用栈,输出 Microtask-1,弹栈- 主线程代码执行完毕,微任务队列存在任务
- 从微任务队列中读取
console.log('Microtask-2')
console.log('Microtask-2')
进入调用栈,输出 Microtask-2,弹栈
分析 —— 手动点击
Promise.resolve().then(() => console.log('Microtask-1'))
进入调用栈- 执行
Promise.resolve()
- then 是一个微任务,将
console.log(Microtask-1)
推入微任务队列
- 执行
Promise.resolve().then(() => console.log('Microtask-1'))
弹栈console.log('Listener-1')
进入调用栈,输出 Listener-1,弹栈- 主线程代码执行完毕,微任务队列存在任务
- 从微任务队列中读取
console.log('Microtask-1')
console.log('Microtask-1')
进入调用栈,输出 Microtask-1,弹栈Promise.resolve().then(() => console.log('Microtask-2'))
进入调用栈- 执行
Promise.resolve()
- then 是一个微任务,将
console.log(Microtask-2)
推入微任务队列
- 执行
Promise.resolve().then(() => console.log('Microtask-2'))
弹栈console.log('Listener-2')
进入调用栈,输出 Listener-2,弹栈- 主线程代码执行完毕,微任务队列存在任务
- 从微任务队列中读取
console.log('Microtask-2')
console.log('Microtask-2')
进入调用栈,输出 Microtask-2,弹栈
题·七
function timerTimeout() {
setTimeout(timerTimeout) // timer
}
function timerPromise() {
Promise.resolve().then(timerPromise) // promise
}
timerTimeout()
/*
OR
*/
timerPromise()
分析 —— timerTimeout
timerTimeout()
进入调用栈- timer 进入调用栈,这是一个宏任务,将
() => { timerTimeout() }
推入定时器触发线程并开始倒计时,等待4毫秒后放入消息队列
- timer 进入调用栈,这是一个宏任务,将
timerTimeout()
弹栈- 微任务队列不存在任务,消息队列存在任务
- 进入下一轮事件循环
- 循环上面流程
- 因为当前任务总是可以弹栈,导致页面刷新会消失,且正常操作
分析 —— timerPromise
timerTimeout()
进入调用栈- 执行
Promise.resolve()
- then 是一个微任务,将
() => { timerPromise() }
推入微任务队列
- 执行
timerTimeout()
弹栈- 微任务队列存在任务
- 循环上面流程
- 永远进不到宏任务,无法进入下一轮事件循环,导致页面刷新符号不会消失
- 微任务队列存在任务
拓展
Node 的事件循环机制 -> 使用基于C语言的 libuv 库让 Node 平台的异步回调回到主线程的机制
- 每次切换阶段会清空当前阶段的所有任务
- 清空任务后,切换阶段前会清空微任务队列
- process.nextTick 优先级 高于 Promise
- poll 阶段会造成阻塞,等待 I/O 池的任务执行 libuv 中存在一个类似定时器触发线程的线程池,在定时器入栈时会将定时器的回调挂载到一课红黑树的节点上,等倒计时完毕后将其推入 timers
┌───────────────────────────┐
┌─> │ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │ <─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└── ┤ close callbacks │
└───────────────────────────┘
( 清空微任务 )
└─ timers 阶段
│ ├─ timers 队列**不为空**,则执行
│ │ └─ 清空队列后进入 poll 阶段
│ │
│ └─ timers 队列中**为空**,则进入 poll 阶段
( 清空微任务 )
├─ poll 阶段
│ ├─ poll 队列**不为空**,则执行
│ │ └─ 清空队列后检测是否存在 setImmediate
│ │ └─ 存在,则进入 check 阶段去执行
│ │
│ └─ poll 队列中**为空**
│ └─ 检测是否存在 I/O 任务
│ ├─ 存在,阻塞
│ │ └─ 检测是否存在可调度的定时器任务
│ │ ├─ 存在,则进入 timers 阶段去执行
│ │ │
│ │ └─ 不存在,则检测是否存在 setImmediate
│ │ ├─ 存在,则进入 check 阶段去执行
│ │ │
│ │ └─ 不存在,则等待 I/O 任务加入队列并执行
│ │
│ └─ 不存在,则检测是否存在 setImmediate
│ ├─ 存在,则进入 check 阶段去执行
│ │
│ └─ 不存在,则进入 timers 阶段
( 清空微任务 )
└─ check 阶段
└─ 清空队列后进入 timers 阶段
( 清空微任务 )
题·一
setTimeout(() => {
console.log('setTimeout')
})
setImmediate(() => {
console.log('setImmediate')
})
分析
结果不确定
- 初始化 -> setTimeout 放入线程池 -> setImmediate 放入 check
- 主线程执行完毕
- 进入 timers 阶段
- 如果这时候倒计时完毕,timers 中就会被推入
console.log('setTimeout')
- 执行并输出 setTimeout -> 进入 poll 阶段
- 如果这时候倒计时未完毕,timers 中队列为空 -> 进入 poll 阶段
- 如果这时候倒计时完毕,timers 中就会被推入
- 进入 poll 阶段
- 检测队列是否为空 -> 为空
- 检测是否存在 I/O 任务 -> 不存在
- 检测是否存在 setImmediate -> 存在 -> 进入 check 阶段
- 进入 check 阶段
- 输出 setImmediate
- 如果之前进入 timers 阶段时 setTimeout 的倒计时未完毕,但这时候完毕了,说明这时候 timers 队列中存在
console.log('setTimeout')
- 输出 setTimeout
题·二
setTimeout(() => {
console.log('setTimeout')
})
time('loop')
for (let i = 0; i < 9999999; i++) { }
timeEnd('loop') // loop: 8ms
setImmediate(() => {
console.log('setImmediate')
})
分析
结果确定
- 初始化 -> setTimeout 放入线程池 -> 执行 for 循环 -> setImmediate 放入 check
- 主线程执行完毕
- 进入 timers 阶段
- 因为 for 循环执行花费了8毫秒,timers 中已经被推入
console.log('setTimeout')
- 执行并输出 setTimeout -> 进入 poll 阶段
- 因为 for 循环执行花费了8毫秒,timers 中已经被推入
- 进入 poll 阶段
- 检测队列是否为空 -> 为空
- 检测是否存在 I/O 任务 -> 不存在
- 检测是否存在 setImmediate -> 存在 -> 进入 check 阶段
- 进入 check 阶段
- 输出 setImmediate
题·三
fs.readFile('/path/to/file', () => {
setTimeout(() => {
console.log('setTimeout')
})
setImmediate(() => {
console.log('setImmediate')
})
})
分析
结果确定
- 初始化 -> readFile 放入 I/O 池
- 主线程执行完毕
- 进入 timers 阶段
- 没有任务 -> 进入 poll 阶段
- 进入 poll 阶段
- 检测队列是否为空 -> ( 假设存在文件已获取到,并将回调加入了 poll 队列中 )
- setTimeout 放入线程池 -> setImmediate 放入 check
- 检测是否存在 setImmediate -> 存在 -> 进入 check 阶段
- 检测队列是否为空 -> ( 假设存在文件已获取到,并将回调加入了 poll 队列中 )
- 进入 check 阶段
- 输出 setImmediate -> 进入 timers 阶段
- 进入 timers 阶段
- 如果这时候倒计时完毕,timers 中就会被推入
console.log('setTimeout')
- 执行并输出 setTimeout
- 如果这时候倒计时未完毕,timers 中队列为空 -> 进入 poll 阶段 -> 循环,直到倒计时完毕
- 执行并输出 setTimeout
题·四
setTimeout(() => { // timer1
console.log('timout-1')
Promise.resolve().then(() => {
console.log('promise-1') // promise1
})
process.nextTick(() => {
console.log('tick-1') // tick1
})
setTimeout(() => { // timer3
console.log('setTimeout-3')
})
})
Promise.resolve().then(() => {
console.log('promise-2') // promise2
})
setTimeout(() => { // timer2
console.log('timeout-2')
Promise.resolve().then(() => {
console.log('promise-3') // promise3
})
process.nextTick(() => {
console.log('tick-2') // // tick2
})
})
分析
结果确定
- 初始化 -> timer1 放入线程池 -> 执行
Promise.resolve()
-> promise2 放入微任务队列 -> timer2 放入线程池 - 主线程执行完毕
- 清空微任务队列
- 执行 promise2,并输出promise-2 -> 进入 timers 阶段
- 进入 timers 阶段 ( 假设 timer1 / timer2 倒计时都完毕了 )
- timers 中存在 timer1 和 timer2,先进先出原则,执行 timer1
- 输出timout-1
- 执行
Promise.resolve()
-> promise1 放入微任务队列 -> tick1 放入微任务队列 -> timer3 放入线程池 - timer1 执行完毕,清空微任务队列
- nextTick 优先级更高,因此依次输出tick-1和promise-1
- 执行 timer2
- 输出timeout-2
- 执行
Promise.resolve()
-> promise3 放入微任务队列 -> tick2 放入微任务队列 - timer2 执行完毕,清空微任务队列
- nextTick 优先级更高,因此依次输出tick-2和promise-3
- 执行
- 输出timeout-2
- timers 队列清空 -> 进入 poll 阶段
- timers 中存在 timer1 和 timer2,先进先出原则,执行 timer1
- 进入 poll 阶段
- 检测队列是否为空 -> 为空
- 检测是否存在 I/O 任务 -> 不存在
- 检测是否存在 setImmediate -> 不存在 -> 进入 timers 阶段
- 进入 timers 阶段 ( 假设 timer3 倒计时完毕了 )
- timers 中存在
console.log('setTimeout-3')
- 执行并输出 setTimeout-3
- timers 中存在