全手打原創,轉載請標明出處:https://www.cnblogs.com/dreamsqin/p/14986941.html, 多謝,=。=~(如果對你有幫助的話請幫我點個贊啦)
作爲一個Web前端開發人員,使用Vue框架進行項目開發已經有一陣子,掐指一算,是時候認真探索一下Vue的底層了,以前的瞭解比較偏理論,這一次打算在弄清基本原理的前提下自己手寫Vue中的核心部分,也許這樣我纔敢說自己“深入理解”了Vue。上一篇完成MVue框架搭建,實現數據劫持並重寫
setter
及getter
,本篇就來手擼發佈訂閱模式~
依賴收集與追蹤
上一篇的結尾我們通過console.log(`${key}屬性更新了:${val}`);
預留了視圖更新的代碼位置,也就是當數據發生變更時,我們需要做數據對應的視圖更新,那麼到底更新哪些視圖,就是依賴收集的意義。首先看一個日常的例子:
new Vue({
template:
`<div>
<span>{{name1}}</span>
<span>{{name2}}</span>
<span>{{name1}}</span>
</div>`,
data: {
name1: 'name1',
name2: 'name2',
name3: 'name3'
},
created() {
this.name1 = 'change name1';
this.name3 = 'change name3';
}
});
根據頁面綁定的data我們可以整理出以下邏輯:
name1
被修改,視圖更新2處;name2
被修改,視圖更新1處;name3
被修改,視圖無需更新;
所以我們需要做的事是掃描視圖收集依賴,得知視圖中哪裏對數據有依賴後,對應數據變更時就可以得到通知,接下來可以對照第一篇《Vue底層學習1——原理解析》中的簡化版原理圖實現,Dep
和Watcher
遵從發佈訂閱模式,也是本篇的重點,建議後續代碼結合着原理圖看,思路會更清晰哦~
依賴對象
首先需要實現的是依賴對象Dep
,主要用於依賴收集,管理Watcher
,它與數據屬性一一對應。其中需要提供2個方法:添加觀察者、通知觀察者。
/*** MVue.js ***/
// 依賴收集,管理Watcher
class Dep {
constructor() {
// 存放所有的依賴(Watcher)
this.deps = [];
}
// 在deps中添加一個觀察者對象
addDep(dep) {
this.deps.push(dep);
}
// 通知所有的觀察者去更新視圖
notify() {
this.deps.forEach((dep => dep.update()));
}
}
觀察者對象
接下來實現觀察者對象Watcher
,主要用於視圖更新。其中需要提供更新視圖的方法,構造函數中有一個看似奇怪的操作,後續會詳細說明。
/*** MVue.js ***/
class Watcher {
constructor() {
// 將當前Watcher的實例指定到Dep靜態屬性target
Dep.target = this;
}
update() {
// 預留視圖更新
console.log('數據更新了,需要我們更新視圖');
}
}
自建框架整合
根據上面的實現,我們把代碼跟之前的做一下初步整合:
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {
// 數據緩存
this.$options = options;
this.$data = options.data;
// 數據遍歷
this.observe(this.$data);
}
observe(data) {
// 確定data存在並且爲對象
if (!data || typeof data !== 'object') {
return;
}
// 遍歷data對象
Object.keys(data).forEach(key => {
// 重寫對象屬性的getter和setter,實現數據的響應化
this.defineReactive(data, key, data[key]);
})
}
defineReactive(obj, key, val) {
// 解決數據嵌套,遞歸實現深度遍歷
this.observe(val);
Object.defineProperty(obj, key, {
get: function() {
return val;
},
set: function(newVal) {
// 判斷屬性值是否發生變化
if (newVal === val) {
return;
}
val = newVal;
// 預留視圖更新
console.log(`${key}屬性更新了:${val}`);
}
})
}
}
// 依賴收集,管理Watcher
class Dep {
constructor() {
// 存放所有的依賴
this.deps = [];
}
// 在deps中添加一個觀察者對象
addDep(dep) {
this.deps.push(dep);
}
// 通知所有的觀察者去更新視圖
notify() {
this.deps.forEach((dep => dep.update()));
}
}
class Watcher {
constructor() {
// 將當前Watcher的實例指定到Dep靜態屬性target
Dep.target = this;
}
update() {
// 預留視圖更新
console.log('數據更新了,需要我們更新視圖');
}
}
接下來需要將原本預留在defineReactive
中的視圖更新替換成發佈訂閱模式:
- 第1步:我們需要在
defineReactive
中定義Dep
,這樣在將來的某個時刻就能把收集的依賴(Watcher
)放進去; - 第2步:我們需要替換掉
Line42
中預留的部分,修改爲dep.notify()
,實現通知Watcher
的功能,最終由Watcher
完成視圖更新。那麼Watcher
怎麼來?交給第三步; - 第3步:在
MVue
構造函數中先模擬一下Watcher
的創建過程,即new Watcher()
,接下來就發生了神奇的現象,也就是前面提到的Watcher
構造函數中那行奇怪的操作,Watcher
當前的實例會被指定到Dep
的target
靜態屬性,這樣做的目的就是爲了將Watcher
添加到之前創建的Dep
中; - 第4步:在屬性的
getter
中添加依賴收集,即dep.addDep(Dep.target)
,當然需要先判斷target
是否存在; - 第5步:要想依賴可以成功收集,那麼我們需要觸發
getter
,也就是讀取一下屬性,同樣在MVue
構造函數中模擬;
修改後代碼如下:
/*** MVue.js ***/
// new MVue({ data: {...} })
class MVue {
constructor(options) {
// 數據緩存
this.$options = options;
this.$data = options.data;
// 數據遍歷
this.observe(this.$data);
// 模擬Watcher的創建過程——第3步
new Watcher();
// 模擬屬性讀取,激活getter,實現依賴收集——第5步
this.$data.name;
// 模擬Watcher的創建過程——第2步
new Watcher();
// 模擬屬性讀取,激活getter,實現依賴收集——第5步
this.$data.infoObj.location;
}
observe(data) {
// 確定data存在並且爲對象
if (!data || typeof data !== 'object') {
return;
}
// 遍歷data對象
Object.keys(data).forEach(key => {
// 重寫對象屬性的getter和setter,實現數據的響應化
this.defineReactive(data, key, data[key]);
})
}
defineReactive(obj, key, val) {
// 解決數據嵌套,遞歸實現深度遍歷
this.observe(val);
// 初始化Dep——第1步
const dep = new Dep();
Object.defineProperty(obj, key, {
get: function() {
// 依賴收集,將當前屬性對應的Watcher添加至Dep中——第4步
Dep.target && dep.addDep(Dep.target);
return val;
},
set: function(newVal) {
// 判斷屬性值是否發生變化
if (newVal === val) {
return;
}
val = newVal;
// 通知觀察者更新視圖——第2步
dep.notify();
}
})
}
}
// 依賴收集,管理Watcher
class Dep {
constructor() {
// 存放所有的依賴
this.deps = [];
}
// 在deps中添加一個觀察者對象
addDep(dep) {
this.deps.push(dep);
}
// 通知所有的觀察者去更新視圖
notify() {
this.deps.forEach((dep => dep.update()));
}
}
class Watcher {
constructor() {
// 將當前Watcher的實例指定到Dep靜態屬性target
Dep.target = this;
}
update() {
// 預留視圖更新
console.log('數據更新了,需要我們更新視圖');
}
}
自建框架測試demo1
運行上一篇《Vue底層學習2——手擼數據響應化》中的demo1案例,執行結果如下,說明我們的發佈訂閱模式成功替換:
總結
本篇實現發佈訂閱模式的整體過程可以歸納如下:新增一個Dep
類的實例來做依賴收集。讀取數據時,會觸發getter
把當前的Watcher
(存放在Dep.target
中)收集到Dep
實例中。寫入數據時,會觸發setter
通知Dep
類調用notify
方法,以此觸發所有Watcher
的update
方法來更新對應的視圖。
最後提個小tips——每個Dep
針對單個屬性,有多少個數據屬性就有多少Dep
,但是一個Dep
中可能有多個Watcher
,因爲一個屬性可能在視圖中出現多次。
參考資料
1、Vue源碼:https://github.com/vuejs/vue;