觀察者模式定義了物件間一對多的依賴關係。即被觀察者狀態發生變動時,所有依賴於它的觀察者都會得到通知並自動更新。解決了主體物件和觀察者之間功能的耦合。
Vue中基於 Observer、Dep、Watcher 三個類實現了觀察者模式
dep.depend()
進行依賴收集;資料變更時,呼叫dep.notify()
通知觀察者更新檢視。我們的資料就是被觀察者dep 和 watcher 是一個多對多的關係。每個元件都對應一個渲染 watcher,每個響應式屬性都有一個 dep 收集器。一個元件可以包含多個屬性(一個 watcher 對應多個 dep),一個屬性可以被多個元件使用(一個 dep 對應多個 watcher)
我們需要給每個屬性都增加一個 dep 收集器,目的就是收集 watcher。當響應式資料發生變化時,更新收集的所有 watcher
dep.depend()
,通知 watcher 訂閱 dep,然後在 watcher內部執行dep.addSub()
,通知 dep 收集 watcherdep.notify()
,通知所有的觀察者 watcher 進行 update 更新操作Dep有一個靜態屬性 target,全域性唯一,Dep.target 是當前正在執行的 watcher 範例,這是一個非常巧妙的設計!因為在同一時間只能有一個全域性的 watcher
注意:
渲染/更新完畢後我們會立即清空 Dep.target,保證了只有在模版渲染/更新階段的取值操作才會進行依賴收集。之後我們手動進行資料存取時,不會觸發依賴收集,因為此時 Dep.target 已經重置為 null
let id = 0
class Dep {
constructor() {
this.id = id++
// 依賴收集,收集當前屬性對應的觀察者 watcher
this.subs = []
}
// 通知 watcher 收集 dep
depend() {
Dep.target.addDep(this)
}
// 讓當前的 dep收集 watcher
addSub(watcher) {
this.subs.push(watcher)
}
// 通知subs 中的所有 watcher 去更新
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 當前渲染的 watcher,靜態變數
Dep.target = null
export default Dep
不同元件有不同的 watcher。我們先只需關注渲染watcher。計算屬性watcer和監聽器watcher後面會單獨講!
watcher 負責訂閱 dep ,並在訂閱的同時執行dep.addSub()
,讓 dep 也收集 watcher。當接收到 dep 釋出的訊息時(通過 dep.notify()
),執行 update 重新渲染
當我們初始化元件時,在 mountComponent 方法內會範例化一個渲染 watcher,其回撥就是 vm._update(vm._render())
import Watcher from './observe/watcher'
// 初始化元素
export function mountComponent(vm, el) {
vm.$el = el
const updateComponent = () => {
vm._update(vm._render())
}
// true用於標識是一個渲染watcher
const watcher = new Watcher(vm, updateComponent, true)
}
當我們範例化渲染 watcher 的時候,在建構函式中會把回撥賦給this.getter
,並呼叫this.get()
方法。
這時!!!我們會把當前的渲染 watcher 放到 Dep.target 上,並在執行完回撥渲染檢視後,立即清空 Dep.target,保證了只有在模版渲染/更新階段的取值操作才會進行依賴收集
import Dep from './dep'
let id = 0
class Watcher {
constructor(vm, fn) {
this.id = id++
this.getter = fn
this.deps = [] // 收集當前 watcher 對應被觀察者屬性的 dep
this.depsId = new Set()
this.get()
}
// 收集 dep
addDep(dep) {
let id = dep.id
// 去重,一個元件 可對應 多個屬性 重複的屬性不用再次記錄
if (!this.depsId.has(id)) {
this.deps.push(dep)
this.depsId.add(id)
dep.addSub(this) // watcher已經收集了去重後的 dep,同時讓 dep也收集 watcher
}
}
// 執行 watcher 回撥
get() {
Dep.target = this // Dep.target 是一個靜態屬性
this.getter() // 執行vm._render時,會劫持到資料存取,呼叫 dep.depend() 進行依賴收集
Dep.target = null // 渲染完畢置空,保證了只有在模版渲染階段的取值操作才會進行依賴收集
}
// 重新渲染
update() {
this.get()
}
}
我們是如何觸發依賴收集的呢?
在執行this.getter()
回撥時,我們會呼叫vm._render()
,在_s()
方法中會去 vm 上取值,這時我們劫持到資料存取走到 getter,進而執行dep.depend()
進行依賴收集
流程:vm._render()
->vm.$options.render.call(vm)
-> with(this){ return _c('div',null,_v(_s(name))) }
-> 會去作用域鏈 this 上取 name
在 MDN 中是這樣描述 with 的
JavaScript 查詢某個未使用名稱空間的變數時,會通過作用域鏈來查詢,作用域鏈是跟執行程式碼的 context 或者包含這個變數的函數有關。'with'語句將某個物件新增到作用域鏈的頂部,如果在 statement 中有某個未使用名稱空間的變數,跟作用域鏈中的某個屬性同名,則這個變數將指向這個屬性值
我們只會在 Observer 類 和 defineReactive 函數中範例化 dep。在 getter 方法中執行dep.depend()
依賴收集,在 setter 方法中執行dep.notity()
派發更新通知
依賴收集的入口就是在Object.defineProperty
的 getter 中,我們重點關注2個地方,一個是在我們範例化 dep 的時機,另一個是為什麼遞迴依賴收集。我們先來看下程式碼
class Observer {
constructor(data) {
// 給陣列/物件的範例都增加一個 dep
this.dep = new Dep()
// data.__ob__ = this 給資料加了一個標識 如果資料上有__ob__ 則說明這個屬性被觀測過了
Object.defineProperty(data, '__ob__', {
value: this,
enumerable: false, // 將__ob__ 變成不可列舉
})
if (Array.isArray(data)) {
// 重寫可以修改陣列本身的方法 7個方法
data.__proto__ = newArrayProto
this.observeArray(data)
} else {
this.walk(data)
}
}
// 迴圈物件"重新定義屬性",對屬性依次劫持,效能差
walk(data) {
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
// 觀測陣列
observeArray(data) {
data.forEach(item => observe(item))
}
}
// 深層次巢狀會遞迴處理,遞迴多了效能就差
function dependArray(value) {
for (let i = 0; i < value.length; i++) {
let current = value[i]
current.__ob__ && current.__ob__.dep.depend()
if (Array.isArray(current)) {
dependArray(current)
}
}
}
export function defineReactive(target, key, value) {
// 深度屬性劫持;給所有的陣列/物件的範例都增加一個 dep,childOb.dep 用來收集依賴
let childOb = observe(value)
let dep = new Dep() // 每一個屬性都有自己的 dep
Object.defineProperty(target, key, {
get() {
// 保證了只有在模版渲染階段的取值操作才會進行依賴收集
if (Dep.target) {
dep.depend() // 依賴收集
if (childOb) {
childOb.dep.depend() // 讓陣列/物件範例本身也實現依賴收集,$set原理
if (Array.isArray(value)) { // 陣列需要遞迴處理
dependArray(value)
}
}
}
return value
},
set(newValue) { ... },
})
}
我們只會在 Observer 類 和 defineReactive 函數中範例化 dep
我們為什麼要在 Observer 類中範例化 dep?
如果想要在通過索引直接改變陣列成員或物件新增屬性後,也可以派發更新。那我們必須要給陣列/物件範例本身增加 dep 收集器,這樣就可以通過 xxx.__ob__.dep.notify()
手動觸發 watcher 更新了
這其實就是 vm.$set 的內部原理!!!
陣列中的巢狀陣列/物件沒辦法走到 Object.defineProperty,無法在 getter 方法中執行dep.depend()
依賴收集,所以需要遞迴收集
舉個栗子:data: {arr: ['a', 'b', ['c', 'd', 'e', ['f', 'g']], {name: 'libc'}]}
我們可以劫持 data.arr,並觸發 arr 範例上的 dep 依賴收集,然後迴圈觸發 arr 成員的 dep依賴收集。對於深層陣列巢狀的['f', 'g']
,我們則需要遞迴觸發其範例上的 dep 依賴收集
在 setter 方法中執行dep.notity()
,通知所有的訂閱者,派發更新通知
注: 這個 dep 是在 defineReactive 函數中範例化的。 它是被閉包讀取的區域性變數,會駐留到記憶體中且不會汙染全域性
Object.defineProperty(target, key, {
get() { ... },
set(newValue) {
if (newValue === value) return
// 修改後重新觀測。新值為物件的話,可以劫持其資料。並給所有的陣列/物件的範例都增加一個 dep
observe(newValue)
value = newValue
// 通知 watcher 更新
dep.notify()
},
})
在陣列的重寫方法中執行xxx.__ob__.dep.notify()
,通知所有的訂閱者,派發更新通知
注: 這個 dep 是在 Observer 類中範例化的,我們給陣列/物件的範例都增加一個 dep。可以通過響應式資料的__ob__獲取到範例,進而存取範例上的屬性和方法
let oldArrayProto = Array.prototype // 獲取陣列的原型
// newArrayProto.__proto__ = oldArrayProto
export let newArrayProto = Object.create(oldArrayProto)
// 找到所有的變異方法
let methods = ['push', 'pop', 'shift', 'unshift', 'reverse', 'sort', 'splice'] // concat slice 都不會改變原陣列
methods.forEach(method => {
// 這裡重寫了陣列的方法
newArrayProto[method] = function (...args) {
// args reset引數收集,args為真正陣列,arguments為偽陣列
const result = oldArrayProto[method].call(this, ...args) // 內部呼叫原來的方法,函數的劫持,切片程式設計
// 我們需要對新增的資料再次進行劫持
let inserted
let ob = this.__ob__
switch (method) {
case 'push':
case 'unshift': // arr.unshift(1,2,3)
inserted = args
break
case 'splice': // arr.splice(0,1,{a:1},{a:1})
inserted = args.slice(2)
default:
break
}
if (inserted) {
// 對新增的內容再次進行觀測
ob.observeArray(inserted)
}
// 通知 watcher 更新渲染
ob.dep.notify()
return result
}
})