一. JavaScript是單執行緒的
為什麼呢 ? 首先JavaScript語言的一大特點就是單執行緒, 通俗點說就是, 同一個時間只能做一件事.那麼會又有新的問題, JavaScript為什麼不能有多個執行緒呢 ?
JavaScript最初被設計用在瀏覽器中, 那麼設想下, 瀏覽器中的JavaScript是多執行緒的.例如 : 假定JavaScript同時有兩個執行緒, 一個執行緒在某個DOM節點上新增內容, 另外一個執行緒刪除了這個節點, 這時瀏覽器應該以哪個執行緒為準呢 ?
後來, HTML5提出Web Worker標準, 允許JavaScript指令碼建立多個執行緒, 但是子執行緒完全受主執行緒控制, 且不得操作DOM, 所以, 這個新標準並沒有改變JavaScript單執行緒本質
二. JavaScript為什麼需要非同步 ?
如果JavaScript中不存在非同步, 只能自上而下執行, 如果上一行解析時間很長, 那麼下面的程式碼就會被阻塞.對於使用者而言, 阻塞就意味著 "卡死", 這樣就導致了很差的使用者體驗.所以, JavaScript中存在非同步執行
三. 那麼又是如何實現非同步的呢 ?
任務佇列 :1. 所有同步任務都在主執行緒上執行, 形成一個執行棧(stack)。2.主執行緒之外, 還存在一個任務佇列Event Loop, 非同步任務在event table中註冊函式, 當滿足觸發條件(即DOM,AJAX,setTimeout,setImmediate有返回結果了) 後, 被推入任務佇列(Event Loop)。3. 一旦執行棧(stack) 中所有同步任務都執行完了, 系統就會讀取任務佇列(Event Loop), 看看裡面有哪些事件.那些對應的非同步任務, 於是結束等待狀態, 進入執行棧, 開始執行 。4.主執行緒不斷重複上面的第三步。
例子1:
console.log(1)
setTimeout(
function() {
console.log(2)
},
0)
console.log(3)
複製程式碼
執行的結果是:1,3,2
程式碼分析:
1.console.log(1)是同步任務,放入主執行緒裡
2.setTimeout是非同步任務,被放入event table,0秒之後被推入任務佇列(Event Loop)裡
3.console.log(3)是同步任務,放到主執行緒裡.
當1,3在控制檯被列印後,主執行緒去Event Loop(事件佇列)裡檢視是否有可執行的函式,執行setTimeout裡的函式,這就是Event Loop
四.Event Loop是什麼 ?
主執行緒從任務佇列(Event Loop) 中讀取事件, 這個過程是迴圈不斷的, 所以整個的這種執行機制又稱為Event Loop(事件迴圈).
上圖中, 主執行緒執行的時候, 產生堆(heap) 和棧(stack), 棧中的程式碼呼叫各種外部API, 它們在” 任務佇列(Event Loop)” 中加入各種事件( click, load, done)。 只要棧中的程式碼執行完畢, 主執行緒就會去讀取” 任務佇列(Event Loop)”, 依次執行那些事件所對應的回撥函式。 例子2:
setTimeout(function() {
console.log('定時器開始啦')
});
new Promise(function(resolve) {
console.log('馬上執行for迴圈啦');
for(var i =0; i <10000; i++) {
i ==99 &&resolve();
}
}).then(function() {
console.log('執行then函式啦')
});
console.log('程式碼執行結束');
複製程式碼
嘗試按照,上文我們剛學到的js執行機制去分析:
1.setTimeout 是非同步任務,被放到event table
2.new Promise是同步任務,被放到主執行緒裡,直接執行列印console.log('馬上執行for迴圈啦');
3..then裡的函式是非同步任務,被放到event table
4.console.log('程式碼執行結束');是同步程式碼,被放到主執行緒裡,直接執行
所以根據分析的結果是:馬上執行for迴圈啦---程式碼執行結束---定時器開始啦---執行then函式啦
自己執行了下程式碼後,結果居然不是這樣的,而是:
馬上執行for迴圈啦---程式碼執行結束---執行then函式啦---定時器開始啦
事實上,按照非同步和同步的方式來劃分,並不準確,而準確的劃分方式是: macro-task(巨集任務):script(整體程式碼), setTimeout, setInterval, setImmediate, I/O, UI rendering。 micro-task(微任務):process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(html5新特性)
按照這種分類方式,js的執行機制是:
1.執行一個巨集任務,過程中如果遇到微任務,就將其放到微任務的"事件佇列"裡
2.當前巨集任務執行完成後,會檢視微任務的"事件佇列",並將裡面全部的微任務依次執行完
3.重複以上2步驟,結合圖1和圖2就是更為準確的js執行機制了
那麼,去分析例2:
1.首先執行script下的巨集任務,遇到setTimeout,將其放到巨集任務的“佇列”裡
2.遇到 new Promise直接執行,列印"馬上執行for迴圈啦"
3.遇到then方法,是微任務,將其放到微任務的“佇列”裡。
4.遇到console.log('程式碼執行結束');是同步任務,直接列印"程式碼執行結束"
5.本輪巨集任務執行完畢,檢視本輪的微任務,發現有一個then方法裡的函式,列印"執行then函式啦"
6.到此,本輪的event loop 全部完成。
7.下一輪的迴圈裡,先執行一個巨集任務,發現巨集任務的“佇列”裡有一個setTimeout裡的函式,執行列印"定時器開始啦"
所以最後的執行順序是: 馬上執行for迴圈啦---程式碼執行結束---執行then函式啦---定時器開始啦
五.定時器setTimeout()和setInterval()
定時器指定某些程式碼在多少時間之後執行這叫做”定時器”(timer)功能,也就是定時執行的程式碼。
例子3:
setTimeout(function(){
console.log('執行了')
},3000)
複製程式碼
我們一般會說:3秒後,會執行setTimeout裡的那個函式,但是這種說法並不嚴謹,準確的解釋是:3秒後,setTimeout裡的函式會被推入事件佇列(Event Loop),而事件佇列(Event Loop)裡的任務,只有在主執行緒空閒時才會執行,所以條件只有同時滿足(ps:3秒後並且主執行緒空閒)時,才會3秒後執行函式
如果主執行緒執行內容很多,執行時間超過3秒,比如主執行緒裡執行棧執行了10秒,那麼這個函式只能10秒後執行了
六.Node.js的Event Loop
Node.js也是單執行緒的Event Loop,但是它的執行機制不同於瀏覽器環境。
如圖所示,Node.js的執行機制如下:
1.V8引擎解析JavaScript指令碼。
2.解析後的程式碼,呼叫Node API。
3.libuv庫負責Node API的執行。它將不同的任務分配給不同的執行緒,形成一個Event
Loop(事件迴圈),以非同步的方式將任務的執行結果返回給V8引擎。
4.V8引擎再將結果返回給使用者。
除了setTimeout和setInterval這兩個方法,Node.js還提供了另外兩個與”任務佇列”有關的方法:process.nextTick和setImmediate。它們可以幫助我們加深對”任務佇列”的理解。 nextTick setImmediate 區別和聯絡 nextTick :把回撥函式放在當前執行棧的底部,而多個process.nextTick語句總是在當前”執行棧”一次執行完 setImmediate :把回撥函式放在事件佇列(event loop)的尾部,而多個setImmediate可能則需要多次loop才能執行完
例子4:
process.nextTick(function A() {
console.log(1);
process.nextTick(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0)
複製程式碼
上面程式碼中,由於process.nextTick方法指定的回撥函式,總是在當前”執行棧”的尾部觸發,所以不僅回撥函式A比setTimeout指定的回撥函式timeout先執行,而且函式B也比timeout先執行。這說明,如果有多個process.nextTick語句(不管它們是否巢狀),將全部在當前”執行棧”執行。所以結果是:1,2,'TIMEOUT FIRED'
現在,再來看看setImmediate 例子5:
setImmediate(function A() {
console.log(1);
setImmediate(function B(){
console.log(2);
});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
複製程式碼
上面程式碼中,setImmediate與setTimeout(fn,0)各自新增了一個回撥函式A和timeout,都是在下一次事件佇列(Event Loop)觸發。那麼,哪個回撥函式先執行呢?答案是不確定。執行結果可能是1,TIMEOUT FIRED,2,也可能是TIMEOUT FIRED,1,2。
令人困惑的是,Node.js文件中稱,setImmediate指定的回撥函式,總是排在setTimeout前面。實際上,這種情況只發生在遞迴呼叫的時候。
例子6:
setImmediate(function (){
setImmediate(function A() {
console.log(1);
setImmediate(function B(){console.log(2);});
});
setTimeout(function timeout() {
console.log('TIMEOUT FIRED');
}, 0);
});
複製程式碼
上面程式碼中,setImmediate和setTimeout被封裝在一個setImmediate裡面,它的執行結果總是1,TIMEOUT FIRED,2,這時函式A一定在timeout前面觸發。至於2排在TIMEOUT FIRED的後面(即函式B在timeout後面觸發),是因為setImmediate總是將事件註冊到下一輪事件佇列(Event Loop),所以函式A和timeout是在同一輪Loop執行,而函式B在下一輪Loop執行
另外,由於process.nextTick指定的回撥函式是在本次”事件迴圈”觸發,而setImmediate指定的是在下次”事件迴圈”觸發,所以很顯然,前者總是比後者發生得早,而且執行效率也高(因為不用檢查”任務佇列”)。