vue 任務隊列和異步更新策略 (清晰理解任務隊列、微任務、宏任務)

事件循環

JavaScript 語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。爲了協調事件、用戶交互、腳本、UI 渲染和網絡處理等行爲,防止主線程的不阻塞,Event Loop 的方案應用而生。Event Loop 包含兩類:一類是基於 Browsing Context,一種是基於 Worker。二者的運行是獨立的,也就是說,每一個 JavaScript 運行的"線程環境"都有一個獨立的 Event Loop,每一個 Web Worker 也有一個獨立的 Event Loop。

任務隊列

vue 數據驅動視圖是數據改變,視圖異步等待所有數據變化完成,統一進行視圖更新。

既然是異步就有順序和優先級, 異步任務隊列是那種順序執行 ?

tip: 微任務優先級高於宏任務 MDN 介紹

任務隊列主要分爲兩種:

1、microtasks(微任務):

  • Promise :ES6的異步處理方案
  • process.nextTick(vue.nextTick) :下輪tick更新機制,
  • Mutation Observer API: DOM改變 監聽 API

2、macrotasks(宏任務也稱任務):

  • setTimeout() : 延時器

  • setInterval(): 計時器

  • setImmediate:node.js 回調函數延遲執行,process.nextTicl() 方法十分類似

    process.nextTick()中的回調函數執行的優先級要高於setImmediate().這裏的原因在於事件循環對觀察者的檢查是有先後順序的,process.nextTick()屬於idle觀察者,setImmediate()屬於check觀察者。在每一個輪循環檢查中,idle觀察者先於I/O觀察者,I/O觀察者先於check觀察者。

    在具體實現上,process.nextTick()的回調函數保存在一個數組中,setImmediate()的結果則是保存在鏈表中。在行爲上,process.nextTick()在每輪循環中會將該數組中的回調函數全部執行完,而setImmediate()在每輪循環中執行鏈表中的一個回調函數

  • I/O :系統IO(input/output)

  • UI render :頁面渲染

  • requestAnimationFrame():異步動畫渲染 ,瀏覽器調用指定的函數以在下次重繪之前更新動畫

如何理解微任務和宏任務?

創造代碼的人也是人,他們的靈感多數來自於生活。我們這裏打個比方(樸靈老師也這樣比喻),javascript處理異步就像去餐館喫飯,服務員會先爲顧客下單,下完單把顧客點的單子給後廚讓其準備,然後就去服務下一位顧客,,而不是一直等待在出餐口。
javascript將顧客下單的行爲進行了細分。無外乎兩種酒水類和非酒水類。對應着我們javascript中的macroTask和microTask。

但是在不同場景下的步驟是不一樣的,就像西餐和中餐。西餐劃分的非常詳細:頭盤->湯->副菜->主菜->蔬菜類菜餚->甜品->咖啡,中餐就相對簡單許多:涼菜->熱菜->湯。

任務隊列,下面看幾個示例, 輸出內容便是執行順序:

setTimeout(()=>{
    console.log('1')

    Promise.resolve().then(function() {
        console.log('2')
    })
}, 0)

setTimeout(()=>{
    console.log('3')

    Promise.resolve().then(function() {
        console.log('4')
    })
}, 0)
setTimeout(function() {console.log('6')}, 0)

requestAnimationFrame(function(){
    console.log('5')
})

setTimeout(function() {console.log('7')}, 0)

new Promise(function executor(resolve) {
    console.log('1')
    resolve()
    console.log('2')
}).then(function() {
    console.log('4')
})

console.log('3')
console.log('1');

setTimeout(() => {
    console.log('5');
    process.nextTick(() => console.log('6'));
}, 0);

process.nextTick(() => {
    console.log('3');
    process.nextTick(() => console.log('4'));
});

console.log('2');

這樣就可以理解Vue的異步更新策略運行機制

  created() {
    this.textDemo()
  },
  methods: {
    textDemo() {
      console.log(1)

      setTimeout(() => { // macroTask
        console.log(4)

        setTimeout(() => { // macroTask
          console.log(8)
        }, 0)

        this.$nextTick(() => { // microTask
          console.log(5)
        })

        Promise.resolve().then(function() { // microTask
          console.log(7)
        })

        this.$nextTick(() => { // microTask
          console.log(6)
        })
      }, 0)

      this.$nextTick(() => { // microTask
        console.log(3)
      })

      console.log(2)
    },
}

到此我們已經知道代碼是如何運行的了, 如果你想要更深入理解, 請繼續向下閱讀。

深究Vue異步更新策略原理

我們先看一個示例:

<template>
  <div>
    <div ref="test">{{test}}</div>
    <button @click="handleClick">tet</button>
  </div>
</template>
<script>
export default {
    data () {
        return {
            test: 'begin'
        };
    },
    methods () {
        handleClick () {
            this.test = 'end';
            console.log(this.$refs.test.innerText);// 結果輸出 begin
        }
    }
}
</script>

通過上面示例可以看出 Vue是異步執行DOM更新, 更新會緩衝到隊列中, 在nextTick 集中刷新隊列並執行實際 (已去重的) 工作

當然新版 this.$nextTick 有一些變化 。從Vue 2.5+開始,抽出來了一個單獨的文件next-tick.js來執行它。
在這裏插入圖片描述

其大概的意思就是:在Vue 2.4之前的版本中,nextTick 幾乎都是基於 microTask 實現的(具體可以看文章nextTick一節),但是由於 microTask 的執行優先級非常高,在某些場景之下它甚至要比事件冒泡還要快,就會導致一些詭異的問題;但是如果全部都改成 macroTask,對一些有重繪和動畫的場景也會有性能的影響。所以最終 nextTick 採取的策略是默認走 microTask,對於一些DOM的交互事件,如 v-on綁定的事件回調處理函數的處理,會強制走 macroTask。

具體做法就是,在Vue執行綁定的DOM事件時,默認會給回調的handler函數調用withMacroTask方法做一層包裝,它保證整個回調函數的執行過程中,遇到數據狀態的改變,這些改變而導致的視圖更新(DOM更新)的任務都會被推到macroTask。

源碼:

function add$1 (event, handler, once$$1, capture, passive) {
    handler = withMacroTask(handler);
    if (once$$1) { handler = createOnceHandler(handler, event, capture); }
    target$1.addEventListener(
        event,
        handler,
        supportsPassive
        ? { capture: capture, passive: passive }
        : capture
    );
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a Task instead of a MicroTask.
 */
function withMacroTask (fn) {
    return fn._withTask || (fn._withTask = function () {
        useMacroTask = true;
        var res = fn.apply(null, arguments);
        useMacroTask = false;
        return res
    })
}

最後,寫一段DEMO驗證一下 :

<template>
	<div>
	    <button @click="handleClick">change</button>
	</div>
</template>
<script>
export default {
   created() {
      this.handleClick() //  得出結果 : 2 3 1 4
  },
   methods: {
	   handleClick() {
	     setTimeout(() => { // macroTask
	       console.log(4)
	     }, 0)
	
	     this.$nextTick(() => { // microTask
	       console.log(2)
	     })
	
	     Promise.resolve().then(function() { // microTask
	       console.log(1)
	     })
	
	     this.$nextTick(() => { // microTask
	       console.log(3)
	     })
	   }
   }
}
</script>

在Vue 2.5+中,這段代碼的輸出順序是1 - 2 - 3 - 4, 而 Vue 2.4 和 不通過DOM 輸出 2 - 3 - 1 - 4。nextTick執行順序的差異正好說明了上面的改變。

tips: 所以這裏需要留意遇到DOM操作, 同步執行受阻或者節點內容未及時更新可以使用 this.$nextTick 等待一下在執行下面的操作。

<div id="example">
    <audio ref="audio"
           :src="url"></audio>
    <span ref="url"></span>
    <button @click="changeUrl">click me</button>
</div>
<script>
const musicList = [
    'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112003137.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3',
    'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112002493.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3',
    'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112004168.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3'
];
var vm = new Vue({
    el: '#example',
    data: {
        index: 0,
        url: ''
    },
    methods: {
        changeUrl() {
            this.index = (this.index + 1) % musicList.length
            this.url = musicList[this.index];
            this.$refs.audio.play();
        }
    }
});
</script>

毫無懸念,這樣肯定是會報錯的:

Uncaught (in promise) DOMException: The element has no supported sources.

原因就在於audio.play()是同步的,而這個時候DOM更新是異步的,src屬性還沒有被更新,結果播放的時候src屬性爲空,就報錯了。

解決辦法就是在play的操作加上this.$nextTick()

this.$nextTick(function() {
    this.$refs.audio.play();
});
異步更新有什麼好處?
<template>
  <div>
    <div>{{test}}</div>
  </div>
</template>
<script>
export default {
    data () {
        return {
            test: 0
        };
    },
    mounted () {
      for(let i = 0; i < 1000; i++) {
        this.test++;
      }
    }
}
</script>

上面的例子非常直觀, 可以這麼理解當數據更新同步操作DOM會出現頻繁渲染視圖造成頁面卡頓,極端的消耗資源。所以異步更新大大提升了性能, 並且數據更新很高效體驗並沒有降低

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章