首先來一段代碼開篇
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線程的事件,那么會扔給對應的線程去處理,等處理完畢后,則把回調函數放入事件隊列中,等待執行棧執行完畢,再去讀取事件隊列中的回調函數執行。
[圖片上傳失敗...(image-fd34d8-1516456564422)]- 當一個函數執行,會產生一個新的執行棧,當執行完畢返回上一層執行棧,直到回到全局執行棧
- 當一個函數調用自己,會產生一個新的執行棧。
整個過程,執行棧,讀取事件隊列就是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