是什么
javascript 是一门单线程编程语言,也就是说同一个时间点,只会有一个 js 任务在执行。 那么对于执行一些需要长时间等待的任务来说,它们会占据线程不放,这会造成后续代码无法执行,程序无法正常使用。这是单线程的弊端,而 JS 是通过事件循环机制(Event Loop)来解决这一弊端。
如果想讲清楚事件循环,需要从以下几个方面入手:
- 栈、堆、队列
- 同步任务
- 异步任务
- 异步任务-宏任务队列
- 异步任务-微任务队列
- 事件循环
- 事件循环-举例理解
- 总结
栈、堆、队列
栈
一种特殊的数据结构,栈内的元素只能通过一端访问,被访问的一端叫做栈顶。
- 栈的数据结构是,后进先出(Last-in-first-out)
- 所有不在栈顶的元素都无法访问
- 如果想得到栈底的数据,必须先去掉上面的元素
- 举例:像子弹夹,先压入弹夹的子弹,最后才射出
堆
一种经过排序的树形数据结构
- 每个结点都有一个值。
- 通常我们所说的堆的数据结构,是指二叉堆。
- 堆的特点是根结点的值最小(或最大),且根结点的两个子树也是一个堆。
- 由于堆的这个特性,常用来实现优先队列
- 举例:我们在图书馆的书架上取书,虽然书的摆放是有顺序的,但是我们想取任意一本时不必像栈一样,先取出前面所有的书,我们只需要关心书的名字。
队列
一种特殊的数据结构,队列的元素只能通过一端访问。
- 队列的数据结构是,先进先出(first-in-first-out)
- 队列的末端添加元素
- 队列的顶端移出元素
- 举例:像超市排队结算,先排队的人结算完肯定先走
同步任务
同步任务,即是主线程的任务,开始执行 javascript 的代码的时候,会先判断这段代码是同步任务还是异步任务。
- 同步任务,直接进入主线程按照调用栈的顺序被执行
- 同步任务都执行完毕,读取任务队列中的 callback 执行
异步任务
异步任务,即是队列任务,非主线程的任务。当任务队列通知主线程,某个异步任务有结果了,可以立即执行了。该任务才会进入主线程执行。
当异步任务进入所谓的 “任务队列” 中,任务队列具有 队列 的性质,先进先出,也就是后加入的任务必须等待前面的任务执行完才能执行。
如果在执行的过程中突然有重要的数据需要获取,或是说有事件突然需要处理一下,按照队列的先进先出顺序这些是无法得到及时处理的。
这个时候就有了宏任务和微任务,微任务使得一些异步任务得到及时的处理。
举例: 宏任务和微任务形象的来说就是:你去营业厅办一个业务会有一个排队号码,当叫到你的号码的时候你去窗口办充值业务(宏任务执行),在你办理充值的时候你又想改个套餐(微任务),这个时候工作人员会直接帮你办,不会让你重新排队。
异步任务-宏任务队列
- 宏任务进入宏任务队列
- 事件回调即是一个宏任务
- 常见的宏任务有
script(整体的代码)
setTimeout
setInterval
I/O 操作
UI 渲染
setImmediate (Node.js 环境)。
异步任务-微任务队列
- 微任务进入微任务队列
- 执行在当前宏任务后,下一个宏任务之前
- 常见的微任务有
Promise.then
Mutation Observer API (具体使用)
Process.nextTick(Node 独有)
事件循环
上面讲述了一些事件循环涉及到的概念,如果想弄明白事件循环,还需要了解一个 执行栈的概念。
执行栈
- 所有同步任务都在主线程上执行,形成一个初始化的执行栈。
- 主线程之外,还存在一个 “任务队列”,它是存放异步任务运行后的回调函数,在”任务队列”之中放置一个事件。
- 一旦 “执行栈” 中的所有同步任务执行完毕,主线程就会读取 “任务队列”,看看里面有哪些事件。然后把那些对应的异步任务,压入执行栈中,开始执行。
- 每一次 Event Loop 会先执行所有宏任务 然后再执行微任务。setTimeout 或者 ajax 这些浏览器 api 会在其他线程进行,当完成时会将回调任务插入 Event Loop 的调用栈,当下一次 Event Loop 被执行时,就会执行到该操作。
- 选择最先进入队列的宏任务执行(最开始是 script 整体代码)
- 检查是否存在微任务,如果存在,执行微任务队列中得所以任务,直至清空微任务队列
- 重复以上步骤
事件循环: 主线程从任务队列中不断的读取异步任务执行,不断循环重复的过程,就称为事件循环。
事件循环-举例理解
console.log(1); // 主-0
setTimeout(function () {
console.log(2); // 宏任务 -1
new Promise(function (resolve) {
console.log(3); // 微任务-2
resolve(4);
}).then(function (num) {
console.log(num); // 微任务-3
});
}, 300);
new Promise(function (resolve) {
console.log(5); // 微任务-1
resolve(6);
}).then(function (num) {
console.log(num); // 微任务-1
});
setTimeout(function () {
console.log(7); // 宏任务-2
}, 400);
// 输出 1 5 6 2 3 4 7
复制代码
// 微任务-2
var p = new Promise((resolve, reject) => {
console.log("Promise - 初始化");
resolve("Promise - 结果");
});
function fn1() {
console.log("fn1 - 执行");
}
function fn2() {
console.log("fn2 - 开始执行");
setTimeout(() => {
console.log("setTimeout - 执行");
});
fn1();
console.log("fn2 - 再次执行");
p.then((res) => {
console.log("Promise - 第一个then :" + res);
}).then(() => {
console.log("Promise - 第二个then");
});
}
fn2();
// 输出
("Promise - 初始化");
("fn2 - 开始执行");
("fn1 - 执行");
("fn2 - 再次执行");
("Promise - 第一个then :结果");
("Promise - 第二个then");
("setTimeout - 执行");
复制代码
var p = new Promise((resolve, reject) => {
console.log("Promise - 初始化");
resolve("Promise - 结果");
});
function fn1() {
console.log("fn1 - 执行");
}
function fn2() {
console.log("fn2 - 开始执行");
setTimeout(() => {
console.log("setTimeout - 执行");
// start
setTimeout(() => {
console.log("又一个宏任务");
});
p.then(() => {
console.log("Promise - 第三个then");
});
// end
});
fn1();
console.log("fn2 - 再次执行");
p.then((res) => {
console.log("Promise - 第一个then :" + res);
}).then(() => {
console.log("Promise - 第二个then");
});
}
fn2();
// 执行
("Promise - 初始化");
("fn2 - 开始执行");
("fn1 - 执行");
("fn2 - 再次执行");
("Promise - 第一个then :");
("Promise - 第二个then");
("setTimeout - 执行");
("Promise - 第三个then");
("又一个宏任务");
复制代码
总结
JS 本身并不慢,慢的是读写外部数据,比如等待 Ajax 请求返回结果。如果等着 Ajax 返回结果出来,再往下执行,就会耗费很长的时间。所以 JS 设计了一种机制,CPU 可以不管 IO 操作,而是挂起该任务,先执行后面的任务,等到 IO 操作返回了结果,再继续执行挂起的任务。
同步任务执行完后,引擎一遍又一遍检查那些挂起来的异步任务是否满足进入主线程的条件。这种循环检查的机制,就叫做事件循环机制。
JS 引擎运行时,除了一个正在运行的主线程,还提供一个或多个任务队列,里面是各种被挂起的异步任务。首先,主线程会去执行所有的同步任务,等到同步任务全部执行完,就会去看任务队列里面的异步任务,如果满足条件,那么异步任务就重新进入主线程开始执行,这时它就会变成同步任务。等到执行完,下一个异步任务再进入主线程开始执行。一旦任务队列清空,程序就结束执行。