首先來一段程式碼開篇
console.log(1);
setTimeout(function() {
console.log(2);
});
function fn() {
console.log(3);
setTimeout(function() {
console.log(4);
}, 2000);
}
new Promise(function(resolve, reject){
console.log(5);
resolve();
console.log(6);
}).then(function() {
console.log(7);
})
fn();
console.log(8);
複製程式碼
思考一下,能給出準確的輸出順序嗎?
下面一步步的瞭解,最後看看這塊程式碼怎麼去執行的。
1.程式,單執行緒與多執行緒
程式: 執行的程式就是一個程式,比如你正在執行的瀏覽器,它會有一個程式。
執行緒: 程式中獨立執行的程式碼段。
一個程式由單個或多個執行緒組成,執行緒是負責執行程式碼的。
學過JS的想必都知道JS是單執行緒的,那麼既然有單執行緒就有多執行緒,下面首先看看單執行緒與多執行緒的區別。
-
單執行緒 從頭執行到尾,一行一行執行,如果其中一行程式碼報錯,那麼剩下程式碼將不再執行。同時容易程式碼阻塞。
-
多執行緒 程式碼執行的環境不同,各執行緒獨立,互不影響,避免阻塞。
2. Event Loop(瀏覽器)
js既然是單執行緒,那麼肯定是排隊執行程式碼,那麼怎麼去排這個隊,就是Event Loop。雖然JS是單執行緒,但瀏覽器不是單執行緒。瀏覽器中分為以下幾個執行緒:
- js執行緒
- UI執行緒
- 事件執行緒(onclick,onchange,...)
- 定時器執行緒(setTimeout, setInterval)
- 非同步http執行緒(ajax)
其中JS執行緒和UI執行緒相互互斥,也就是說,當UI執行緒在渲染的時候,JS執行緒會掛起,等待UI執行緒完成,再執行JS執行緒
-
JS會存在執行棧,從上至下執行js程式碼,當遇到非同步api時,列如上面所述的各種非JS執行緒的事件,那麼會扔給對應的執行緒去處理,等處理完畢後,則把回撥函式放入事件佇列中,等待執行棧執行完畢,再去讀取事件佇列中的回撥函式執行。
- 當一個函式執行,會產生一個新的執行棧,當執行完畢返回上一層執行棧,直到回到全域性執行棧
- 當一個函式呼叫自己,會產生一個新的執行棧。
整個過程,執行棧,讀取事件佇列就是Event Loop
-
再來看看promise, 如果對promise不是很瞭解的同學可以看看另一篇我寫的文章Promise是個什麼鬼?實現一個Promise.
Promise在整個執行中是個特殊的存在,傳入Promise的fn是在當前執行棧中的,會立即執行,但它的then方法是在執行棧之後,事件佇列之前,當然這個和瀏覽器實現有關,大部分瀏覽器是微任務(Microtask),也有瀏覽器放入了巨集任務(Macrotask),chorme大哥是放入了微任務,其他紛紛效仿。那大家可能會問什麼是微任務?什麼是巨集任務了?
- 巨集任務(Macrotask) 也就是上面所說的 事件佇列 callback queue
- 微任務(microtask) 是在執行棧和事件佇列之間 在執行棧之後先清空在微任務中的任務,再去執行事件佇列
3. Node Event Loop
Nodejs是通過V8引擎去解析的,解析後的程式碼會去呼叫node提供的api執行,這些API由libuv這個庫去分配執行緒執行,最後非同步返回給V8引擎。
在Node中提供了2個方法和我們的執行佇列有關
- process.nextTick
把方法放入執行棧的底部,並不放入巨集任務和微任務
cosnole.log(1);
process.nextTick(function(){
console.log(2);
});
new Promise(function() {
console.log(3);
}).then(function() {
console.log(4);
})
console.log(5);
複製程式碼
因為nextTick是放入了執行棧的底部,那麼會優先於Promise的then方法,故輸出為1 3 5 2 4
- setImmediate
把方法放入巨集任務的佇列中去,但有一個奇怪的事發生,看下面程式碼:
setImmediate(function() {
console,log(1);
});
setTimeout(function() {
console.log(2);
}, 0);
複製程式碼
大家可以試試把程式碼多次執行,發現輸出順序不一定,他們都是放入了巨集任務中,但在node文件中,setImmediate總是排在setTimeout前面,但是在實際中確不一定,不知道是不是一個bug。
4. 講講setTimeout, setInterval
- 任務佇列與定時器 上面講到了定時器都是放入了巨集任務。如果當前執行棧消耗時間已經大於我們設定的定時器時間,那麼定時器的回撥在巨集任務裡,並沒有及時去呼叫,所有這個時間不是特別準確。
setTimeout(function(){
console.log(1);
}, 2000);
task();
複製程式碼
假設task函式執行需要5秒鐘,那麼列印1需要在5秒之後再列印,task佔用了當前執行棧,要等執行棧執行完畢後再去讀取微任務,等微任務完成,這個時候才會去讀取巨集任務裡面的setTimeout回撥函式執行。setInterval同理,例如每3秒放入巨集任務,也要等到執行棧的完成。
- 定時器自身 有時候為了延後執行程式碼會寫:
setTimeout(function() {
console.log(1);
},0);
複製程式碼
但是根據標準這個時候最低是4毫秒,即便現在執行棧已經完成。0是不成立的。寫0瀏覽器為預設為最低毫秒數。
5. 回到開篇的程式碼
現在再回到上面的程式碼,有答案了嗎?
// 非非同步api,立即執行
console.log(1);
// 放入全域性巨集任務
setTimeout(function() {
console.log(2);
});
// 宣告函式,但暫時未呼叫,不會立馬形成執行棧
function fn() {
// 呼叫fn時立即執行
console.log(3);
// 放入當前fn執行棧巨集任務
setTimeout(function() {
console.log(4);
}, 2000);
}
new Promise(function(resolve, reject){
// task任務立即執行
console.log(5);
resolve();
console.log(6);
}).then(function() {
// then方法放入微任務
console.log(7);
})
// 呼叫fn進入下個執行棧
fn();
// fn執行棧完成執行
console.log(8);
複製程式碼
答案就是 1 5 6 3 8 7 2 4