setTimeout()
函數:用來指定某個函數或某段代碼在多少毫秒以後執行。它返回一個整數,表示定時器timer
的編號,能夠用來取消該定時器。javascript
console.log(1);
setTimeout(function () {
console.log(2);
}, 0);
console.log(3);
複製代碼
問:最後的打印順序是什麼?(若是不瞭解js的運行機制就會答錯)java
正確答案:1 3 2
面試
解析:不管setTimeout的執行時間是0仍是1000,結果都是先輸出3後輸出2,這就是面試官經常考查的js運行機制的問題,接下來咱們要引入一個概念,JavaScript 是單線程的。ajax
JavasScript引擎是基於事件驅動和單線程執行的,JS引擎一直等待着任務隊列中任務的到來,而後加以處理,瀏覽器不管何時都只有一個JS線程在運行程序,即主線程。瀏覽器
通俗的說:JS在同一時間內只能作一件事,這也常被稱爲 「阻塞式執行」。閉包
那麼單線程的JavasScript是怎麼實現「非阻塞執行」呢?異步
答:異步容易實現非阻塞,因此在JavaScript中對於耗時的操做或者時間不肯定的操做,使用異步就成了必然的選擇。async
諸如事件點擊觸發回調函數、ajax通訊、計時器這種異步處理是如何實現的呢?函數
答:任務隊列oop
全部任務能夠分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)。
任務隊列:一個先進先出的隊列,它裏面存放着各類事件和任務。
同步任務:在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。
console.log()
除此以外,任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在ES5標準中,它們被分別稱爲task與job。
一次事件循環中,先執行宏任務隊列裏的一個任務,再把微任務隊列裏的全部任務執行完畢,再去宏任務隊列取下一個宏任務執行。
注:在當前的微任務沒有執行完成時,是不會執行下一個宏任務的。
setTimeout 和 setInterval的運行機制是將指定的代碼移出本次執行,等到下一輪 Event Loop 時,再檢查是否到了指定時間。若是到了,就執行對應的代碼;若是不到,就等到再下一輪 Event Loop 時從新判斷。
這意味着,setTimeout指定的代碼,必須等到本次執行的全部同步代碼都執行完,纔會執行。
優先關係:異步任務要掛起,先執行同步任務,同步任務執行完畢纔會響應異步任務。
console.log('A');
setTimeout(function () {
console.log('B');
}, 0);
while (1) {}
複製代碼
你們再猜一下這段程序輸出的結果會是什麼?
答:A
注:建議先註釋掉while循環代碼塊的代碼,執行後強制刪除進程,否則會形成「假死」。
同步隊列輸出A
以後,陷入while(true){}
的死循環中,異步任務不會被執行。
相似的,有時addEventListener()
方法監聽點擊事件click
,用戶點了某個按鈕會卡死,就是由於當前JS正在處理同步隊列,沒法將click
觸發事件放入執行棧,不會執行,出現「假死」。
for (var i = 0; i < 4; i++) {
setTimeout(function () {
console.log(i);
}, 1000);
}
複製代碼
輸出結果爲,隔1s後一塊兒輸出:4 4 4 4
for循環是一個同步任務,爲何連續輸出四個4?
答:由於有隊列插入的時間,即便執行時間從1000改爲0,仍是輸出四個4。
那麼這個問題是如何產生和解決的呢?請接着閱讀
執行到異步任務的時候,會直接放到異步隊列中嗎? 答案是不必定的。
由於瀏覽器有個定時器(timer)模塊,定時器到了執行時間纔會把異步任務放到異步隊列。
for循環體執行的過程當中並無把setTimeout放到異步隊列中,只是交給定時器模塊了。4個循環體執行速度很是快(不到1毫秒)。定時器到了設置的時間纔會把setTimeout語句放到異步隊列中。
即便setTimeout設置的執行時間爲0毫秒,也按4毫秒算。
這就解釋了上題爲何會連續輸出四個4的緣由。
HTML5 標準規定了
setTimeout()
的第二個參數的最小值,即最短間隔,不得低於4毫秒。若是低於這個值,就會自動增長。在此以前,老版本的瀏覽器都將最短間隔設爲10毫秒。
for (let i = 0; i < 4; i++) {
(function (j) {
setTimeout(function () {
console.log(j);
}, 1000 * i)
})(i);
}
複製代碼
執行後,會隔1s輸出一個值,分別是:0 1 2 3
IIFE
聲明即執行的函數表達式來解決閉包形成的問題。這裏也能夠用setInterval()
方法來實現間歇調用。
var output = function (i) {
setTimeout(function () {
console.log(i);
}, 1000 * i)
}
for (let i = 0; i < 4; i++) {
output(i);
}
複製代碼
執行後,會隔1s輸出一個值,分別是:0 1 2 3
實現原理:傳過去的i
值被複制了。
const tasks = [];
const output = (i) => new Promise((resolve) => {
setTimeout(() => {
console.log(i);
resolve();
}, 1000 * i);
});
//生成所有的異步操做
for (var i = 0; i < 5; i++) {
tasks.push(output(i));
}
//同步操做完成後,輸出最後的i
Promise.all(tasks).then(() => {
setTimeout(() => {
console.log(i);
}, 1000)
})
複製代碼
執行後,會隔1s輸出一個值,分別是:0 1 2 3 4 5
優勢:提升了代碼的可讀性。
注意:若是沒有處理Promise的reject,會致使錯誤被丟進黑洞。
const sleep = (timeountMS) => new Promise((resolve) => {
setTimeout(resolve, timeountMS);
});
(async () => { //聲明即執行的async
for (var i = 0; i < 5; i++) {
await sleep(1000);
console.log(i);
}
await sleep(1000);
console.log(i);
})();
複製代碼
執行後,會隔1s輸出一個值,分別是:0 1 2 3 4 5
主線程從任務隊列中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop。
有時候 setTimeout明明寫的延時3秒,實際卻5,6秒才執行函數,這又是由於什麼?
答:setTimeout 並不能保證執行的時間,是否及時執行取決於 JavaScript 線程是擁擠仍是空閒。
瀏覽器的JS引擎遇到setTimeout,拿走以後不會當即放入異步隊列,同步任務執行以後,timer模塊會到設置時間以後放到異步隊列中。js引擎發現同步隊列中沒有要執行的東西了,即運行棧空了就從異步隊列中讀取,而後放到運行棧中執行。因此setTimeout可能會多了等待線程的時間。
這時setTimeout函數體就變成了運行棧中的執行任務,運行棧空了,再監聽異步隊列中有沒有要執行的任務,若是有就繼續執行,如此循環,就叫Event Loop。
JavaScript經過事件循環和瀏覽器各線程協調共同實現異步。同步能夠保證順序一致,可是容易致使阻塞;異步能夠解決阻塞問題,可是會改變順序性。
知識點梳理:
最後,但願你們閱後有所收穫。🤠