背景
首先我们知道JS是单线程运行的,也就是每次只能执行一个任务。那么为了协调事件,用户交互,脚本,渲染,网络任务等,必须使用事件循环进行协调
事件循环机制有浏览器和Node两种实现,这里暂不讨论Node部分
基础知识
- 栈 是一种先进后出的数据结构
- 队列 是一种先进先出的数据结构
JS运行机制
- 首先任务一个一个地进入主线程执行,形成一个执行栈(stack)
- 同步任务会立刻执行,异步任务会记录下,等待回调函数执行的时机
- 当回调函数需要触发了,就把回调函数放进任务队列(queue)
- 当执行栈里的任务全部执行完毕,主线程空闲,就回去读取任务队列里的任务,进入主线程执行
- 如此又回到了步骤1,循环这样一个“读取——执行”的过程
宏任务和微任务
上面的运行机制是简化了的,实际上任务可以细分为宏任务和微任务。上面所说的步骤2实际上会区分宏任务和微任务,分别放入不同的队列。
- 宏任务:
script(整体代码)
,setTimeout
,setInterval
,I/O
,UI Rendering
,setImmediate(IE10和Node独有)
- 微任务:
Promise(async/await)
,Object.observe(废弃)
,MutationObserver
,Process.nextTick(Node独有)
Event Loop的完整过程
- 第一次事件循环,整体代码作为一个宏任务,进入执行栈
- 执行过程中,同步代码立刻执行,需要等待的任务根据宏任务和微任务,把回调函数分别放入两个队列
- 当主线程空闲的时候,首先检查微任务队列是否有任务,如果有的话,把微任务队列全部执行,如果在微任务的执行中又加入了新的微任务,那么循环检查并执行,直到队列为空才结束
- 然后再从宏任务队列中取出一个任务,进入执行栈
- 如此又回到了步骤1进行循环
补充
DOM渲染在什么时候
首先回忆上面提到的微任务MutationObserver
最初的页面是没有提供对DOM变化的监听的,不能实时感知到DOM的变化,这个接口的引入是为了监视DOM树发生变化并作出响应。当DOM发生变化的时候,生成一个微任务,推入队列中。
那么每次event loop结束都会进行页面渲染吗?
不一定,这个需要根据屏幕刷新率、页面性能、页面是否在后台运行等多个条件综合决定。一般来说这个渲染频率是固定的。
如果两个宏任务间隔时间很短,比如都是setTimeout(func, 0)那么只会触发一次渲染
如果两个宏任务间隔稍长,那么是会触发多次渲染的。
例题
new Promise(resolve => {
console.log(1) // 这里需要注意
resolve()
}).then(() => {
console.log(2)
})
console.log(3)
setTimeout(() => {
console.log(4)
})
打印结果是:1 3 2 4
需要注意的是我们在说加入任务队列的时候,都是把回调函数加入队列。不要看到Promise就觉得所有的内容都扔到队列里去了,new Promise的时候会执行传入的这个函数参数,console.log(1)是同步的,会立刻执行