一、前言
“響應(yīng)式”是 Vue 的一大特點,當(dāng)我們在根實例的 data 中定義了數(shù)據(jù)后,在模板中使用 {{}} 的語法便可以使用其中的數(shù)據(jù)了,且在之后的編碼中我們無需關(guān)心其視圖層面的表現(xiàn),可以將精力集中處理業(yè)務(wù)邏輯與數(shù)據(jù)。因為 Vue 會代替我們監(jiān)聽數(shù)據(jù)的變化,并在視圖層同步我們更新的數(shù)據(jù)。那 Vue 是如何實現(xiàn)對數(shù)據(jù)的監(jiān)聽與使用的呢?例如對象的屬性,數(shù)組的項。
二、數(shù)據(jù)偵測
(一)偵測 Object
1. Object.defineProperty(obj, prop, descriptor)
該方法用于設(shè)置對象屬性,其接受 3 個參數(shù),第一個參數(shù)是需要定義的對象,第二個是需要定義的屬性,第三個則被稱為描述符。JS 中的對象身上有兩種描述符,第一種是數(shù)據(jù)描述符,具有以下鍵值:
- configurable,用于描述數(shù)據(jù)的可配置性,是否可通過 delete 操作符刪除,默認(rèn)值為 false;
- enumerable,表示該屬性是否可枚舉,即可通過 for...in 訪問其屬性,默認(rèn)值為 false;
- value,表示該屬性的值,可以是任何有效的 JS 值,默認(rèn)值為 undefined;
- writable,表示該屬性的值是否可寫,默認(rèn)值為 false,當(dāng)值為 true 時其值才可被賦值運算符改變。
第二種是訪問器描述符,具有以下鍵值:
- configurable,用于描述數(shù)據(jù)的可配置性,是否可通過 delete 操作符刪除,默認(rèn)值為 false;
- enumerable,表示該屬性是否可枚舉,即可通過 for...in 訪問其屬性,默認(rèn)值為 false;
- getter,在讀取屬性時調(diào)用的函數(shù),默認(rèn)值為 undefined;
- setter,在寫入屬性時調(diào)用的函數(shù),默認(rèn)值為 undefined。
這兩種描述符不可以同時使用。同時,請注意 getter 與 setter 兩個方法,一個是讀取對象屬性時調(diào)用,一個是寫入時調(diào)用。這意味著對象的屬性是可“偵測”的。
2. 數(shù)據(jù)響應(yīng)
現(xiàn)在,對象屬性已經(jīng)是可以偵測的了,但實現(xiàn)視圖層的同步更新是一個問題。
其實,數(shù)據(jù)雖多,但只需在使用數(shù)據(jù)的位置進行更新即可。前面也說到,getter 是一個屬性被讀取時調(diào)用的函數(shù),通過它,Vue 知道了誰讀取了該屬性,順即將其存在getter中。同樣的,誰觸發(fā)了 setter 方法,由 setter 去通知依賴了該屬性的元素更新就好。但Vue中的實現(xiàn)并非這么簡單,Vue 建立了一個 Watcher,其代替 getter 行使收集那些依賴了數(shù)據(jù)的元素的職能,誰使用了數(shù)據(jù),便為其建立一個 watcher 實例,當(dāng)數(shù)據(jù)更新時,由這個 watcher 通知元素更新數(shù)據(jù)。但這個通知不是直接通知,Vue是通過虛擬 Dom 實現(xiàn)真實 Dom 的更新,這個 watcher 通知渲染函數(shù),再由渲染函數(shù)操作虛擬 Dom 實現(xiàn)視圖的更新。
3. 屬性的使用
Vue 無法檢測 property 的添加或移除。由于 Vue 會在初始化實例時對 property 執(zhí)行 getter、setter 轉(zhuǎn)化,所以 property 必須在 data 對象上存在才能讓 Vue 將它轉(zhuǎn)換為響應(yīng)式的。
(二)偵測 Array
1. 利用對象的 getter 與 setter
Array 類型并不同于 Object 類型,并沒有 getter 與 setter,其并不能使用這兩種方法。但巧妙的是,data 里 return 的就是一個對象,數(shù)組存放在其中,恰作為 data 的一個屬性,誰讀取它,便可以被偵測到了。但這僅僅是偵測到 array 的使用者,array 的自身的變化如何偵測?
2. 操作即變化
對于數(shù)組而言,數(shù)組的變化便表明使用者調(diào)用了數(shù)組方法,例如 push(),pop() 等。明確了這個概念,Vue 便可以實現(xiàn) Array 的偵測。其將可以改變數(shù)組的方法(push,pop,shift,unshift,splice,sort,reverse)進行了從新封裝,在不改變這些方法的原始功能的基礎(chǔ)上安插了偵測器(攔截器),一旦調(diào)用這些方法,意味著數(shù)組的改變,這些偵聽器便會通知Vue數(shù)組發(fā)生了變化。通過實現(xiàn)一個存儲那些使用了數(shù)組的依賴存儲器,再實現(xiàn)一個訪問通知這些依賴的方法,便實現(xiàn)了數(shù)組的偵測。
3. 彌補不足
由于數(shù)組的檢測通過重寫數(shù)組方法實現(xiàn),所以 Vue 并不能直接通過索引操作數(shù)組,也不能使用其 length 屬性操作數(shù)組。于是 Vue 實現(xiàn)了兩個 API。以下兩個方法實現(xiàn)第一種功能:
1. Vue.set(vm.items, indexOfItem, newValue)或vm.$set(vm.items, indexOfItem, newValue)
2. vm.items.splice(indexOfItem, 1, newValue)
以下方法實現(xiàn)第二種功能:
1. vm.items.splice(newLength)
這兩個方法有效彌補了上述不足。
二、聲明響應(yīng)式property
由于 Vue 不允許動態(tài)添加根級響應(yīng)式 property,所以你必須在初始化實例前聲明所有根級響應(yīng)式 property,哪怕只是一個空值:
var vm = new Vue({
data: {
// 聲明 message 為一個空值字符串
message: ''
},
template: '<div>{{ message }}</div>'
})
// 之后設(shè)置 `message`
vm.message = 'Hello!'
如果你未在 data 選項中聲明 message,Vue 將警告你渲染函數(shù)正在試圖訪問不存在的 property。
三、異步更新隊列
Vue 在觀察數(shù)據(jù)變化時并不是直接更新 DOM,而是開啟一個隊列,并緩存同一個事件循環(huán)中發(fā)生的所有數(shù)據(jù)改變。在緩存時會除去重復(fù)數(shù)據(jù),從而避免不必要的計算和 DOM 操作。然后,在下一個事件循環(huán) tick 中,Vue 刷新隊列并執(zhí)行實際(已去重)的工作。
Vue 這種去重機制減少了開銷,如果一個for循環(huán)來動態(tài)改變數(shù)據(jù) 100 次,其實它只會應(yīng)用最后一次改變,如果沒有這種機制,DOM 就要重繪 100 次。Vue會根據(jù)當(dāng)前瀏覽器環(huán)境優(yōu)先使用原生的Promise.then 和MutationObserve。如果都不支持就會采用setTimeout代替。為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM,可以在數(shù)據(jù)變化之后立即使用 Vue.nextTick(callback)。這樣回調(diào)函數(shù)將在 DOM 更新完成后被調(diào)用。
說人話就是 Vue 中的 Dom 更新并不是立即執(zhí)行的,在觀察數(shù)據(jù)變化后會將 Dom 更新存放起來,并對其進行一些去重工作,比如某個數(shù)據(jù)被循環(huán)了,不會直接循環(huán)N次,而是存起來一次性更新完以實現(xiàn)更高的性能。也就是說數(shù)據(jù)雖然已經(jīng)改變,但Dom的更新不是實時的,要想針對Dom做一些操作,就得用到 Vue.nextTick(callback) 方法,在其回調(diào)函數(shù)里實現(xiàn)自己想要進行的操作。