JavaScript 运行机制
- 所有同步任务都在主线程上执行,形成一个
执行栈(Execution Context Stack)
。 - 主线程之外,还存在
任务队列(Task Queue)
。只要异步任务有了运行结果,就在任务队列之中放置一个事件。 - 一旦
执行栈
中的所有同步任务执行完毕,系统就会读取任务队列
,看看里面有哪些事件。如果有那些对应的异步任务
,于是结束等待状态,进入执行栈,开始执行。 - 主线程不断重复上面的第三步
- 一个事件循环中有一个或者是多个任务队列
总结:调用栈中的同步任务都执行完毕,栈内被清空了,就代表主线程空闲了。
这个时候就会去任务队列中按照顺序读取一个任务放入到栈中执行。
每次栈内被清空,都会去读取任务队列有没有任务,有就读取执行,一直循环读取~执行的操作。
EventLoop 事件循环
介绍
主线程从“任务队列”中读取执行事件,这个过程是循环不断的,这个机制被称为事件循环。此机制具体如下:
- JavaScript 中有两种异步任务:
宏任务(MacroTask)
、微任务(MicroTask)
。 - 主线程会不断从任务队列中按顺序取任务执行,每执行完一个任务都会检查『微任务』队列是否为空(执行完一个任务的具体标志是函数执行栈为空),如果不为空则会一次性执行完所有『微任务』。
- 然后再进入下一个循环去任务队列中取下一个任务执行。
MacroTask 宏任务
- script 全部代码、
setTimeout
、setInterval
、setImmediate
(浏览器暂时不支持,只有 IE10 支持,具体可见 MDN。)、I/O
、UI Rendering
。
MicroTask 微任务
Process.nextTick
(Node独有)、Promise
、Object.observe(废弃)
、MutationObserver
(具体使用方式查看这里)
同步任务和异步任务
JavaScript 单线程任务被分为同步任务和异步任务,同步任务会在调用栈中按照顺序等待主线程依次执行。异步任务会在异步任务有了结果后,将注册的回调函数放入任务队列中,等待主线程空闲的时候(调用栈被清空),被读取到栈内等待主线程的执行。
事件循环的进程模型
- 选择当前要执行的任务队列,选择任务队列中最先进入的任务,如果任务队列为空即 null,则执行跳转到微任务的执行步骤。
- 将事件循环中的任务设置为当前选择任务
- 执行任务
- 将事件循环中当前运行任务设置为 null
- 将已经运行完成的任务从任务队列中删除
- Microtasks 检查步骤,进入微任务检查点。
- 设置微任务检查点标志为 true。
- 当事件循环微任务执行队列不为空时:选择一个
最先进入的微任务队列
的微任务,将事件循环的微任务设置为当前选择的微任务
。 - 运行微任务
- 将已经执行完成的微任务设置为 null
- 移除微任务队列中的当前运行完成的微任务
- 清理 IndexDB 事务
- 设置进入微任务检查点的标志为 false。
- 更新界面渲染
- 返回第一步
总结
- 执行栈在执行完 同步任务 后,查看 执行栈 是否为空,如果为空,就会去检查 微任务队列 是否为空,如果为空的话,就执行 宏任务,否则就一次性执行完 所有微任务。
- 每次单个 宏任务 执行完毕后,检查 微任务队列 是否为空,如果不为空的话,会按照
先入先出
的规则全部执行完 微任务 后,设置 微任务队列 为 null,然后再执行宏任务,如此循环。
举个栗子
代码
1 | console.log('script start'); |
执行结果
1 | script start |
执行步骤
- 第一次执行:执行同步代码,将宏任务和微任务划分到各自队列中。
- 第二次执行:执行宏任务后,检测到 微任务队列 中不为空,执行
Promise1
,执行完成Promise1
后,调用Promise2.then
,放入 微任务队列 中,再执行Promise2.then
。 - 第三次执行:当 微任务队列 中为空时,执行 宏任务,执行
setTimeout callback
,打印日志。 - 第四次执行:清空任务队列和调用栈
图例
1 | setTimeout(function () { |
再举个栗子
1 | // 例子 |
执行步骤如上所示
- 开始执行
- 首先我们执行同步代码,先打印
script start
。 - 再打印
async2 end
,将 async2.then 放入微任务队列中。 - 继续执行,将 setTimeout 放入宏任务队列中。
- 再打印
Promise
,将 Promise.then 放入微任务队列中。 - 最后打印
script end
- 首先我们执行同步代码,先打印
- 执行完成后,检查微任务队列不为空,按照先进先出原则继续执行。
- 执行 async2.then 打印
async1 end
- 执行 Promise.then 打印
promise1
,并将 promise1.then 放入微任务队列中。
- 执行 async2.then 打印
- 此时检查微任务队列继续不为空
- 执行 promise1.then 打印
promise2
- 执行 promise1.then 打印
- 最后执行宏任务队列中的任务
- 执行 setTimeout,延迟时间到后,将其回调函数放入任务队列中。
- 执行回调函数
- 打印
setTimeout
- 打印
习题
题目
1 | console.log('script start'); |
答案
先思考再查看答案哦~
运行结果
1 | script start |
运行步骤
- 开始执行
- 首先我们执行同步代码,先打印
script start
。 - 再打印
async2 end
,将 async2.then 放入微任务队列中。 - 继续执行,将 setTimeout1、setTimeout2、setTimeout3、setTimeout4 依次放入宏任务队列中。
- 再打印
Promise
,将 Promise.then 放入微任务队列中,将 Promise-setTimeout 放入宏任务队列中。 - 最后打印
script end
- 首先我们执行同步代码,先打印
- 执行完成后,检查微任务队列不为空,按照先进先出原则继续执行。
- 执行 async2.then 打印
async1 end
- 执行 Promise.then 将 promise1-setTimeout 放入宏任务队列中,打印
promise1
,并将 promise1.then 放入微任务队列中。
- 执行 async2.then 打印
- 此时检查微任务队列继续不为空
- 执行 promise1.then 将 promise2-setTimeout 放入宏任务队列中,打印
promise2
。
- 执行 promise1.then 将 promise2-setTimeout 放入宏任务队列中,打印
- 最后执行宏任务队列中的任务
- 依次执行【setTimeout1、setTimeout2、setTimeout3、setTimeout4、Promise-setTimeout、promise1-setTimeout、promise2-setTimeout】。
- 等待延迟时间到后,将其回调函数放入任务队列中。
- 依次执行回调函数
- 打印
setTimeout3
- 打印
setTimeout4
- 打印
promise2-setTimeout
- 打印
setTimeout2
- 打印
Promise-setTimeout
- 打印
promise1-setTimeout
- 打印
setTimeout1
- 打印
参考来源
- 并发模型与事件循环
- EventLoop
- Node EventLoop
- JavaScript 垃圾回收
- 阮一峰 EventLoop