衆所周知,JS 是一門單線程語言,但是瀏覽器又能很好的處理異步請求,那麼究竟是爲何呢?前端
JS 的執行環境通常是瀏覽器和 Node.js,二者稍有不一樣,這裏只討論瀏覽器環境下的狀況。面試
JS 執行過程當中會產生兩種任務,分別是:同步任務和異步任務。ajax
任務隊列中的任務也分爲兩種,分別是:宏任務(Macro-take)和微任務(Micro-take)segmentfault
任務隊列的執行過程是:先執行一個宏任務,執行過程當中若是產出新的宏/微任務,就將他們推入相應的任務隊列,以後在執行一隊微任務,以後再執行宏任務,如此循環。以上不斷重複的過程就叫作 Event Loop(事件循環)。promise
每一次的循環操做被稱爲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
script end
微任務:執行微任務隊列(promise1)數據結構
promise1
,then 以後產生一個微任務,加入微任務隊列,當前微任務隊列(promise2)promise2
宏任務:執行 setTimeout異步
setTimeout
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
promise1
,直接 resolve,將 then 加入微任務,遇到 timeout2,加入宏任務。script end
微任務:
then1
宏任務:
timeout1
timeout2
微任務:
宏任務:
timeout2
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
,並將 then(async1 end)加入微任務promise1
,直接 resolve,將 then(promise2)加入微任務script end
微任務:
promise2
async1 end
宏任務:
timeout
"任務隊列"是一個事件的隊列(也能夠理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務能夠進入"執行棧"了。主線程讀取"任務隊列",就是讀取裏面有哪些事件。
"任務隊列"中的事件,除了IO設備的事件之外,還包括一些用戶產生的事件(好比鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列",等待主線程讀取。
所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。
"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。可是,因爲存在後文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。
----JavaScript中沒有任何代碼時當即執行的,都是進程空閒時儘快執行
由上咱們已經知道了 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 是一門單線程語言,異步操做都是放到事件循環隊列裏面,等待主執行棧來執行的,並無專門的異步執行線程。