標籤:article 延遲 使用 最小 解決 為什麼 targe script stack
栗子如下:
for (var i = 0; i < 5; i++) { setTimeout(function() { console.log(‘i: ‘,i); //一秒之後輸出幾乎沒有時間間隔依次輸出5個5 }, 1000);}console.log(i); //立即輸出5
想必很多人看到立馬能看出答案吧,但是為什麼定時器不能依次列印出1,2,3,4,5呢?答案稍後分曉。
那到底怎麼才能依次輸出我們想要的結果呢?大家可能都想到是利用閉包,或者是利ES6中的let聲明,但是今天我們不講這個。
一、為什麼js是單線程?
大家都知道js不同於其他語言,它是單線程的。那麼問題來了,為什麼不是多線程呢?按道理來說多線程不是能夠同時解決問題提高效率嗎?除了多線程產生衝突、搶佔資源等答案,還可以是什麼呢?
其實,這跟它作為瀏覽器指令碼語言的用途有關,瀏覽器的指令碼語言主要的用途是用來與使用者互動,會產生DOM的操作,這就是問題的關鍵,假設js是多線程,有一個線程是刪除DOM操作,有個在當前DOM新增內容,這時候瀏覽器應該怎麼辦呢?這就決定了js應該被設計成單線程。那js有沒有多線程的可能呢?答案是肯定的,HTML5提出的web Worker標準,但是,
“為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript指令碼建立多個線程,但是子線程完全受主線程式控制制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單線程的本質” ----阮一峰的一篇部落格。
二 、 Event Loop 事件迴圈
因為js是單線程,所以執行時候需要排隊,前一個任務結束之後,後一個任務才會執行,那麼如果前一個任務執行很久,後面一個任務就要等很久。如果一些等待不是因為CPU計算慢產生的,比如IO裝置的使用,那麼js會把等待的任務掛起,執行後面的任務,等IO返回結果,再回頭執行直線掛起的任務。
這樣任務就有了同步任務和非同步任務,同步任務是主線程上執行的任務,形成一個執行棧(execution context stack),是排隊進行的。非同步任務不是在主線程執行的,是在“任務隊列”(Queue)裡面,只有當主線程所有同步的任務執行完成,任務隊列中的非同步任務才會進入主線程,然後被執行。
非同步執行機制如下:---阮一峰
可視化描述---MDN
主線程上:
一個函數1被調用了,建立一個堆疊框架,包含了函數1的參數和局部變數,當函數1中又調用了函數2,又建立了一個堆疊框架,此時這個堆疊框架在第一個堆疊框架之前,包含了函數2的參數和局部變數。主線程先執行了至於頂層的堆疊框架(函數2產生的),當函數2返回時,對應的堆疊框架就出棧了,接著繼續執行函數1的堆疊框架,直到棧空了。
訊息佇列:
一個 js運行時包含了一個待處理的訊息佇列。每一個訊息都與一個函數相關聯。當棧為空白時,從隊列中取出一個訊息進行處理。這個處理過程包含了調用與這個訊息相關聯的函數(以及因而建立了一個初始堆疊框架)。當棧再次為空白的時候,也就意味著訊息處理結束。
添加訊息:
在瀏覽器裡,添加事件可以是當一個事件出現且有一個事件監聽器被綁定時,訊息被隨時添加。也可以是在調用setTimeout等函數時候,
在將來的某個時間後在訊息對列中添加。
setTimeout:
調用該函數時候會在將來的某個時間後在訊息對列中添加一個訊息,如果執行棧中有其他任務沒有完成(假設有一個很耗時的計算),setTimeout訊息必須必須等到執行棧的任務完成才會處理,所以說該函數的第二個參數僅僅表示最少的時間 而非確切的時間。
即使你設定零延遲:
setTimeout(function cb1() { console.log(‘我是第二個被執行的’); }, 0); console.log(‘我是第一個被執行的’); //先列印這句
事實上,js中規定,定時器的第二個參數設定最少不能小於4ms, 小於的話就按最小的4ms執行。
中,主線程啟動並執行時候,產生堆(heap)和棧(stack),棧中的代碼調用各種外部API,它們在"任
務隊列"中加入各種事件(click,load)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回呼函數。
三、回頭看看開頭的栗子
for迴圈和外面的console.log()是主線程上的同步任務,他們按循序執行,先執行for迴圈結束(此時i變為5),再執行console.log();
for迴圈中的setTimeout是在任務隊列中,它只有等在前主線程中的棧空了之後,到一個時間才會被被執行,此時範圍中的i已經變為5,此時setTimeout中的回呼函數所讀取的i就是5了。
還有一個問題?
輸出5的時間間隔是多少?答案顯而易見是,立即列印一個5,1000ms之後幾乎同時輸出5個5; why???
正如上面解釋的,for迴圈一次,會在任務隊列中加上一個setTimeou任務(該任務是在1000ms後執行回呼函數),這樣迴圈結束,工作清單裡面就有了5個setTimeou任務,且當主線程中棧空了之後,工作清單就開始進棧,等待1000ms之後執行回調,所以後面的5個5幾乎在一個點依次列印出來。
那麼js執行的順序就可以總結為:
主線程(同步任務) =》任務隊列(非同步) =》回調
從一個例子引發對JS運行機制之 Event Loop 的思考