前置知識
MVVM
MVVM是前端視圖層的分層開發思想。它把頁面分成了M、V和VM。其中,VM是MVVM思想的核心;因為VM是M和V之間的調度者。M 是指數據層,V 是指視圖層。
MVVM 框架實現了雙向綁定,減少通過操縱 DOM 去更新視圖。
通過ViewModel 對 Model 層 獲取到的數據進行處理,展現到 View 層上。
這就解耦了 View 層和 Model 層,是前后端分離方案實施的重要一環。
發布/訂閱設計模式
首先先來看了解一下觀察者模式。
觀察者模式:一個被稱作被觀察者的對象,維護一組被稱為觀察者的對象,這些對象依賴於被觀察者,被觀察者自動將自身的狀態的任何變化通知給它們。
它有幾個重要的部分:
- 被觀察者:維護一組觀察者, 提供用於增加和移除觀察者的方法。
- 觀察者:提供一個更新接口,用於當被觀察者狀態變化時,得到通知。
- 具體的被觀察者:狀態變化時廣播通知給觀察者,保持具體的觀察者的信息。
- 具體的觀察者:保持一個指向具體被觀察者的引用,實現一個更新接口,用於觀察,以便保證自身狀態總是和被觀察者狀態一致的。
發布/訂閱模式
觀察者模式確實很有用,但是在javascript實踐里面,通常我們使用一種叫做發布/訂閱模式的變體來實現觀察者模式。
從圖中也能看到,這兩種模式很相似,但是也有一些值得注意的不同。
發布/訂閱模式使用一個主題/事件頻道,這個頻道處於想要獲取通知的訂閱者和發起事件的發布者之間。這個事件系統允許代碼定義應用相關的事件,這個事件可以傳遞特殊的參數,參數中包含有訂閱者所需要的值。
觀察者模式和發布訂閱模式的不同點:
-
觀察者模式要求想要接受相關通知的觀察者必須到發起這個事件的被觀察者上注冊這個事件。
-
發布/訂閱模式使用一個主題/事件頻道(類似於中介/中間商),可以減少訂閱者和發布者之間的依賴性。
-
發布/訂閱模式中訂閱者可以實現一個合適的事件處理函數,用於注冊和接受由發布者廣播的相關通知。
這兩種模式的優缺點
優點:觀察者和發布/訂閱模式鼓勵人們認真考慮應用不同部分之間的關系,同時幫助我們找出這樣的層,該層中包含有直接的關系,這些關系可以通過一些列的觀察者和被觀察者來替換掉。這種方式可以有效地將一個應用程序切割成小塊,這些小塊耦合度低,從而改善代碼的管理,以及用於潛在的代碼復用。
使用觀察者模式更深層次的動機是,當我們需要維護相關對象的一致性的時候,我們可以避免對象之間的緊密耦合。例如,一個對象可以通知另外一個對象,而不需要知道這個對象的信息。
兩種模式下,觀察者和被觀察者之間都可以存在動態關系。這提供很好的靈活性,而當我們的應用中不同的部分之間緊密耦合的時候,是很難實現這種靈活性的。
然而,正是由於這些優點,這種模式也暴露出一些缺點:
在發布/訂閱模式中,將發布者共訂閱者上解耦,將會在一些情況下,導致很難確保我們應用中的特定部分按照我們預期的那樣正常工作。
例如,發布者可以假設有一個或者多個訂閱者正在監聽它們。比如我們基於這樣的假設,在某些應用處理過程中來記錄或者輸出錯誤日志。如果訂閱者執行日志功能崩潰了(或者因為某些原因不能正常工作),因為系統本身的解耦本質,發布者沒有辦法感知到這些事情。
訂閱者對彼此之間存在沒有感知,對切換發布者的代價無從得知。因為訂閱者和發布者之間的動態關系,更新依賴也很能去追蹤。
以上知識可以幫助我們理解vue響應式的實現,接下來讓我們正式進入正題吧。
vue 實現數據雙向綁定
我們會通過實現以下幾個部分,來實現數據的雙向綁定,這里暫時不考慮對數組的監聽,在接下來的講解中會給出解釋,等到下一章節,會對這部分內容進行重點分析。
- 實現一個監聽器 Observer ,用來劫持並監聽所有屬性,如果屬性發生變化,就通知訂閱者;
- 實現一個訂閱器 Dep,這個對象用來存放 Watcher 對象的實例,對監聽器 Observer 和 訂閱者 Watcher 進行統一管理;
- 實現一個觀察者 Watcher,可以收到屬性的變化通知並執行相應的方法,從而更新視圖;
監聽器 Observer
-
首先我們定義一個 cb 函數,這個函數用來模擬視圖更新,調用它即代表更新視圖,內部可以是一些更新視圖的方法。
-
然后我們定義一個 defineReactive ,這個方法通過 Object.defineProperty 來實現對對象的「響應式」化,
-
參數obj(需要綁定的對象)、key(obj的某一個屬性),val(具體的值)。
-
經過 defineReactive 處理以后,obj 的 key 屬性在「讀」的時候會觸發 reactiveGetter 方法,
-
而在該屬性被「寫」的時候則會觸發 reactiveSetter 方法。
- 還需要在上面再封裝一層 observer。
這個函數傳入一個 obj(需要「響應式」化的對象),通過遍歷所有屬性的方式對該對象的每一個屬性都通過 defineReactive 處理。(注:實際上 observer 會進行遞歸調用,為了便於理解去掉了遞歸的過程)
function cb (val) {
/* 渲染視圖 */
console.log("視圖更新啦~");
}
function defineReactive (obj, key, val) {
Object.defineProperty(obj, key, {
enumerable: true, /* 屬性可枚舉 */
configurable: true, /* 屬性可被修改或刪除 */
get: function reactiveGetter () {
return val; /* 實際上會依賴收集,下一小節會講 */
},
set: function reactiveSetter (newVal) {
if (newVal === val) return;
val = newVal;
cb(newVal);
}
});
}
function observer (obj) {
if (!obj || (typeof obj !== 'object')) {
return;
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
- 最后,可以用 observer 來封裝一個 Vue 。
在 Vue 的構造函數中,對 options 的 data 進行處理,這里的 data 就是平時我們在寫 Vue 項目時組件中的 data 屬性(實際上是一個函數,這里當作一個對象來簡單處理)。
- 這樣我們只要 new 一個 Vue 對象,就會將 data 中的數據進行「響應式」化。
如果我們對 data 的屬性進行下面的操作,就會觸發 cb 方法更新視圖。
class Vue {
/* Vue構造類 */
constructor(options) {
this._data = options.data;
observer(this._data);
}
}
let o = new Vue({
data: {
test: "I am test."
}
});
o._data.test = "hello,world."; /* 視圖更新啦~ */
訂閱者 Dep
實現一個訂閱者 Dep ,它的主要作用是用來存放 Watcher 觀察者對象。
class Dep {
constructor () {
/* 用來存放Watcher對象的數組 */
this.subs = [];
}
/* 在subs中添加一個Watcher對象 */
addSub (sub) {
this.subs.push(sub);
}
/* 通知所有Watcher對象更新視圖 */
notify () {
this.subs.forEach((sub) => {
sub.update();
})
}
}
為了便於理解我們只實現了添加的部分代碼,主要是兩件事情:
- 用 addSub 方法可以在目前的 Dep 對象中增加一個 Watcher 的訂閱操作;
- 用 notify 方法通知目前 Dep 對象的 subs 中的所有 Watcher 對象觸發更新操作。
觀察者 Watcher
class Watcher {
constructor () {
/* 在new一個Watcher對象時將該對象賦值給Dep.target,在get中會用到 */
Dep.target = this;
}
/* 更新視圖的方法 */
update () {
console.log("視圖更新啦~");
}
}
Dep.target = null;
依賴收集
接下來我們修改一下 defineReactive 以及 Vue 的構造函數,來完成依賴收集。
我們在閉包中增加了一個 Dep 類的對象,用來收集 Watcher 對象。
在對象被「讀」的時候,會觸發 reactiveGetter 函數把當前的 Watcher 對象(存放在 Dep.target 中)收集到 Dep 類中去。
之后如果當該對象被「寫」的時候,則會觸發 reactiveSetter 方法,通知 Dep 類調用 notify 來觸發所有 Watcher 對象的 update 方法更新對應視圖。
function defineReactive (obj, key, val) {
/* 一個Dep類對象 */
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
/* 將Dep.target(即當前的Watcher對象存入dep的subs中) */
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter (newVal) {
if (newVal === val) return;
/* 在set的時候觸發dep的notify來通知所有的Watcher對象更新視圖 */
dep.notify();
}
});
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一個Watcher觀察者對象,這時候Dep.target會指向這個Watcher對象 */
new Watcher();
/* 在這里模擬render的過程,為了觸發test屬性的get函數 */
console.log('render~', this._data.test);
}
}
完整的代碼及說明
class Dep {
constructor() {
/* 用來存放Watcher對象的數組 */
this.subs = [];
}
/* 在subs中添加一個Watcher對象 */
addSub(sub) {
this.subs.push(sub);
}
/* 通知所有Watcher對象更新視圖 */
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
class Watcher {
constructor() {
/* 在new一個Watcher對象時將該對象賦值給Dep.target,在get中會用到 */
Dep.target = this;
}
/* 更新視圖的方法 */
update() {
console.log("視圖更新啦~");
}
}
Dep.target = null;
function defineReactive(obj, key, val) {
/* 一個Dep類對象 */
const dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
/* 將Dep.target(即當前的Watcher對象存入dep的subs中) */
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
// 設置新值
// 注意,val 一直在閉包中,此處設置完之后,再 get 時也是會獲取最新的值
val = newVal
/* 在set的時候觸發dep的notify來通知所有的Watcher對象更新視圖 */
dep.notify();
}
});
}
function observer(obj) {
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一個Watcher觀察者對象,這時候Dep.target會指向這個Watcher對象 */
new Watcher();
/* 在這里模擬render的過程,為了觸發test屬性的get函數 */
console.log('render~', this._data.test);
}
}
let o = new Vue({
data: {
test: 'I am a test'
}
})
首先在 observer 的過程中會注冊 get 方法,該方法用來進行「依賴收集」。
在它的閉包中會有一個 Dep 對象,這個對象用來存放 Watcher 對象的實例。
其實「依賴收集」的過程就是把 Watcher 實例存放到對應的 Dep 對象中去。
get 方法可以讓當前的 Watcher 對象(Dep.target)存放到它的 subs 中(addSub)方法,
在數據變化時,set 會調用 Dep 對象的 notify 方法通知它內部所有的 Watcher 對象進行視圖更新。
這是 Object.defineProperty 的 set/get 方法處理的事情,那么「依賴收集」的前提條件還有兩個:
- 觸發 get 方法;
- 新建一個 Watcher 對象。
這個我們在 Vue 的構造類中處理。
新建一個 Watcher 對象只需要 new 出來,這時候 Dep.target 已經指向了這個 new 出來的 Watcher 對象來。
而觸發 get 方法也很簡單,實際上只要把 render function 進行渲染,那么其中的依賴的對象都會被「讀取」,
這里我們通過打印來模擬這個過程,讀取 test 來觸發 get 進行「依賴收集」。
vue 如何深度監聽data變化
Object.defineProperty的缺點
- 深度監聽,需要遞歸到底,一次性計算量大
- 無法監聽新增屬性/刪除屬性(需要 Vue.set Vue.delete)
- 無法原生監聽數組,需要特殊處理
class Dep {
constructor() {
/* 用來存放Watcher對象的數組 */
this.subs = [];
}
/* 在subs中添加一個Watcher對象 */
addSub(sub) {
this.subs.push(sub);
}
/* 通知所有Watcher對象更新視圖 */
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
class Watcher {
constructor() {
/* 在new一個Watcher對象時將該對象賦值給Dep.target,在get中會用到 */
Dep.target = this;
}
/* 更新視圖的方法 */
update() {
console.log("視圖更新啦~");
}
}
Dep.target = null;
function defineReactive(obj, key, val) {
/* 一個Dep類對象 */
const dep = new Dep();
// 深度監聽
observer(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
/* 將Dep.target(即當前的Watcher對象存入dep的subs中) */
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
// 深度監聽
observer(newVal)
// 設置新值
// 注意,val 一直在閉包中,此處設置完之后,再 get 時也是會獲取最新的值
val = newVal
/* 在set的時候觸發dep的notify來通知所有的Watcher對象更新視圖 */
dep.notify();
}
});
}
function observer(obj) {
if (!obj || typeof obj !== 'object') {
return
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一個Watcher觀察者對象,這時候Dep.target會指向這個Watcher對象 */
new Watcher();
// 在這里模擬render的過程
// 測試 深度監聽
console.log('深度監聽', this._data.info.address);
}
}
let o = new Vue({
data: {
name: 'zhangsan',
age: 20,
test: 'I am a test',
info: {
address: '北京' // 需要深度監聽
}
}
})
// 測試 深度監聽
o._data.info.address = '杭州';
console.log(o._data.info.address);
vue 如何監聽數組變化
Vue 的 Observer 對數組做了單獨的處理,對數組的方法進行編譯,並賦值給數組屬性的 proto 屬性上,因為原型鏈的機制,找到對應的方法就不會繼續往上找了。編譯方法中會對一些會增加索引的方法(push,unshift,splice)進行手動 observe。
class Dep {
constructor() {
/* 用來存放Watcher對象的數組 */
this.subs = [];
}
/* 在subs中添加一個Watcher對象 */
addSub(sub) {
this.subs.push(sub);
}
/* 通知所有Watcher對象更新視圖 */
notify() {
this.subs.forEach((sub) => {
sub.update();
})
}
}
class Watcher {
constructor() {
/* 在new一個Watcher對象時將該對象賦值給Dep.target,在get中會用到 */
Dep.target = this;
}
/* 更新視圖的方法 */
update() {
console.log("視圖更新啦~");
}
}
Dep.target = null;
// 重新定義數組原型
const oldArrayProperty = Array.prototype
// 創建新對象,原型指向 oldArrayProperty ,再擴展新的方法不會影響原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
arrProto[methodName] = function() {
console.log('數組相關更新')
oldArrayProperty[methodName].call(this, ...arguments)
// Array.prototype.push.call(this, ...arguments)
}
})
function defineReactive(obj, key, val) {
/* 一個Dep類對象 */
const dep = new Dep();
// 深度監聽
observer(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
/* 將Dep.target(即當前的Watcher對象存入dep的subs中) */
dep.addSub(Dep.target);
return val;
},
set: function reactiveSetter(newVal) {
if (newVal === val) return;
// 深度監聽
observer(newVal)
// 設置新值
// 注意,val 一直在閉包中,此處設置完之后,再 get 時也是會獲取最新的值
val = newVal
/* 在set的時候觸發dep的notify來通知所有的Watcher對象更新視圖 */
dep.notify();
}
});
}
function observer(obj) {
if (!obj || typeof obj !== 'object') {
return
}
// 判斷 如果是 數組
if (Array.isArray(obj)) {
obj.__proto__ = arrProto
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key])
})
}
class Vue {
constructor(options) {
this._data = options.data;
observer(this._data);
/* 新建一個Watcher觀察者對象,這時候Dep.target會指向這個Watcher對象 */
new Watcher();
}
}
let o = new Vue({
data: {
name: 'zhangsan',
age: 20,
test: 'I am a test',
info: {
address: '北京' // 需要深度監聽
},
nums: [10, 20, 30]
}
})
// 測試 監聽數組
o._data.nums.push(7)
console.log('數組監聽', o._data.nums);
參考文章: