Vue2 Observer實例dep和閉包中dep區別詳解
start
此前學習 Vue2 源碼。對 Vue 源碼中兩次出現的new Dep(),不清楚它們的區別,寫一個文章記錄一下。
Vue2 源碼中有兩處通過new Dep生成dep實例:
1. Observer實例上的dep
export class Observer { value; dep; vmCount; constructor(value) { this.value = value; /* Observer的實例上有一個 dep 屬性 */ this.dep = new Dep(); def(value, "__ob__", this); if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods); } else { copyAugment(value, arrayMethods, arrayKeys); } this.observeArray(value); } else { this.walk(value); } }
2. 定義getter,setter中dep
export function defineReactive(obj, key, val, customSetter, shallown) { /* 這個地方也定義瞭一個dep */ const dep = new Dep() const property = Object.getOwnPropertyDescriptor(obj, key) if (property && property.configurable === false) { return } const getter = property && property.get const setter = property && property.set if ((!getter || setter) && arguments.length === 2) { val = obj[key] } let childOb = !shallow && observe(val) Object.defineProperty(obj, key, { enumerable, configurable, get: function reactiveGetter() { const value = getter ? getter.call(obj) : val if (Dep.target) { dep.depend() if (childOb) { childOb.dep.depend() if (Array.isArray(value)) { dependArray(value) } } } return value }, set: function reactiveSetter(newVal) { /* ... */ }, }) }
為瞭方便後續介紹,我對兩種dep的名稱做一下縮減。
1. Observer實例上的dep => Observer實例上的dep
2. 定義getter,setter中dep => 閉包中的dep
既然是 new Dep(),肯定是想要實現某些功能。想要弄懂這兩種 dep 的區別,先看看在源碼中它們如何工作的。
1. 依賴收集
在打包輸出的dist文件夾中,找到完整的 Vue.js 源碼,全局搜索一下dep.depend() (收集依賴的方法)。
僅三處做瞭依賴收集
// 第 1 種情況 // 當觸發對象屬性 getter 的時候,`閉包中的dep`會收集依賴。 dep.depend() // 第 2 種情況 // 當觸發對象屬性 getter 的時候,若屬性值有更深的子層級。`Observer實例上的dep`會收集依賴。 childOb.dep.depend() // 第 3 種情況 // 當觸發對象屬性 getter的時候,數據有更深的子層級且子層級是數組類型。遞歸遍歷數組的每一項,若數組項上存在`Observer的實例`,則對應的`Observer實例上的dep`會收集依賴。 function dependArray(value) { for (var e = void 0, i = 0, l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() if (Array.isArray(e)) { dependArray(e) } } }
情況說明:
- 當觸發對象屬性 getter 的時候,閉包中的dep會收集依賴。
- 當觸發對象屬性 getter 的時候,若屬性值有更深的子層級。Observer實例上的dep會收集依賴。
當觸發對象屬性 getter 的時候,數據有更深的子層級且子層級是數組類型。遞歸遍歷數組的每一項,若數組項上存在Observer的實例,則對應的Observer實例上的dep會收集依賴。
寫到這裡我有點疑惑,為什麼數組的情況,還要額外處理?
- 因為數組中如果存儲的不是對象或數組,對應數組項不會有Observer的實例;
- 因為數組中如果存儲的是對象或數組,對應數組項身上就會有Observer的實例;
- (核心原因:數組的每一項並不是都會被設置 getter,所以這裡需要遞歸處理一下數組)
舉個例子
寫一個 Html 頁面測試
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8" /> <title>lazy_tomato</title> </head> <body> <div id="app"> <div>{{ c }}</div> </div> <script src="./vue.js"></script> <script> new Vue({ el: '#app', data() { return { b: '你好呀', c: [{}], d: {}, } }, }) </script> </body> </html>
修改一下Vue源碼,加入打印:
if (Dep.target) { dep.depend() /* 這裡 */ console.log('111') if (childOb) { childOb.dep.depend() /* 這裡 */ console.log('222') if (Array.isArray(value)) { dependArray(value) } } } function dependArray(value) { for (var e = void 0, i = 0, l = value.length; i < l; i++) { e = value[i] e && e.__ob__ && e.__ob__.dep.depend() if (e && e.__ob__ && e.__ob__.dep) { /* 這裡 */ console.log('333') } if (Array.isArray(e)) { dependArray(e) } } }
實驗結果:
- 如果頁面沒有使用 data 中的數據,那麼三種情況的 dep.depend() 都不會觸發。(因為沒有觸發 getter)
- 如果頁面僅僅使用到瞭 b,b 是簡單類型的數據,沒有子層級, 1會觸發, 2、3不觸發;
- 如果頁面僅僅使用到瞭 c,c 是數組類型的數據,而且數組中的項是對象,1、2、3會觸發 2不觸發;
- 如果頁面僅僅使用到瞭 d,d 是對象類型的數據,有子層級,1、2會觸發, 3不觸發;
結論: 由上面的結果,可以得到結論:
- 閉包中的dep 關註的是對象屬性;
- Observer實例上的dep,關註的是對象中屬性值是對象或者是數組的情況;
2. 通知更新
在打包輸出的dist文件夾中,找到完整的 Vue.js 源碼,全局搜索一下dep.notify()(通知更新的方法)。僅四處做瞭依賴收集。
// 第 1 種情況 // 當觸發對象屬性 setter 的時候,`閉包中的dep`會通知更新。 dep.notify() // 還有三處,都是使用 `ob.dep.notify()` 的方式會通知更新。 // 場景分別為: // 1. 改寫數組的七個方法 // 2. set 方法 // 3. del 方法
結論:
- 閉包中的dep 用於由對象本身修改而觸發 setter 函數導致閉包中的 Dep 通知所有的 Watcher 對象。
- Observer實例上的dep 則是在對象本身增刪屬性或者數組變化的時候被觸發的 Dep。
思考
看到上述的內容,可以想到兩種 dep 其實是各司其職。
- 閉包中的 dep 用於管理對象本身修改而觸發的依賴。
- Observer 實例上的 dep 用於對象本身增刪屬性或者數組變化的時候被觸發的依賴。
它可以用於彌補 Object.defineProperty() 的缺陷。
- 對象屬性 新增或者刪除;
- 數組方法不支持響應式;
- 通過數組索引修改數據項;
拓展一
如果把 Observer 中初始化 dep 的代碼註釋掉,那麼:$set,$del,重寫的七種數組方法 都將失效。
class Observer { constructor(value) { // this.dep = new Dep(); // 正確的寫法 this.dep = { depend() {}, notify() {} } /* ... */ } }
拓展二
既然源碼中的 $set,$del,重寫的七種數組方法 ,通知更新,使用的是ob.dep.notify()。 那我們是否可以自己手動通知嗎?
理論是可行的,但是 Vue 源碼相對來說,做瞭更多特殊場景的考慮。所以用官方提供的 API 更可靠。
手動通知的案例
<!DOCTYPE html> <html lang="zh"> <head> <meta charset="UTF-8" /> <title>lazy_tomato</title> </head> <body> <div id="app"> <ul> <li v-for="item in list" :key="item">{{item}}</li> </ul> <h2 @click="foo">點擊我通過數組索引添加數據</h2> <h2 @click="bar">點擊我手動更新通知依賴更新</h2> </div> <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/vue.js"></script> <script> new Vue({ el: '#app', data() { return { list: [1, 2, 3], } }, methods: { // 點擊我向數組中添加數據 foo() { let length = this.list.length this.list[length] = 'tomato' + length // 通過數組索引修改數據,默認是無法通知依賴更新的。 console.log(this.list) }, // 點擊我手動更新通知更新 bar() { this.list.__ob__.dep.notify() }, }, }) </script> </body> </html>
效果圖:
end
- 本文就 Dep 的初始化和基礎的使用場景做瞭學習。
再總結一下:
- 閉包中的dep 用於由對象本身修改而觸發 setter 函數導致閉包中的 Dep 通知所有的 Watcher 對象。
- Observer實例上的dep 則是在對象本身增刪屬性或者數組變化的時候被觸發的 Dep。
更多關於Vue2 Observer與dep閉包的資料請關註WalkonNet其它相關文章!