javascript單線程,異步與執行機制

js的單線程模型與遊覽器的進程/線程息息相關,在瞭解js單線程與異步的時候,建議先看看這篇文章html

爲何是單線程

  • 因爲js是可操做dom的,若是js是多線程,在多線程的交互下,處於界面中的dom節點就可能成爲一個臨界資源。
  • 這個時候,若是兩個線程同時操做一個dom,一個負責修改,一個負責刪除,這時就會出現問題。
  • 雖然能夠經過鎖來解決上面的問題,但爲了不由於引入了鎖而帶來更大的複雜性,js在最初就選擇了單線程。

爲何須要異步

  • 因爲js是可操縱dom的,若是在修改這些dom的同時渲染界面(即js線程和gui線程同時運行),那麼渲染線程先後得到的元素數據就可能不一致了。
  • 爲了防止渲染出現不可預期的結果,瀏覽器將gui線程與js引擎線程設置爲互斥關係,當js引擎執行時,gui線程會被掛起,等到js引擎線程空閒時纔會被執行。
  • 因此,若是js執行時間過長(同步ajax),就會讓頁面卡死,形成渲染阻塞。所以,js的異步特性就顯得頗有必要了。

如何實現異步

  • 經過事件驅動機制,來實現異步任務等待,同步任務先執行。
  • 當js主線程執行完同步任務後,再自動去拿留待的異步任務去執行。

異步編程模型

  • 傳統異步回調的問題
    • 代碼可讀性
    • 流程控制
    • 異常和錯誤處理
  • 異步編程的變革
    • Promise
    • Generator
    • Async/await

執行機制

  • js執行涉及主線程和執行棧,全部的程序任務都會被放到執行棧中被主線程執行。
  • js執行採用後進先出的原則。當函數執行的時候,會被添加到棧的頂部;當執行棧執行完後,就會從棧頂被移出,直到棧內被清空。
  • 主線程執行,由js引擎線程負責;事件隊列,由事件觸發線程管理。

事件驅動機制

  • 事件驅動機制(event driven)經過事件隊列(event queue)和事件循環(event loop)來實現。
  • 事件隊列(event queue),也稱消息隊列/任務隊列,由異步I/O操做發起,裏面存放着各類事件消息,這些消息都關聯着回調函數。
  • 事件循環(event loop),是指js主線程重複從消息隊列中取消息、執行的過程。
  • 模型圖示

任務類型

  • 從執行時機的角度
    • 同步任務,存放在執行棧中,會被主線程依次執行的任務
    • 異步任務,存放在事件隊列中,會在異步操做有告終果後,將註冊回調放入這個隊列,等待主線程空閒時,被拉取到執行棧中執行。(空閒時,意味着同步任務已被執行完,執行棧爲空了)
  • 從提供者的角度
    • 宏任務(macrotask),由宿主環境提供——全局script,setTimeout,setInterval,setImmediate,I/O,UI rendering,postMessage,MessageChannel
    • 微任務(microtask),由語言標準提供——Promise.then,process.nextTick,Object.observe(已廢棄),MutationObserver

任務機制

  • 全部同步任務在執行完以前,任何的異步任務是不會執行的。
console.log("A");
setTimeout(function(){
   console.log("B");
},0);
while(true){}
// 結果爲A。由於同步任務被死循環卡住了,任務隊列裏的任務不會被主線程拉取進執行棧
  • 每執行一個宏任務後,就會執行全部微任務。

js_macrotask_microtask.png

  • 爲了使js任務與dom任務可以有序執行,會在一個task執行結束後,在下一個task執行開始前,對頁面進行從新渲染 (task(宏->微)->render->task(宏->微)-->...)

宏任務/微任務拓展

  • 1個事件循環中,宏任務能夠有多個,微任務只有1個。
  • 以銀行排號爲例,1個櫃檯對應多個用戶,每一個用戶都是1個宏任務,當用戶辦完(宏)主任務後,忽然想到要辦理不少(微)次任務,銀行櫃員會一次幫他解決全部需求,而不是讓他從新排隊
  • 程序模型圖示

  • 執行機制詳述
  1. 執行一個宏任務,主棧中沒有就從事件隊列中獲取。
  2. 執行過程當中若是遇到微任務,就將它添加到微任務的任務隊列中。
  3. 宏任務執行完畢後,當即依次執行當前微任務隊列中的全部微任務。
  4. 當微任務執行完畢,開始檢查渲染,而後gui線程接管渲染。
  5. 渲染完畢後,js線程繼續接管,開始下一個宏任務。
console.log('1');
setTimeout(function() {
    console.log('5');
    Promise.resolve().then(function() {
        console.log('6');
    })
}, 0);
Promise.resolve().then(function() {
    console.log('3');
}).then(function() {
    console.log('4');
});
console.log('2');

// 1,2,3,4,5,6
// 第一輪任務中,宏任務爲全局script(恰好處於執行棧內,不用在事件隊列中取),因此先是1,2;
// 同時,因爲執行過程當中遇到了setTimeout,將其再放入宏任務隊列,遇到了promise,將其放入微任務隊列;
// 該輪宏任務執行完畢後,開始執行微任務,將微任務所有取出,一次執行,所以再是3,4;
// 開始第二輪任務,取出的宏任務爲setTimeout回調,所以結果是5;
// 同時執行這輪宏任務回調時,又遇到promise,再將其放入微任務隊列;
// 當這輪宏任務setTimeout回調結束後,當即剛纔加入的微任務取出執行,所以結果爲6;
  • api優先級順序
    • html5新特性MutationObserver屬於微任務,優先級小於Promise
    • html5新特性MessageChannel屬於宏任務,優先級是:setImmediate->MessageChannel->setTimeout。
    • 在node環境的微任務執行中,process.nextTick的優先級高於promise。
    • 在node環境的宏任務執行中,setImmediate的優先級高於setTimeout。
  • Vue.nextTick實現
    • 2.4版本時,Vue經過利用MutationObserver來模擬nextTick(MutationObserver爲H5新特性,用於監聽一個dom變更, 當dom對象樹發生任何變更時,Mutation Observer會獲得通知)
    • 2.5版本開始,nextTick實現移除了MutationObserver的方式(兼容性緣由), 取而代之的是使用MessageChannel (固然,默認狀況仍然是Promise,不支持才兼容的)
    • 因爲,js執行是單線程,在一個tick的過程當中,可能會存在屢次修改數據,vue會把這些數據修改先統一push到一個隊列裏,而後內部調用1次nextTick去更新視圖。
    • 所以,vue從數據改變到dom視圖變化是須要在下一個tick才能完成的,這種數據驅動變化的原理符合遊覽器的原理(js引擎線程和gui渲染互斥)和處理策略(task(宏->微)->render->task(宏->微)-->...)
    • 最終,Vue.nextTick採起的策略是默認走 microtask,對於一些dom交互事件,如v-on綁定的事件回調函數的處理,會強制走macrotask。對於macrotask的執行,vue優先檢測是否支持原生setImmediate(高版本遊覽器支持),不支持的話再去檢測是否支持原生的MessageChannel,若是也不支持的話就會降級爲setTimeout 0。

參考

相關文章
相關標籤/搜索