前言
JS 的执行机制、event loop、JS 异步执行原理,其实都是同一个东西,在面试时很容易被问到,今天笔者就跟各位聊聊这个问题。
前置知识
先说点大家都知道的:
- Javascript是一门单线程的语言,为啥是单线程,不能是多线程?(假如是多线程的,两个线程在同一时间操作了 DOM,到底以谁为准呢?这门语言的用途决定了它只能是单线程)这么一来,那我们写的代码执行就得有顺序了,是需要排队的,一排队呢就发现有些动作会阻塞在那里,要等很久才能接着往下执行,JS 就发展出了 异步 这个概念。
- JavaScript引擎把我们写的代码当成一个个
任务排队执行。 任务可以分为同步任务和异步任务同步按你的代码顺序在主线程执行,异步不按照代码顺序执行,异步的执行效率更高。- 通俗的解释一下
异步:就是从主线程发射一个子线程来完成任务,子线程独立于主线程,所以即使出现阻塞也不会影响主线程的运行。但主线程无法确定它的结束,如果结束之后需要处理一些事情,比如处理来自服务器的信息,是无法将它合并到主线程中去的,为了解决这个问题,JavaScript 中的异步操作函数往往通过回调函数来实现异步任务的结果处理。 - 异步任务有哪些:接口请求,
setTimeout,setInterval,Promise.then,process.nextTick,异步的意思其实就是说你不知道什么时候会成功的任务,就比如请求接口,你不知道它啥时候会成功吧,你只知道将来可能会成功,然后进入回调执行逻辑,也可能失败,都不会执行回调的逻辑。有些掘友可能就会说了:
setTimeout(() => console.log(1), 1000)
这不是很明确吗,1秒钟过后会在控制台打印1
其实这并不是那么准确的1秒钟,其等待的时间取决于任务队列里待处理的任务数量。这里的1秒钟是运行时处理请求所需的最小等待时间,但并不保证是准确的等待时间。
基本上,setTimeout 需要等待当前队列中所有的消息都处理完毕之后才能执行,即使已经超出了由第二参数所指定的时间。
接下来看个例子。
思考题
const { log } = console;
log(1);
setTimeout(() => {
log(2);
});
new Promise((resolve, reject) => {
log(3);
resolve();
}).then(() => {
log(4);
});
各位掘友可以先自己思考下打印的顺序,再往下看自己做的对不对
解析
- 执行
const { log } = console;,把console.log的引用给log变量 - 打印
1 - 遇到异步任务
setTimeout, 把回调函数添加到异步队列等待调用 - 遇到
Promise,先打印3, 这个位置的代码还是同步执行的,调用resolve()后会执行.then,不过是异步执行,添加到异步任务队列 - 然后就没代码了,此时异步任务队列中有两个任务
[() => {log(2);}, () => {log(4);}],各位是不是觉得先进队列的先执行,先打印2, 再打印4,然而并不是,异步任务还可以再细分为微任务和宏任务 - 先说结论,先打印
4,最后打印2 - 正确的顺序:1 -> 3 -> 4 -> 2
- 你说你不信?那看图说话:
别告诉我连浏览器你都不信。好了说说微任务和宏任务
微任务有哪些
Promise.thenprocess.nextTick(node)Promise.catchPromise.finallyawait
宏任务有哪些
整体的JS代码setTimeoutsetInterval
JS 的事件循环(event loop)
概念
JavaScript 有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务 并发模型与事件循环 - JavaScript | MDN (mozilla.org)
图解事件循环
应该还算清晰明了吧,做个题试试?
复杂的思考题
setTimeout(() => {
console.log('set1 ')
new Promise((resolve, reject) => {
console.log('pr1 ')
resolve();
}).then(() => {
console.log('then1 ');
})
})
setTimeout(() => {
console.log('set2 ')
})
new Promise((resolve, reject) => {
console.log('pr2 ')
resolve();
}).then(() => {
console.log('then2 ');
})
new Promise((resolve, reject) => {
console.log('pr3 ')
setTimeout(() => {
console.log('set3 ');
})
resolve();
}).then(() => {
console.log('then3 ');
})
console.log(1);
正确的顺序:pr2 pr3 1 then2 then3 set1 pr1 then1 set2 set3
带上async
async function async1() {
console.log('async1 start')
async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
console.log('promisel')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
正确的顺序:script start -> async1 start -> async2 -> async1 end -> promisel -> script end -> promise2 -> setTimeout
带上async await
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
setTimeout(function () {
console.log('setTimeout')
}, 0)
async1()
new Promise(function (resolve) {
console.log('promisel')
resolve()
}).then(function () {
console.log('promise2')
})
console.log('script end')
正确的顺序:script start -> async1 start -> async2 -> promisel -> script end -> async1 end -> promise2 -> setTimeout
其实就当作把每个await后面的代码放到.then里执行就好
举个例子:
function fn1() {
return new Promise((resolve, reject) => {
resolve(1)
})
}
function fn2() {
return new Promise((resolve, reject) => {
resolve(2)
})
}
// 用async await语法糖
async function test() {
const res1 = await fn1()
console.log(res1) // 1
const res2 = await fn2()
console.log(res2) // 2
}
// 相当于
function test() {
const res1 = fn1()
res1.then(val1 => {
console.log(val1) // 1
const res2 = fn2()
res2.then(val2 => {
console.log(val2) // 2
})
})
}
Vue 的nextTick是微任务还是宏任务
当我们使用 Vue 提供的接口去更新数据时,这个更新并不会立即生效,而是会被推入到一个队列里。待到适当的时机,队列中的更新任务会被批量触发。这就是异步更新。异步更新可以帮助我们避免过度渲染页面以提升性能。
先说答案,Vue 的nextTick准确来说是优先微任务
那答案是怎么来的呢?源码看来的,Vue的nextTick的实现在 源码的src/core/util/next-tick.js中,大致逻辑如下图(感兴趣的掘友可自行下载源码阅读分析)
实现非常的清晰,
timerFunc就是用来异步执行回调的,如果支持Promise, 优先使用Promise,这就是答案的由来。
总结
- 同步的任务按顺序执行
- 异步任务分为
微任务和宏任务 - 微任务:
Promise.then,process.nextTick(node),Promise.catch,Promise.finally,await - 宏任务:
整体的JS代码,setTimeout,setInterval - 当宏任务执行完,会先执行微任务队列中的任务直到微任务队列为空,才会去执行宏任务队列中的宏任务
Vue的nextTick准确来说是优先微任务
相信看到这里,各位掘友已经理解了Javascript异步执行机制。
动动你发财的小手,点个赞吧 🌹🌹🌹