vue 雙向綁定
vue 雙向綁定,問就是 Object.defineProperty 劫持數據獲得狀態變更,發佈訂閱者模式進行變更通知。好吧,現在地球人都知道這兩句話了,要是別人再問你實現細節,我們應該如何回答呢。也是爲了讓自己更加了解,從自己大概知道和能與人清楚表達,這就是今天寫文章的目的了。
閱讀前,你需要具備 vue 生命週期,Object.defineProperty,以及發佈訂閱者模式(相關博客)的一些知識。
劫持數據
......
callHook(vm, 'beforeCreate')
initInjections(vm) //注入 Data/prop 數據
initProvide(vm) //注入完成
callHook(vm, 'created')
在進入 beforeCreate 週期後,會注入 data/prop 等數據,數據劫持就是發生在此時(initInjections 函數)。
function initInjections(){
......
Object.keys(result).forEach(key => {
defineReactive(vm, key, result[key])
})
}
export function defineReactive( obj: Object, key: string, val: any ......){
......
Object.defineProperty(obj, key, {
get:function(){
......
},
set:function(){
......
}
}
}
把對象的屬性鍵轉換爲getter/setter,一旦劫持了數據,我們就可以爲所欲爲了。
但是這裏只轉換了第一層數據,我們還要嵌套遍歷對象。
Observer 監聽類
在轉化對象屬性 defineReactive 函數中,會先 new 一個監聽類(Observer)。監聽類通過遞歸數據,把 getter/setter 附加到每個被觀察對象(讓數據變化綁定訂閱和通知這兩個行爲)。
function observe(value){
let ob
if (hasOwn(value, '__ob__') //通過有無__ob__屬性判斷是否處於監聽中
ob = value.__ob__
else
ob = new Observer(value)
return ob
}
export class Observer{
......
def(value, '__ob__', this) //爲屬性添加屬性__ob__,指向監聽實例
this.walk(value)
}
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]]) //通過第三個參數遞歸,繼續監聽對象
}
}
單單只是獲取數據狀態變更不行,還得發送通知。
Dep 調度中心類
瞭解發佈訂閱者模式就該知道,訂閱者(觀察者)把自己想訂閱的事件註冊到調度中心,當該事件觸發時候,發佈者(目標)發佈該事件到調度中心,由調度中心統一調度訂閱者註冊到調度中心的處理代碼。
我們通過 getter 爲每個訂閱者訂閱事件,setter 來發送通知。
export function defineReactive( obj, key, val ......){
......
const dep = new Dep() //每一個數據都會有一個專門的 Dep 實例。
observe(val) //遞歸遍歷數據
Object.defineProperty(obj, key, {
get:function(){
......
dep.depend() //訂閱事件
return value
},
set:function(){
......
dep.notify() //發送通知
}
}
}
在 defineReactive 的 get 裏面訂閱事件。然後在 set 中發送通知。
export class Dep{
......
constructor () {
this.subs = [] //依賴隊列
}
//訂閱
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
//發送通知
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
//添加依賴(訂閱者)
addSub (sub: Watcher) {
this.subs.push(sub)
}
}
Dep.target 後續會講到,指向目標 watcher 實例
Watcher 訂閱者類
......
callHook(vm, 'beforeMount')
new Watcher()
......
在 beforeMount 生命週期後,會 new 一個 Watcher 對象。
export class Watcher {
......
constructor (){
this.cb = cb
this.get()
}
get(){
......
Dep.target = this; //讓當前組件實例的 Dep.target 指向這個 Watcher 對象
this.getter.call(vm, vm) //執行一次 getter
Dep.target = null
return value
}
addDep(dep){ //爲調度中心依賴隊列添加當前 Watcher 對象
dep.addSub(this)
}
update(){
this.cb.call(this.vm, value, oldValue) //執行 setter
}
}
生成訂閱者 watcher,會執行一次數據的 getter(通過監聽類遞歸,監聽並讓每一個屬性都有一個調度中心),然後通過訂閱事件,把這個訂閱者關聯到所有調度中心的依賴內(通過 Dep.target 的指向互相調用)。當數據變化時,該處的調度中心就會通知相應的訂閱者。
每個數據都有一個調度中心,每個組件實例都有一個訂閱者
總結
實現數據的雙向綁定,首先要對數據進行劫持監聽,所以需要設置一個監聽器Observer,用來監聽所有屬性。如果屬性發上變化了,就需要告訴訂閱者Watcher看是否需要更新。我們通過消息調度中心Dep來專門收集這些訂閱者併發布消息通知,以此在監聽器Observer和訂閱者Watcher之間進行統一管理。