本博文基於知乎"JavaScript作用域問題?"一問,而引起了對JavaScript事件循環和單線程等概念與實踐上的研究、深入理解。
一、概念
0.關鍵詞:JavaScript單線程、事件循環(event loop)、事件隊列(event queue)、執行棧(execution context stack)
1.JavaScript引擎屬於單線程作業。所謂單線程,是指在JS引擎中負責解釋和執行JavaScript代碼的線程只有一個,也不妨叫它主線程。JavaScript引擎屬於單線程作業,意味着:在同一時間只能執行一個代碼塊,這些代碼塊的執行就阻塞了異步事件的處理。[From JavaScript忍者秘籍]
JavaScript的單線程,與它的用途有關。作為瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很復雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程為准?
1.1 單線程意味着,【所有任務】都需要排隊,前一個任務結束,才會執行后一個任務。如果前一個任務耗時很長,后一個任務就不得不一直等着。
1.2 如果排隊是因為計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閑着的,因為IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等着結果出來,再往下執行。
1.3 JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處於等待中的任務,先運行排在后面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。
1.4 於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。
1.5 同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務;
1.6 異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務才會進入主線程執行。
1.7 具體來說,異步執行的運行機制如下。(同步執行也是如此,因為它可以被視為沒有異步任務的異步執行。)
(1)所有同步任務都在主線程上執行,形成一個執行棧(execution context stack)。
(2)主線程之外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
(3)一旦"執行棧"中的所有同步任務執行完畢,系統就會讀取"任務隊列",看看里面有哪些事件。那些對應的異步任務,於是結束等待狀態,進入執行棧,開始執行。
(4)主線程不斷重復上面的第三步。
1.8 只要主線程空了,就會去讀取"任務隊列",這就是JavaScript的運行機制。這個過程會不斷重復。
[From 阮一峰老師:http://www.ruanyifeng.com/blog/2014/10/event-loop.html]
2.事件循環(用於解決:異步問題/異步事件):在初期許多人會把異步理解成類似多線程的編程模式,其實他們中有着很大的差別,要完全理解異步,就需要了解 JS 的運行核心——事件循環(event loop)。
2.1 事件循環:【事件隊列】是一個存儲着待執行任務的隊列,其中的任務嚴格按照時間先后順序執行,排在隊頭的任務將會率先執行,而排在隊尾的任務會最后執行。事件隊列每次僅執行一個任務,在該任務執行完畢之后,再執行下一個任務。【執行棧】則是一個類似於函數調用棧的運行容器,當執行棧為空時,JavaScript引擎便檢查事件隊列,如果不為空的話,事件隊列便將第一個任務壓入中運行。
[From http://www.php.cn/js-tutorial-369771.html]
2.2 常見異步任務:定時器任務(setTimeout();setInterval();)、Ajax事件、瀏覽器/用戶行為事件(例如:瀏覽器加載(load)、鼠標單擊click、鼠標滑動/滑過/離開(mouseover、mouseout、mouseleave等)
二、分析
由於JavaScript是單線程作業,當一個異步事件發生時(比如:鼠標單擊、定時器觸發甚至是XMLHttpRequest的完成事件),它就會排隊,並且在線程空閑時才進行執行。且實際上,每個瀏覽器的排隊機制是不同的。當我們設置一個延遲函數的時候,當前腳本並不會阻塞,它只是會在瀏覽器的事件表中進行記錄,程序會繼續向下執行。當延遲的時間結束之后,事件表會將回調函數添加至事件隊列(task queue)中,事件隊列拿到了任務過后便將任務壓入執行棧(stack)當中,執行棧執行任務,執行console.log("after 1000 mills:",i);
for(var i = 0; i < 10; i++) { console.log("cur:",i); setTimeout(function() { console.log("after 1000 mills:",i); //當 console.log 被調用的時候,匿名函數保持對外部變量 i 的引用,此時for循環已經結束, i 的值被修改成了 10. }, 1000); }
三、解決方案
即時函數
格式:(function(){ //...statement })();
for(var i = 0; i < 10; i++) { console.log("cur:",i), (function(i){ setTimeout(function() { console.log("after 1000 mills:",i); }, 1000); })(i);//通過即時函數(1.創建函數實例,2.執行該函數,3.銷毀該函數),將循環體異步事件壓入執行棧中,立即執行的特性,以維護好變量當前的值 }
四、引用文獻
《JavaScript忍者秘籍》