## 為什么要異步更新
通過前面幾個章節我們介紹,相信大家已經明白了 Vue.js 是如何在我們修改 `data` 中的數據后修改視圖了。簡單回顧一下,這里面其實就是一個“`setter -> Dep -> Watcher -> patch -> 視圖`”的過程。
假設我們有如下這么一種情況。
```
<template>
<div>
<div>{{number}}</div>
<div @click="handleClick">click</div>
</div>
</template>
```
```
export default {
data () {
return {
number: 0
};
},
methods: {
handleClick () {
for(let i = 0; i < 1000; i++) {
this.number++;
}
}
}
}
```
當我們按下 click 按鈕的時候,`number` 會被循環增加1000次。
那么按照之前的理解,每次 `number` 被 +1 的時候,都會觸發 `number` 的 `setter` 方法,從而根據上面的流程一直跑下來最后修改真實 DOM。那么在這個過程中,DOM 會被更新 1000 次!太可怕了。
Vue.js 肯定不會以如此低效的方法來處理。Vue.js在默認情況下,每次觸發某個數據的 `setter` 方法后,對應的 `Watcher` 對象其實會被 `push` 進一個隊列 `queue` 中,在下一個 tick 的時候將這個隊列 `queue` 全部拿出來 `run`( `Watcher` 對象的一個方法,用來觸發 `patch` 操作) 一遍。

那么什么是下一個 tick 呢?
## nextTick
Vue.js 實現了一個 `nextTick` 函數,傳入一個 `cb` ,這個 `cb` 會被存儲到一個隊列中,在下一個 tick 時觸發隊列中的所有 `cb` 事件。
因為目前瀏覽器平臺并沒有實現 `nextTick` 方法,所以 Vue.js 源碼中分別用 `Promise`、`setTimeout`、`setImmediate` 等方式在 microtask(或是task)中創建一個事件,目的是在當前調用棧執行完畢以后(不一定立即)才會去執行這個事件。
筆者用 `setTimeout` 來模擬這個方法,當然,真實的源碼中會更加復雜,筆者在小冊中只講原理,有興趣了解源碼中 `nextTick` 的具體實現的同學可以參考[next-tick](https://github.com/vuejs/vue/blob/dev/src/core/util/next-tick.js#L90)。
首先定義一個 `callbacks` 數組用來存儲 `nextTick`,在下一個 tick 處理這些回調函數之前,所有的 `cb` 都會被存在這個 `callbacks` 數組中。`pending` 是一個標記位,代表一個等待的狀態。
`setTimeout` 會在 task 中創建一個事件 `flushCallbacks` ,`flushCallbacks` 則會在執行時將 `callbacks` 中的所有 `cb` 依次執行。
```
let callbacks = [];
let pending = false;
function nextTick (cb) {
callbacks.push(cb);
if (!pending) {
pending = true;
setTimeout(flushCallbacks, 0);
}
}
function flushCallbacks () {
pending = false;
const copies = callbacks.slice(0);
callbacks.length = 0;
for (let i = 0; i < copies.length; i++) {
copies[i]();
}
}
```
## 再寫 Watcher
第一個例子中,當我們將 `number` 增加 1000 次時,先將對應的 `Watcher` 對象給 `push` 進一個隊列 `queue` 中去,等下一個 tick 的時候再去執行,這樣做是對的。但是有沒有發現,另一個問題出現了?
因為 `number` 執行 ++ 操作以后對應的 `Watcher` 對象都是同一個,我們并不需要在下一個 tick 的時候執行 1000 個同樣的 `Watcher` 對象去修改界面,而是只需要執行一個 `Watcher` 對象,使其將界面上的 0 變成 1000 即可。
那么,我們就需要執行一個過濾的操作,同一個的 `Watcher` 在同一個 tick 的時候應該只被執行一次,也就是說隊列 `queue` 中不應該出現重復的 `Watcher` 對象。
那么我們給 `Watcher` 對象起個名字吧~用 `id` 來標記每一個 `Watcher` 對象,讓他們看起來“不太一樣”。
實現 `update` 方法,在修改數據后由 `Dep` 來調用, 而 `run` 方法才是真正的觸發 `patch` 更新視圖的方法。
```
let uid = 0;
class Watcher {
constructor () {
this.id = ++uid;
}
update () {
console.log('watch' + this.id + ' update');
queueWatcher(this);
}
run () {
console.log('watch' + this.id + '視圖更新啦~');
}
}
```
## queueWatcher
不知道大家注意到了沒有?筆者已經將 `Watcher` 的 `update` 中的實現改成了
```
queueWatcher(this);
```
將 `Watcher` 對象自身傳遞給 `queueWatcher` 方法。
我們來實現一下 `queueWatcher` 方法。
```
let has = {};
let queue = [];
let waiting = false;
function queueWatcher(watcher) {
const id = watcher.id;
if (has[id] == null) {
has[id] = true;
queue.push(watcher);
if (!waiting) {
waiting = true;
nextTick(flushSchedulerQueue);
}
}
}
```
我們使用一個叫做 `has` 的 map,里面存放 id -> true ( false ) 的形式,用來判斷是否已經存在相同的 `Watcher` 對象 (這樣比每次都去遍歷 `queue` 效率上會高很多)。
如果目前隊列 `queue` 中還沒有這個 `Watcher` 對象,則該對象會被 `push` 進隊列 `queue` 中去。
`waiting` 是一個標記位,標記是否已經向 `nextTick` 傳遞了 `flushSchedulerQueue` 方法,在下一個 tick 的時候執行 `flushSchedulerQueue` 方法來 flush 隊列 `queue`,執行它里面的所有 `Watcher` 對象的 `run` 方法。
## flushSchedulerQueue
```
function flushSchedulerQueue () {
let watcher, id;
for (index = 0; index < queue.length; index++) {
watcher = queue[index];
id = watcher.id;
has[id] = null;
watcher.run();
}
waiting = false;
}
```
## 舉個例子
```
let watch1 = new Watcher();
let watch2 = new Watcher();
watch1.update();
watch1.update();
watch2.update();
```
我們現在 new 了兩個 `Watcher` 對象,因為修改了 `data` 的數據,所以我們模擬觸發了兩次 `watch1` 的 `update` 以及 一次 `watch2` 的 `update`。
假設沒有批量異步更新策略的話,理論上應該執行 `Watcher` 對象的 `run`,那么會打印。
```
watch1 update
watch1視圖更新啦~
watch1 update
watch1視圖更新啦~
watch2 update
watch2視圖更新啦~
```
實際上則執行
```
watch1 update
watch1 update
watch2 update
watch1視圖更新啦~
watch2視圖更新啦~
```
這就是異步更新策略的效果,相同的 `Watcher` 對象會在這個過程中被剔除,在下一個 tick 的時候去更新視圖,從而達到對我們第一個例子的優化。
我們再回過頭聊一下第一個例子, `number` 會被不停地進行 `++` 操作,不斷地觸發它對應的 `Dep` 中的 `Watcher` 對象的 `update` 方法。然后最終 `queue` 中因為對相同 `id` 的 `Watcher` 對象進行了篩選,從而 `queue` 中實際上只會存在一個 `number` 對應的 `Watcher` 對象。在下一個 tick 的時候(此時 `number` 已經變成了 1000),觸發 `Watcher` 對象的 `run` 方法來更新視圖,將視圖上的 `number` 從 0 直接變成 1000。
到這里,批量異步更新策略及 nextTick 原理已經講完了,接下來讓我們學習一下 Vuex 狀態管理的工作原理。
注:本節代碼參考[《批量異步更新策略及 nextTick 原理》](https://github.com/answershuto/VueDemo/blob/master/%E3%80%8A%E6%89%B9%E9%87%8F%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0%E7%AD%96%E7%95%A5%E5%8F%8A%20nextTick%20%E5%8E%9F%E7%90%86%E3%80%8B.js)。