Vue的響應式原理(MVVM)深入解析
1. 如何實現一個響應式物件
最近在看 Vue 的原始碼,其中最核心基礎的一塊就是 Observer/Watcher/Dep, 簡而言之就是,Vue 是如何攔截資料的讀寫, 如果實現對應的監聽,並且特定的監聽執行特定的回撥或者渲染邏輯的。總的可以拆成三大塊來說。這一塊,主要說的是 Vue 是如何將一個 plain object 給處理成 reactive object 的,也就是,Vue 是如何攔截攔截物件的 get/set 的
我們知道,用 Object.defineProperty 攔截資料的 get/set 是 vue 的核心邏輯之一。這裡我們先考慮一個最簡單的情況 一個 plain obj 的資料,經過你的程式之後,使得這個 obj 變成 Reactive Obj (不考慮陣列等因素,只考慮最簡單的基礎資料型別,和物件):
如果這個 obj 的某個 key 被 get, 則打印出 get ${key} - ${val}
的資訊
如果這個 obj 的某個 key 被 set, 如果監測到這個 key 對應的 value 發生了變化,則打印出 set ${key} - ${val} - ${newVal}
的資訊。
對應的簡要程式碼如下:
Observer.js
export class Observer {
constructor(obj) {
this.obj = obj;
this.transform(obj);
}
// 將 obj 裡的所有層級的 key 都用 defineProperty 重新定義一遍, 使之 reactive
transform(obj) {
const _this = this;
for (let key in obj) {
const value = obj[key];
makeItReactive(obj, key, value);
}
}
}
function makeItReactive(obj, key, val) {
// 如果某個 key 對應的 val 是 object, 則重新迭代該 val, 使之 reactive
if (isObject(val)) {
const childObj = val;
new Observer(childObj);
}
// 如果某個 key 對應的 val 不是 Object, 而是基礎型別,我們則對這個 key 進行 defineProperty 定義
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
console.info(`get ${key}-${val}`)
return val;
},
set: (newVal) => {
// 如果 newVal 和 val 相等,則不做任何操作(不執行渲染邏輯)
if (newVal === val) {
return;
}
// 如果 newVal 和 val 不相等,且因為 newVal 為 Object, 所以先用 Observer迭代 newVal, 使之 reactive, 再用 newVal 替換掉 val, 再執行對應操作(渲染邏輯)
else if (isObject(newVal)) {
console.info(`set ${key} - ${val} - ${newVal} - newVal is Object`);
new Observer(newVal);
val = newVal;
}
// 如果 newVal 和 val 不相等,且因為 newVal 為基礎型別, 所以用 newVal 替換掉 val, 再執行對應操作(渲染邏輯)
else if (!isObject(newVal)) {
console.info(`set ${key} - ${val} - ${newVal} - newVal is Basic Value`);
val = newVal;
}
}
})
}
function isObject(data) {
if (typeof data === 'object' && data != 'null') {
return true;
}
return false;
}
index.js
import { Observer } from './source/Observer.js';
// 宣告一個 obj,為 plain Object
const obj = {
a: {
aa: 1
},
b: 2,
}
// 將 obj 整體 reactive 化
new Observer(obj);
// 無輸出
obj.b = 2;
// set b - 2 - 3 - newVal is Basic Value
obj.b = 3;
// set b - 3 - [object Object] - newVal is Object
obj.b = {
bb: 4
}
// get b-[object Object]
obj.b;
// get a-[object Object]
obj.a;
// get aa-1
obj.a.aa
// set aa - 1 - 3 - newVal is Basic Value
obj.a.aa = 3
這樣,我們就完成了 Vue 的第一個核心邏輯, 成功把一個任意層級的 plain object 轉化成 reactive object
2. 如何實現一個 watcher
前面講的是如何將 plain object 轉換成 reactive object. 接下來講一下,如何實現一個watcher.
實現的虛擬碼應如下:
虛擬碼
// 傳入 data 引數新建新建一個 vue 物件
const v = new Vue({
data: {
a:1,
b:2,
}
});
// watch data 裡面某個 a 節點的變動了,如果變動,則執行 cb
v.$watch('a',function(){
console.info('the value of a has been changed !');
});
// watch data 裡面某個 b 節點的變動了,如果變動,則執行 cb
v.$watch('b',function(){
console.info('the value of b has been changed !');
})
所以我們自然而然的想到,對某個 key 的 $watch 方法應該是新建了一個 watcher 的例項的. 並且,vue 也是這樣實現的. 簡要的 Vue 類的實現如下 (只貼出相關程式碼)
Vue.js
// 引入將上面中實現的 Observer
import { Observer } from './Observer.js';
import { Watcher } from './Watcher.js';
export default class Vue {
constructor(options) {
// 在 this 上掛載一個公有變數 $options ,用來暫存所有引數
this.$options = options
// 宣告一個私有變數 _data ,用來暫存 data
let data = this._data = this.$options.data
// 在 this 上掛載所有 data 裡的 key 值, 這些 key 值對應的 get/set 都被代理到 this._data 上對應的同名 key 值
Object.keys(data).forEach(key => this._proxy(key));
// 將 this._data 進行 reactive 化
new Observer(data, this)
}
// 對外暴露 $watch 的公有方法,可以對某個 this._data 裡的 key 值建立一個 watcher 例項
$watch(expOrFn, cb) {
// 注意,每一個 watcher 的例項化都依賴於 Vue 的例項化物件, 即 this
new Watcher(this, expOrFn, cb)
}
// 將 this.keyName 的某個 key 值的 get/set 代理到 this._data.keyName 的具體實現
_proxy(key) {
var self = this
Object.defineProperty(self, key, {
configurable: true,
enumerable: true,
get: function proxyGetter() {
return self._data[key]
},
set: function proxySetter(val) {
self._data[key] = val
}
})
}
}
Watch.js
// 引入Dep.js, 是什麼我們待會再說
import { Dep } from './Dep.js';
export class Watcher {
constructor(vm, expOrFn, cb) {
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
// 初始化 watcher 時, vm._data[this.expOrFn] 對應的 val
this.value = this.get();
}
// 用於獲取當前 vm._data 對應的 key = expOrFn 對應的 val 值
get() {
Dep.target = this;
const value = this.vm._data[this.expOrFn];
Dep.target = null;
return value;
}
// 每次 vm._data 裡對應的 expOrFn, 即 key 的 setter 被觸發,都會呼叫 watcher 裡對應的 update方法
update() {
this.run();
}
run() {
// 這個 value 是 key 被 setter 呼叫之後的 newVal, 然後比較 this.value 和 newVal, 如果不相等,則替換 this.value 為 newVal, 並執行傳入的cb.
const value = this.get();
if (value !== this.value) {
this.value = value;
this.cb.call(this.vm);
}
}
}
對於什麼是 Dep, 和 Watcher 裡的 update() 方法到底是在哪個時候被誰呼叫的,後面會說
3. 如何收集 watcher 的依賴
前面我們講了 watcher 的大致實現,以及 Vue 代理 data 到 this 上的原理。現在我們就來梳理一下,Observer/Watcher 之間的關係,來說明它們是如何呼叫的.
首先, 我們要來理解一下 watcher 例項的概念。實際上 Vue 的 v-model, v-bind , {{ mustache }}, computed, watcher
等等本質上是分別對 data 裡的某個 key 節點聲明瞭一個 watcher 例項.
<input v-model="abc">
<span>{{ abc }}</span>
<p :data-key="abc"></p>
...
const v = new Vue({
data:{
abc: 111,
}
computed:{
cbd:function(){
return `${this.abc} after computed`;
}
watch:{
abc:function(val){
console.info(`${val} after watch`)
}
}
}
})
這裡,Vue 一共聲明瞭 4 個 watcher 例項來監聽abc, 1個 watcher 例項來監聽 cbd. 如果 abc 的值被更改,那麼 4 個 abc - watcher 的例項會執行自身對應的特定回撥(比如重新渲染dom,或者是列印資訊等等)
不過,Vue 是如何知道,某個 key 對應了多少個 watcher, 而 key 對應的 value 發生變化後,又是如何通知到這些 watcher 來執行對應的不同的回撥的呢?
實際上更深層次的邏輯是:
在 Observer階段,會為每個 key 都建立一個 dep 例項。並且,如果該 key 被某個 watcher 例項 get, 把該 watcher 例項加入 dep 例項的佇列裡。如果該 key 被 set, 則通知該 key 對應的 dep 例項, 然後 dep 例項會將依次通知佇列裡的 watcher 例項, 讓它們去執行自身的回撥方法
dep 例項是收集該 key 所有 watcher 例項的地方.
watcher 例項用來監聽某個 key ,如果該 key 產生變化,便會執行 watcher 例項自身的回撥
相關程式碼如下:
Dep.js
export class Dep {
constructor() {
this.subs = [];
}
// 將 watcher 例項置入佇列
addSub(sub) {
this.subs.push(sub);
}
// 通知佇列裡的所有 watcher 例項,告知該 key 的 對應的 val 被改變
notify() {
this.subs.forEach((sub, index, arr) => sub.update());
}
}
// Dep 類的的某個靜態屬性,用於指向某個特定的 watcher 例項.
Dep.target = null
observer.js
import {Dep} from './dep'
function makeItReactive(obj, key, val) {
var dep = new Dep()
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
// 收集依賴! 如果該 key 被某個 watcher 例項依賴,則將該 watcher 例項置入該 key 對應的 dep 例項裡
if(Dep.target){
dep.addSub(Dep.target)
}
return val
},
set: (newVal) => {
if (newVal === val) {
return;
}
else if (isObject(newVal)) {
new Observer(newVal);
val = newVal;
// 通知 dep 例項, 該 key 被 set,讓 dep 例項向所有收集到的該 key 的 watcher 例項傳送通知
dep.notify()
}
else if (!isObject(newVal)) {
val = newVal;
// 通知 dep 例項, 該 key 被 set,讓 dep 例項向所有收集到的該 key 的 watcher 傳送通知
dep.notify()
}
}
})
}
watcher.js
import { Dep } from './Dep.js';
export class Watcher {
constructor(vm, expOrFn, cb) {
this.cb = cb;
this.vm = vm;
this.expOrFn = expOrFn;
this.value = this.get();
}
get() {
// 在例項化某個 watcher 的時候,會將Dep類的靜態屬性 Dep.target 指向這個 watcher 例項
Dep.target = this;
// 在這一步 this.vm._data[this.expOrFn] 呼叫了 data 裡某個 key 的 getter, 然後 getter 判斷類的靜態屬性 Dep.target 不為null, 而為 watcher 的例項, 從而把這個 watcher 例項新增到 這個 key 對應的 dep 例項裡。 巧妙!
const value = this.vm._data[this.expOrFn];
// 重置類屬性 Dep.target
Dep.target = null;
return value;
}
// 如果 data 裡的某個 key 的 setter 被呼叫,則 key 會通知到 該 key 對應的 dep 例項, 該Dep例項, 該 dep 例項會呼叫所有 依賴於該 key 的 watcher 例項的 update 方法。
update() {
this.run();
}
run() {
const value = this.get();
if (value !== this.value) {
this.value = value;
// 執行 cb 回撥
this.cb.call(this.vm);
}
}
}
總結:
至此, Watcher, Observer , Dep 的關係全都梳理完成。而這些也是 Vue 實現的核心邏輯之一。再來簡單總結一下三者的關係,其實是一個簡單的 觀察-訂閱 的設計模式, 簡單來說就是, 觀察者觀察資料狀態變化, 一旦資料發生變化,則會通知對應的訂閱者,讓訂閱者執行對應的業務邏輯 。我們熟知的事件機制,就是一種典型的觀察-訂閱的模式
Observer, 觀察者,用來觀察資料來源變化.
Dep, 觀察者和訂閱者是典型的 一對多 的關係,所以這裡設計了一個依賴中心,來管理某個觀察者和所有這個觀察者對應的訂閱者的關係, 訊息排程和依賴管理都靠它。
Watcher, 訂閱者,當某個觀察者觀察到資料發生變化的時候,這個變化經過訊息排程中心,最終會傳遞到所有該觀察者對應的訂閱者身上,然後這些訂閱者分別執行自身的業務回撥即可
參考
Vue原始碼解讀-滴滴FED
程式碼參考