Vue 數據雙向響應機制
參考資料(感謝各位前輩的分享和資料)
尤雨溪講解 Vue 源碼
Vue 源碼解析-Vue 中文社區
小馬哥 Vue 源碼解析
小馬哥 Vue 源碼解析代碼示範
vue-cli 源碼
MDN
Vue 的特點是數據驅動視圖,也就是說,數據變化時視圖隨之變化, 所以要先監聽到數據的變化,然後再去響應依賴該數據的視圖,Vue 使用 Object 的 defineProperty 函數劫持數據的變化,用 Complite 源碼解析器解析我們編寫的 Vue 代碼, 用 Dep 類收集數據依賴,使用 Watcher 類響應數據變化並更新視圖。
一、Complite
源碼解析器
因爲要結合 Watcher
,所以 Complite
只在這裏做簡單的介紹,如果有時間單獨寫一篇.
我們在 .vue
的文件中編寫的代碼,或者創建 Vue
實例後編寫的代碼,部分如指令、直接在 DOM
中渲染調用並計算變量等是不被瀏覽器直接識別和解析的,所以在代碼正常渲染在瀏覽器並執行業務邏輯之前,Vue
要先將我們的代碼進行編譯。
編譯流程如下(來自 Vue 中文社區 Vue 源碼解析,全文鏈接在文章開頭):
我們編寫的 Vue
實例的結構代碼其實是一個字符串,這個字符串被 Vue
的 Complite
源碼解析器編譯成抽象語法數AST
(DOM 節點及屬性以對象嵌套的形式存在),然後根據 AST
爲每個節點生成對應的 render
函數,調用某個節點的 render
函數就能形成對應的 VNode
。
Complite
除了要要編譯源碼之外,還要捕獲數據變化,也就是綁定 Watcher
,然後根據 Watcher
類的,更新視圖。
二、Observer
類
1. Object.defineProperty
函數 ( Object.defineProperty(obj, key, desc);
)
- 爲一個對象添加屬性,或者修改對象已有的屬性,並返回這個對象。
- 由
Object
構造器直接調用,對象的實例不能調用。 - 接受三個參數,
obj
指要操作屬性的源對象,key
指要操作的這個屬性,desc
指要操作的屬性的描述對象。 - 可以定義
Symbol
類型的數據作爲key
。 - 對象屬性的描述中,非常重要的兩個函數
get
和set
。 - 更具體的內容,MDN 上有非常詳細的說明,建議一看。
Object.defineProperty(obj, 'foo', {
get() {
// 每當 obj 訪問 foo 屬性時,get 函數會自動執行
// 不傳入參數,會傳入 this,但是 this 不一定指想 obj
// 該函數的返回值會被當做是 foo 的屬性值
},
set(newVal) {
// 當 foo 屬性值被修改時,set 函數會自動執行
// 接受一個參數 newVal,是爲 foo 賦的新值
// 會傳入 賦值時的 this 對象
}
});
2. Object.defineProperty
函數和 vue-cli
的關聯
在 Vue
中,一個組件是一個 VueComponent
實例,每個實例有自己的 $data
對象,其中存儲的是當前組件的數據列表,要讓每個數據動態響應,就需要爲每個數據添加 get
和 set
,來劫持數據值的改變。
// 爲指定對象的每個鍵添加 setter 和 getter
function covert(obj) {
const keyList = Object.keys(obj);
keyList.forEach(key => {
// _val 如果該屬性已經存在,就是上一次的賦值,不存在爲undifined
// 每個 key 的操作都會形成一個閉包,所以這個閉包就成了單獨存儲對象某個屬性值(_val)的位置
let _val = obj[key];
Object.defineProperty(obj, key, {
get() {
// 在這裏可以捕獲到屬性的變化,並限制取值操作,如果 return 一個固定值,那麼 obj 的某個鍵就永遠是這個值,重新賦值也沒用
return _val;
},
set(val) {
// 這裏可以對比屬性的新舊值,並限制賦值操作
_val = val;
}
});
});
}
// -----------------------------------------------
// 調用
const data = {
visible: true,
num: 10
};
covert(data); // 爲 data 的兩個屬性綁定 get 和 set
data.visible = false; // 調用 set
console.log(data.visible); // 調用 get
3. vue-cli
源碼中的 Observer
類簡介(極簡)
class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
// 數組的另一套劫持方式
} else {
this.walk(value)
}
}
walk (obj: Object) {
// 爲每個鍵綁定 get 和 set
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
observeArray (items: Array<any>) {
// 數組的特殊操作:由於數組數據類型的特殊性,數組的整體值的變更和劫持依舊在 set 和 get 中
// 但是 vue 爲 Array 這個類的原型函數們添加了劫持,也就是說當數組的值發生改變時,要調用原型函數之前,先處理我們需要的業務操作
}
}
// ----------------------------------------------------------------
function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
...
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
...
},
set: function reactiveSetter (newVal) {
...
}
})
}
三、Dep
類
通過 Observer
類爲數據們添加了 getter
和 setter
之後,就能觀察到數據的變化,我們要改變跟這個數據有關係的一系列內容,可能是其他數據,也可能是頁面渲染的內容。
1. 依賴、依賴收集和通知依賴
- 某個數據
A
直接控制的內容,或者通過A
計算得到的內容,都被稱爲依賴了A
。 - 都有誰依賴了
A
,需要一個準確的計量,才能在A
每次改變時,執行對應的操作,這種計量方式是一個數組,因爲依賴A
的數據很可能有多個。 - 確定誰依賴了數據
A
,並將這些依賴收集起來:依賴數據A
一定要獲取數據A
,所以在A
的getter
中,做依賴收集。 - 數據
A
每次發生值的改變時,setter
會執行,所以在A
的setter
中通知依賴。
2. 依賴的操作
依賴可能被添加,也有可能伴隨着組件卸載、銷燬而被刪除,所以依賴除了要被聲明之外,還要有其他操作,依賴類 Dep
就是用來爲每個數據創建依賴並處理依賴的。
3. vue-cli
中的 Dep
類
...
let uid = 0
class Dep {
static target: ?Watcher; // 靜態屬性,Dep 構造器訪問,Dep 的實例不能訪問
id: number; // 每個 Dep 實例唯一的id
subs: Array<Watcher>;
constructor () {
this.id = uid++
// 某個數據的依賴列表
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
// 當某個數據的值發生變化時,要循環這個數據的依賴列表,並且讓他們相關的所有操作都更新一次
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
...
4. Observer
中調用 Dep
(簡寫)
class Observer {
constructor() {
this.dep = new Dep();
}
...
}
function defineReactive() {
Object.defineProperty(obj, key, {
get() {
// get 被調用一次,都代表有一個數據依賴於當前數據,要向當前數據的依賴列表 subs 中添加一個依賴
dep.depend()
...
},
set() {
// set 被調用且值發生改變時,代表當前數據更新,那麼依賴於當前數據的所有內容都要發生對應變化
dep.notify()
...
}
...
});
}
四、Watcher
類
上面的 Dep
類只是對數據的依賴進行管理的一套方式,從 Dep
的源碼中我們可以捕捉到,真正的被添加到 subs
數組中的依賴是 Watcher
。
1. 什麼是 Watcher
某個表達式 Express
,用到到了某個數據 A
,我們就說 Express
訂閱了 A
,爲了能讓這個 Express
在 A
每次發生變化時,都能動態的重新計算自己,我們就爲它編寫一個 update
方法來更新自己並做後續的數據渲染。每次 A
變化,都通知 Express
讓它 update
。
在 Vue
中,到處都是這樣的訂閱與響應,所以產生了 Watcher
類,專門處理數據變化之後其依賴們的響應動作。
2. Watcher
需要具備的功能
以下三點來自小馬哥源碼解析及總結
- 在自身實例化時往屬性訂閱器(
dep
)裏面添加自己。 - 自身必須有一個
update()
方法。 - 待屬性變動
dep.notify()
通知時,能調用自身的update()
方法,並觸發Compile中綁定的回調,則功成身退。
3. 結合 Observer
和 Dep
,整個數據雙向響應流程如下:
- 數據初始化,初始化了
dep
屬性,繼承了Dep
類的subs
屬性,來承接依賴列表; - 爲數據綁定了
getter
和setter
; - 數據每次被調用都代表被訂閱,
getter
返回數據值的同時,向subs
數組中添加了一個Watcher
; - 數據值發生改變,調用
setter
,setter
設置數據值的同時,調用Dep
提供的notify
函數,來通知該數據的依賴做出響應; notify
函數內部遍歷依賴列表(即訂閱者Watcher
列表)subs
數組,調用每個訂閱者的update
函數,讓其根絕數據新的值重新計算自己。