同步与异步
异步:代码执行的顺序并不是按照从上到下的顺序一次性一次执行,而是在不同的时间段执行,一部分代码在“未来执行”。
单线程与多线程
JavaScript 是单线程的,怎么还存在异步,那些耗时操作到底交给谁去执行了?
JavaScript 其实就是一门语言,说是单线程还是多线程得结合具体运行环境。 JavaScript 的运行通常是在浏览器中进行的,具体由 JavaScript 引擎去解析和运行。
浏览器的内核是多线程的,一个浏览器通常由以下几个常驻的线程:
- 渲染引擎线程:负责页面的渲染
- JS引擎线程:负责 JavaScript 的解析和执行
- 定时触发器线程:处理定时事件,比如 setTimeout ,setInterval
- 事件触发线程:处理 DOM 事件
- 异步 HTTP 请求线程:处理 HTTP 请求
注意:渲染线程和 JS 引擎线程是不能同时进行的。渲染线程在执行的时候, JS 引擎线程会被挂起。因为 JS 可以操作 DOM ,若在渲染中 JS 处理了 DOM ,浏览器可能就不知所措了。
_
虽然 JavaScript 是单线程的,可是浏览器内部不是单线程的。一些 I/O 操作、定时器的计时和事件监听(click、keydown……)等都是由浏览器提供的其它线程来完成的。
事件循环
JS 引擎用来执行栈中的同步任务,当所有同步任务执行完毕后,栈被清空,然后读取消息队列中的一个待处理任务,并把相关回调函数压入栈中,单线程开始执行新的同步任务。
JS 引擎线程从消息队列中读取任务是不断循环的,每次栈被清空后,都会在消息队列中读取新的任务,如果没有新的任务,就会等待,直到有新的任务,这就叫事件循环。
事件,用户交互,脚本,渲染,网络这些都是我们所熟悉的东西,它们都是由 event loop
协调的。触发一个 click
事件,进行一次 ajax
请求,背后都有 event loop
在运作。
注意:node 的 event loop 和浏览器端的 event loop 从规范上就存在差异,我们下面介绍的都是浏览器端的规范。
任务队列
一个事件循环中有一个或者多个 Task 队列。
当用户代理安排一个任务,必须将任务增加到事件循环的一个Task 队列中。
每一个 Task 都来源于指定的任务源,比如可以为鼠标、键盘事件提供一个 Task 事件,其它事件又是一个单独的队列。
宏任务macro task
也被称为 task。
总结来说任务的来源有:
- srcipt
- setTimeout
- setInterval
- setImmediate
- I/O
-
微任务micro task
微任务在最新的标准中也被称为
jobs
。每一个 event loop 都只有一个 micro task 队列。一个 micro task 会被 push 进 micro task 队列而非 task 队列。
通常任务 micro task 任务源有: process.nextTick
- Promise
- Object.observe(已废弃)
- MutaitonObserver(HTML5新特性)
事件循环的执行过程(重点)
- 从 script (整体代码)开始第一次循环,之后全局上下文进入函数调用栈,直到调用栈清空(只剩全局)。
- 执行所有的 micro task 。
- 执行完 micro task 队列里的任务,有可能会渲染更新。
- 执行一个最老的 macro task。
- 到第二步,一致这样循环下去。
渲染更新会在 event loop 中的 tasks 和 microtasks 完成后进行,但并不是每轮 event loop 都会更新渲染,这取决于是否修改了 DOM 和浏览器觉得是否有必要在此时立即将新状态呈现给用户。如果在一帧的时间内(时间不确定,因为浏览器每秒的帧数总在波动,16.7 ms 只是估算,并不准确)修改了多出 DOM,浏览器可能将变动积攒起来,只进行一次绘制,这是合理的。
检验你的学习成果吧
下面我们通过一连串的例子来看看我们是否真的掌握了
例子一:
console.log('script start');
setTimeout(function () {
console.log('setTimeout');
}, 0);
Promise.resolve()
.then(function () {
console.log('promise1');
})
.then(function () {
console.log('promise2');
});
console.log('script end');
答案一:
script start
script end
promise1
promise2
setTimeout
例子二:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
答案二:在浏览器端是这样,在node环境就是其它的结果了
1 7 6 8
2 4 3 5
9 11 10 12
解析:
第一轮事件循环:
- 整体 script 作为第一个宏任务进入主线程,遇到
console.log
,输出 1 。 - 遇到
setTimeout
,其回调函数被分发到宏任务 Event Queue 中,我们暂且记为setTimeout1
。 - 遇到
process.nextTick()
,其回调函数被分发到微任务 Event Queue 中,我们记为process1
。 - 遇到
Promise
,new Promise
直接执行,输出 7 。then
被分发到微任务 Event Queue 中,我们记为then1
。 - 又遇到
setTimeout
,其回调函数被分发到宏任务 Event Queue 中,我们记为setTimeout2
中。
当第一轮事件循环宏任务结束时输出了 1 和 7,接着执行 process1
和 then1 微任务,输出 6 和 8 。
接着第二轮事件循环从 setTimeout1
宏任务开始:
- 首先输出 2 。
- 遇到
process.nextTick()
,将其分发到微任务 Event Queue 中,记为process2
。 - 遇到
Promise
,new Promise
立即执行输出 4 ,then
被分发到微任务 Event Queue 中,记为then2
。
第二轮事件循环宏任务结束,输出了 2 和 4 。接下来执行process2
和 then2
这两个微任务,输出 3 和 5 。
接着第三轮事件循环从 setTimeout2
宏任务开始:
- 首先输出 9 。
- 遇到
process.nextTick()
,将其分发到微任务 Event Queue 中,记为process3
。 - 遇到
Promise
,new Promise
立即执行输出 11 ,then
被分发到微任务 Event Queue 中,记为then3
。
第二轮事件循环宏任务结束,输出了 9 和 11 。接下来执行process3
和then3
这两个微任务,输出 10 和 12 。
demo演示
JavaScript 运行机制详解:再谈Event Loop——阮一峰
从event loop规范探究javaScript异步及浏览器更新渲染时机 —— 杨敬卓写得很详细
Tasks, microtasks, queues and schedules有关于代码的执行过程可以看
深入理解JavaScript的执行机制(同步和异步)