共计 3595 个字符,预计需要花费 9 分钟才能阅读完成。
1. 说明
读过本文章后,您能知道:
JavaScript 代码在浏览器中的执行机制和事件循环
面试中经常遇到的代码输出顺序问题
首先通过一段代码来验证你是否了解代码输出顺序,如果你不知道输出顺序,那么本文可以帮助你了解:
console.log(1)
setTimeout(function () {
new Promise(function (resolve) {
console.log(2)
resolve()
})
.then(() => { console.log(3) })
})
setTimeout(function () {
console.log(4)
})
console.log(5)
2. JavaScript 执行机制
JavaScript 语言的执行是单线程 (single thread) 的。
所谓的单线程,就是指一次只执行一个任务,如果有多个任务,就必须排队,前面一个任务完成,才能执行后面任务。
这种模式的好处是实现起来比较简单,执行环境相对单纯;坏处是只要有一个任务耗时很长,后面的任务都必须排队等待,会拖延整个程序的执行。常见的浏览器无响应(假死),往往就是因为某一段 JavaScript 代码长时间运行(比如死循环),导致整个页面卡在这个地方,其他任务无法执行。
为了解决这个问题,JavaScript 语言将任务的执行模式分成两种:同步 (Synchronous) 和异步(Asynchronous)。
同步任务:在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。
异步任务:不进入主线程、而进入 ” 任务队列 ”(task queue)的任务,只有 ” 任务队列 ” 通知主线程,某个异步任务可以执行了,该任务才会进入主线程执行。
JavaScript 执行机制:
1、所有同步任务都在主线程上执行,形成一个执行栈(execution context stack)。2、主线程之外,还存在一个 ” 任务队列 ”(task queue)。只要异步任务有了运行结果,就在 ” 任务队列 ” 之中放置一个事件。3、一旦 ” 执行栈 ” 中的所有同步任务执行完毕,系统就会读取 ” 任务队列 ”,如果有有执行任务,则进入执行栈,开始执行。4、主线程不断重复上面的三步,此过程也就是常说的 Event Loop(事件循环)。
3. JavaScript 执行机制中名词介绍
3.1 执行栈
当我们调用一个方法的时候,js 会生成一个与这个方法相对应的执行环境,也叫执行上下文,这个执行环境存在着这个方法的私有作用域、参数、this 对象等等。因为 js 是单线程的,同一时间只能执行一个方法,所以当一系列的方法被依次调用的时候,js 会先解析这些方法,把其中的同步任务按照执行顺序排队到一个地方,这个地方叫做执行栈。
3.2 主线程
JavaScript 是单线程的,那么这个单线程就成为主线程。而事件循环在主线程执行完执行栈代码后,才执行的。所以主线程代码执行时间过长,会阻塞事件循环的执行。只有当执行栈为空的时候(同步代码执行完毕),才会执行事件循环来观察有哪些事件回调需要执行,当事件循环检测到任务队列有事件就读取出回调放到执行栈由主线程执行。
3.3 任务队列
任务队列也有时称叫消息队列、回调队列。
异步操作会将相关回调添加到任务队列中。而不同的异步操作添加到任务队列的时机也不同,如 onclick, setTimeout,ajax 处理的方式都不同,这些异步操作是由浏览器内核的 webcore 来执行的,webcore 包含下图中的 3 种 webAPI,分别是 DOM Binding、network、timer 模块。
DOM Binding 模块处理一些 DOM 绑定事件,如 onclick 事件触发时,回调函数会立即被 webcore 添加到任务队列中。
network 模块处理 Ajax 请求,在网络请求返回时,才会将对应的回调函数添加到任务队列中。
timer 模块会对 setTimeout 等计时器进行延时处理,当时间到达的时候,才会将回调函数添加到任务队列中。
3.4 事件循环
如上图所示,JavaScript 整体执行过程:
主线程运行的时候会生成堆(heap)和栈(stack);
js 从上到下解析方法,将其中的同步任务按照执行顺序排列到执行栈中;
当程序调用外部的 API 时,比如 ajax、setTimeout 等,会将此类异步任务挂起,继续执行执行栈中的任务,等异步任务返回结果后,再按照执行顺序排列到任务队列中;
主线程先将执行栈中的同步任务清空,然后检查任务队列中是否有任务,如果有,就将第一个事件对应的回调推到执行栈中执行,若在执行过程中遇到异步任务,则继续将这个异步任务排列到任务队列中。
主线程每次将执行栈清空后,就去任务队列中检查是否有任务,如果有,就每次取出一个推到执行栈中执行,这个过程是循环往复的 … …,这个过程被称为“Event Loop 事件循环”。
也可以参考如下链接:https://html.spec.whatwg.org/…
4. 宏任务和微任务
出现 Promise 后,JavaScript 对于任务的定义除了广义的同步任务和异步任务,又对任务做了更精细的定义,macrotask(宏任务)和 microtask(微任务):
macrotask(按优先级顺序排列): script(你的全部 JS 代码,“同步代码”), setTimeout, setInterval, setImmediate(node 的), I/O,UI rendering
microtask(按优先级顺序排列):process.nextTick(node 的),Promise(这里指浏览器原生实现的 Promise), Object.observe, MutationObserver
注意:宏任务、微任务中出现的 nodejs 中的方法是 nodejs 专有的,浏览器的 JavaScript 环境暂时没有支持。
4.1 事件循环对宏任务和微任务的处理
有了宏任务和微任务后,JavaScript 事件循环对此处理方法如下形式:
js 引擎首先从 macrotask queue 中取出第一个任务,执行完毕后,将 microtask queue 中的所有任务取出,按顺序全部执行;
然后再从 macrotask queue(宏任务队列)中取下一个,执行完毕后,再次将 microtask queue(微任务队列)中的全部取出;
循环往复,直到两个 queue 中的任务都取完。
注意::此处把执行同步代码算成第一个宏任务了。
5. 一个实际例子讲解 JavaScript 执行流程
如下代码:
console.log(‘1’);
setTimeout(function() {
console.log(‘2’);
new Promise(function(resolve) {
console.log(‘3’);
resolve();
}).then(function() {
console.log(‘4’)
})
})
console.log(‘5’);
setTimeout(function() {
console.log(‘6’);
new Promise(function(resolve) {
console.log(‘7’);
resolve();
}).then(function() {
console.log(‘8’)
})
})
console.log(‘9’);
上面代码执行过程:
第一轮事件循环
整体 script 代码 (同步代码) 作为第一个宏任务进入主线程,遇到 console.log,输出 1
遇到 setTimeout, 其回调函数被放到宏任务队列中,暂记为 setTmineout1
遇到 console.log,输出 5
遇到 setTimeout, 其回调函数被放到宏任务队列中,暂记为 setTmineout2
遇到 console.log,输出 9
一个宏任务执行结束,去微任务队列查找是否有待执行的任务,没有,结束
第一轮循环结束,输出:1 5 9
第二轮事件循环
从宏任务队列中取出一个任务,即 setTmineout1,开始执行
遇到 console.log,输出 2
遇到 Promise, 创建 Promise, 输出了 3, 同时把 Promise.then 回调函数放到微任务队列
一个宏任务执行结束,去微任务队列查找是否有待执行的任务, 发现有微任务,全部取出放到执行栈执行
执行微任务,此时就一个微任务,console.log,输出 4
微任务执行结束
第二轮循环结束,输出:2 4
第三轮事件循环与第二轮一样,输出:6 7 8
事件循环发现所有任务都已经处理完毕,此时程序执行结束
全部的输出:1 5 9 2 3 4 6 7 8, 可复制代码到 chrome 浏览器控制台中运行校验结果
注意:浏览器环境 JavaScript 的执行机制和 node 中 JavaScript 的执行机制是不同的。
参考资料
这一次,彻底弄懂 JavaScript 执行机制
JavaScript 任务队列的顺序机制(事件循环)
搞懂 JavsScript 异步 — 事件轮询
JS 与 Node.js 中的事件循环