什么是雙向綁定
我們先從單向綁定切入,其實單向綁定非常簡單,就是把Model綁定到View,當我們用JavaScript代碼更新Model時,View就會自動更新。那么雙向綁定就可以從此聯想到,即在單向綁定的基礎上,用戶更新了View,Model數據也會自動被更新,這種情況就是雙向綁定。實例如下:
?當用戶填寫表單,View的狀態就被更新了,如果此時可以自動更新Model的狀態,那就相當于我們把Model和View做了雙向綁定關系圖如下:
雙向綁定的原理是什么
我們都知道Vue是數據雙向綁定的框架,雙向綁定由三個重要部分組成
- Model:應用數據以及業務邏輯
- View:應用視圖,各類UI組件
- ViewModel:框架封裝的核心,它負責將數據與視圖關聯起來
上面這個分層的架構方案,即是我們經常耳熟能詳的MVVM,他的控制層的核心功能便是“數據雙向綁定”
理解ViewModel
它的主要職責就是:
- 數據變化后更新視圖
- 視圖變化后更細數據
當然,它還有兩個主要部分組成
- 監聽器:對所有的數據進行監聽
- 解析器(Compiler):對每個元素節點的指令進行掃描和解析,根據指令模板替換數據,以及綁定相應的更新函數
實現雙向綁定
我們還是以Vue為例,先看看Vue中雙向綁定流程是什么
1.new Vue()首先執行初始化,對data執行響應化處理,這個過程發生在Observe中(類似于Vue生命周期created之前執行的一系列初始化操作)
2.同時對模板執行編譯,找到其中動態綁定的數據,從data中獲取并初始化視圖,這個過程發生在Complie中(類似于Vue生命周期mounted之前執行的一系列初始化操作)
3.同時定義一個更新函數和Watcher,將來對應數據變化時Watcher會調用更新函數
4.由于data的某個key在一個視圖中可能會出現多次,所以每個key都需要一個管家Dep來管理多個Watcher
5.將來data中數據一旦發生變化,會首先找到ui應的Dep,同時所有Watcher執行更新函數
流程圖如下:
劫持監聽所有屬性Observe
先來一個構造函數:執行初始化,對data執行響應化處理
class Vue { constructor(options) { this.$options = options; this.$data = options.data; // 對data選項做響應式處理 observe(this.$data); // 代理data到vm上 proxy(this); // 執行編譯 new Compile(options.el, this); }
}
對data選項進行響應化具體操作
function proxy(vm) {Object.keys(vm.$data).forEach(key=>{Object.defineProperty(vm, key, {get() {return vm.$data[key]},set(newVal) {vm.$data[key] = newVal}})})
}function observe(obj) { if (typeof obj !== "object" || obj == null) { return; } new Observer(obj);
} class Observer { constructor(value) { this.value = value; this.walk(value); } walk(obj) { Object.keys(obj).forEach((key) => { defineReactive(obj, key, obj[key]); }); }
}
編譯Complie
對每個元素節點的指令進行掃面和解析,根據指令模板替換數據,同時綁定相應的更新函數
class Compile { constructor(el, vm) { this.$vm = vm; this.$el = document.querySelector(el); // 獲取dom if (this.$el) { this.compile(this.$el); } } compile(el) { const childNodes = el.childNodes; Array.from(childNodes).forEach((node) => { // 遍歷子元素 if (this.isElement(node)) { // 判斷是否為節點 console.log("編譯元素" + node.nodeName); } else if (this.isInterpolation(node)) { // 判斷是否為插值文本 {{}} console.log("編譯插值?本" + node.textContent); } if (node.childNodes && node.childNodes.length > 0) { // 判斷是否有子元素 this.compile(node); // 對子元素進行遞歸遍歷 } }); } isElement(node) { return node.nodeType == 1; } isInterpolation(node) { return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent); }
}
依賴收集
????????Vue2.x中的響應式原理主要死依賴于Object.defineProperty()方法實現屬性的getter和setter。在Vue中,每個組件實例都有一個對應的Watcher實例,Watcher實例會負責依賴的收集以及觸發更新。
????????具體來說,當一個組件渲染時,會執行render函數來生成Virtual DOM,并且在執行過程中,當訪問到組件的data中的屬性時,會觸發屬性的getter方法。并在getter方法中,會進行依賴收集,將當前的Watcher對象存儲到當前屬性的依賴列表中。
當個屬性收集具體如下圖:
多個屬性的收集如下:
依賴收集的過程可以簡單描述如下:
1.在組件渲染過程中,當訪問data中的屬性時,會觸發屬性的getter方法;
2.在getter方法中,會將當前Watcher對象存儲到當前依賴列表中(Dep);
3.當屬性被修改時,會觸發屬性的setter方法;
4.在setter方法中,會通知所有依賴于該屬性的Watcher對象,執行更新操作;
這樣,當數據發生變化 時,Vue能夠精確的知道哪些地方需要更新,并且只更新相關的部分,提高了性能(因為只有存儲了觸發getter方法時的watcher,做到了對應關系)
簡化版的實現代碼如下:
// 定義 Dep 類,用于管理依賴
class Dep {constructor() {this.subscribers = new Set(); // 存儲 Watcher 實例的集合}// 添加依賴depend() {if (activeWatcher) {this.subscribers.add(activeWatcher);}}// 通知依賴更新notify() {this.subscribers.forEach(watcher => {watcher.update();});}
}let activeWatcher = null;// 定義 Watcher 類,用于觀察數據變化
class Watcher {constructor(update) {this.update = update; // 更新函數this.value = null; // 存儲當前值this.get(); // 初始化時進行依賴收集}// 獲取當前值,并進行依賴收集get() {activeWatcher = this;// 在這里模擬讀取 data 中的屬性的過程this.value = this.update();activeWatcher = null;}
}// 定義 reactive 函數,將對象轉換為響應式對象
function reactive(obj) {// 遍歷對象的每個屬性,轉換為響應式屬性for (let key in obj) {let value = obj[key];const dep = new Dep(); // 每個屬性對應一個依賴管理對象Object.defineProperty(obj, key, {get() {dep.depend(); // 依賴收集return value;},set(newValue) {value = newValue;dep.notify(); // 通知依賴更新}});}return obj;
}// 示例用法
const data = reactive({count: 0
});new Watcher(() => {console.log("Value updated:", data.count);
});data.count++; // 觸發更新
在這個示例中
- Dep類用于管理依賴,每個響應式屬性都會對應一個'Dep'實例,用于存儲依賴于該屬性的'Watcher'對象
- ’Watcher‘類用于觀察數據變化,當數據發生改變時會執行更新函數
- ’reactive‘函數用于將對象轉為響應式對象,在該函數中,通過'Object.defineProperty'來定義對象的屬性,實現了屬性的getter和setter,從而在讀取和修改屬性時進行依賴收集和通知更新
?在實際的 Vue 源碼中,會有更復雜的邏輯和優化,但基本原理與上述代碼類似。
個人備注說明:
1.上述代碼設計中為什么activeWatcher變量是全局存儲,同時在Watcher類的get方法中先是指向了this,然后又賦值為空?
答疑:在Vue源碼中,activeWatcher
通常是通過棧結構來管理的,這里這樣可以支持嵌套的依賴收集。而上述代碼Watcher
類的 get
方法中,將activeWatcher
設置為當前的Watcher實例的原因是依賴收集過程中給需要知道當前的依賴是誰,從而在屬性發生變化時可以通知到相關的 Watcher
實例進行更新。在依賴收集完成后,將activeWatcher
設置為空的原因時為了防止在非依賴收集的情況下,誤操作導致activeWatcher
保留了值。
一般來說,在Vue的相應式系統中,activeWatcher
在以下幾種情況下會被設置為某個具體的 Watcher
對象:
- 組件渲染過程中:在組件的渲染過程中,Vue會創建一個Watcher對象來實現觀察組件的渲染函數。此時
activeWatcher
會被設置為這個渲染Watcher對象,以便在渲染函數中訪問組件的響應式數據時進行依賴收集 - 計算屬性或者偵聽的求職過程中:當計算屬性或者偵聽器的值被求值時,Vue會創建一個Watcher對象來觀察相關的響應式數據,以便在求值過程中訪問相關的響應式數據時進行依賴收集。
- 用戶手動創建的Watcher
以上情況下,activeWatcher
都會在相應的Watcher對象的get方法中被設置為當前Watcher實例。在依賴收集完成后,activeWatcher
會被重新設置為null,以便下一次依賴收集的時候再次被設置為新的Watcher對象。
2.Watcher 類中的value的作用是什么?
答疑:在 Vue 的響應式系統中,Watcher 類負責觀察數據的變化,value 的存在可以讓 Watcher 在依賴收集時記錄當前的值,在數據發生變化時,可以通過對比新舊值來判斷是否需要觸發更新操作。