家喻户晓,JS 是一门单线程语言,可是浏览器又能很好的解决异步申请,那么到底是为什么呢?
JS 的执行环境个别是浏览器和 Node.js,两者稍有不同,这里只探讨浏览器环境下的状况。
JS 执行过程中会产生两种工作,别离是:同步工作和异步工作。
- 同步工作:比方申明语句、for、赋值等,读取后根据从上到下从左到右,立刻执行。
- 异步工作:比方 ajax 网络申请,setTimeout 定时函数等都属于异步工作。异步工作会通过工作队列 (Event Queue) 的机制(先进先出的机制)来进行协调。
工作队列(Event Queue)
工作队列中的工作也分为两种,别离是:宏工作(Macro-take)和微工作(Micro-take)
- 宏工作次要包含:scrip(JS 整体代码)、setTimeout、setInterval、setImmediate、I/O、UI 交互
- 微工作次要包含:Promise(重点关注)、process.nextTick(Node.js)、MutaionObserver
工作队列的执行过程是:先执行一个宏工作 ,执行过程中如果产出新的宏 / 微工作,就将他们推入相应的工作队列, 之后在执行一队微工作 ,之后再执行宏工作,如此循环。 以上一直反复的过程就叫做 Event Loop(事件循环)。
每一次的循环操作被称为tick。
了解微工作和宏工作的执行执行过程
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>
中的代码):- 输入:
script start
- 遇到 setTimeout,退出宏工作队列,以后宏工作队列(setTimeout)
- 遇到 promise,退出微工作,以后微工作队列(promise1)
- 输入:
script end
- 输入:
-
微工作:执行微工作队列(promise1)
- 输入:
promise1
,then 之后产生一个微工作,退出微工作队列,以后微工作队列(promise2) - 执行 then,输入
promise2
- 输入:
- 执行渲染操作,更新界面(敲黑板划重点)。
-
宏工作:执行 setTimeout
- 输入:
setTimeout
- 输入:
Promise 的执行
new Promise(..)
中的代码,也是同步代码,会立刻执行。只有 then
之后的代码,才是异步执行的代码,是一个微工作。
console.log("script start");
setTimeout(function () {console.log("timeout1");
}, 10);
new Promise((resolve) => {console.log("promise1");
resolve();
setTimeout(() => console.log("timeout2"), 10);
}).then(function () {console.log("then1");
});
console.log("script end");
步骤解析:
- 当前任务队列:微工作: [], 宏工作:[
<script>
]
-
宏工作:
- 输入:
script start
- 遇到 timeout1,退出宏工作
- 遇到 Promise,输入
promise1
,间接 resolve,将 then 退出微工作,遇到 timeout2,退出宏工作。 - 输入
script end
- 宏工作第一个执行完结
- 输入:
- 当前任务队列:微工作[then1],宏工作[timeou1, timeout2]
-
微工作:
- 执行 then1,输入
then1
- 微工作队列清空
- 执行 then1,输入
- 当前任务队列:微工作[],宏工作[timeou1, timeout2]
-
宏工作:
- 输入
timeout1
- 输入
timeout2
- 输入
- 当前任务队列:微工作[],宏工作[timeou2]
-
微工作:
- 为空跳过
- 当前任务队列:微工作[],宏工作[timeou2]
-
宏工作:
- 输入
timeout2
- 输入
async/await 的执行
async 和 await 其实就是 Generator 和 Promise 的语法糖。
async 函数和一般 函数没有什么不同,他只是示意这个函数里有异步操作的办法,并返回一个 Promise 对象
翻译过去其实就是:
// async/await 写法
async function async1() {console.log("async1 start");
await async2();
console.log("async1 end");
}
// Promise 写法
async function async1() {console.log("async1 start");
Promise.resolve(async2()).then(() => console.log("async1 end"));
}
看例子:
async function async1() {console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {console.log("async2");
}
async1();
setTimeout(() => {console.log("timeout");
}, 0);
new Promise(function (resolve) {console.log("promise1");
resolve();}).then(function () {console.log("promise2");
});
console.log("script end");
步骤解析:
- 当前任务队列:宏工作:[
<script>
],微工作: []
-
宏工作:
- 输入:
async1 start
- 遇到 async2,输入:
async2
,并将 then(async1 end)退出微工作 - 遇到 setTimeout,退出宏工作。
- 遇到 Promise,输入:
promise1
,间接 resolve,将 then(promise2)退出微工作 - 输入:
script end
- 输入:
- 当前任务队列:微工作[promise2, async1 end],宏工作[timeout]
-
微工作:
- 输入:
promise2
- promise2 出队
- 输入:
async1 end
- async1 end 出队
- 微工作队列清空
- 输入:
- 当前任务队列:微工作[],宏工作[timeout]
-
宏工作:
- 输入:
timeout
- timeout 出队,宏工作清空
- 输入:
“ 工作队列 ” 是一个事件的队列(也能够了解成音讯的队列),IO 设施实现一项工作,就在 ” 工作队列 ” 中增加一个事件,示意相干的异步工作能够进入 ” 执行栈 ” 了。主线程读取 ” 工作队列 ”,就是读取外面有哪些事件。
“ 工作队列 ” 中的事件,除了 IO 设施的事件以外,还包含一些用户产生的事件(比方鼠标点击、页面滚动等等)。只有指定过回调函数,这些事件产生时就会进入 ” 工作队列 ”,期待主线程读取。
所谓 ” 回调函数 ”(callback),就是那些会被主线程挂起来的代码。异步工作必须指定回调函数,当主线程开始执行异步工作,就是执行对应的回调函数。
“ 工作队列 ” 是一个先进先出的数据结构,排在后面的事件,优先被主线程读取。主线程的读取过程基本上是主动的,只有执行栈一清空,” 工作队列 ” 上第一位的事件就主动进入主线程。然而,因为存在后文提到的 ” 定时器 ” 性能,主线程首先要检查一下执行工夫,某些事件只有到了规定的工夫,能力返回主线程。
—-JavaScript 中没有任何代码时立刻执行的,都是过程闲暇时尽快执行
setTimerout 并不精确
由上咱们曾经晓得了 setTimeout 是一个宏工作,会被增加到宏工作队列当中去,按程序执行,如果后面有。
setTimeout() 的第二个参数是为了通知 JavaScript 再过多长时间把当前任务增加到队列中。
如果队列是空的,那么增加的代码会立刻执行;如果队列不是空的,那么它就要等后面的代码执行完了当前再执行。
看代码:
const s = new Date().getSeconds();
console.log("script start");
new Promise((resolve) => {console.log("promise");
resolve();}).then(() => {console.log("then1");
while (true) {if (new Date().getSeconds() - s >= 4) {console.log("while");
break;
}
}
});
setTimeout(() => {console.log("timeout");
}, 2000);
console.log("script end");
因为 then 是一个微工作,会先于 setTimeout 执行,所以,尽管 setTimeout 是在两秒后退出的宏工作,然而因为 then 中的在 while 操作被提早了 4s,所以始终推延到了 4s 秒后才执行的 setTimeout。
所以输入的程序是:script start、promise、script end、then1。
四秒后输入:while、timeout
留神:对于 setTimeout 要补充的是,即使主线程为空,0 毫秒实际上也是达不到的。依据 HTML 的规范,最低是 4 毫秒。有趣味的同学能够自行理解。
<!– ### 异步渲染策略 –>
<!– 以 Vue 为例 nextTick –>
总结
有个小 tip:从标准来看,microtask 优先于 task 执行,所以如果有须要优先执行的逻辑,放入 microtask 队列会比 task 更早的被执行。
最初的最初,记住,JavaScript 是一门单线程语言,异步操作都是放到事件循环队列外面,期待主执行栈来执行的,并没有专门的异步执行线程。
参考
- 知乎 -【JS】深刻了解事件循环, 这一篇就够了!(必看)
- 掘金小册 - 前端性能优化 -Event Loop 与异步更新策略
- Segmentfault- 译文:JS 事件循环机制(event loop)之宏工作、微工作
- 这一次,彻底弄懂 JavaScript 执行机制
- 面试肯定会问到的 -js 事件循环