Vue源碼解析---數據的雙向綁定

本文主要抽離Vue源碼中數據雙向綁定的核心代碼,解析Vue是如何實現數據的雙向綁定
核心思想是ES5的Object.defineProperty()發佈-訂閱模式數組

總體結構

  1. 改造Vue實例中的data,經過Object.defineProperty()將其全部屬性設置爲訪問器屬性
  2. 對每一個屬性添加Observer,並在observer中添加訂閱者對象序列Dep
  3. 添加訂閱者對象Watcher,每次初始化的時候添加到對應data屬性中的Dep之中

全部,咱們從代碼的角度將總體分爲三個部分:監聽數據變化管理訂閱者訂閱者this

監聽數據變化

使用ES5中的Object.defineProperty將data中的屬性修改成訪問者屬性雙向綁定

// Dep用於訂閱者的存儲和收集,將在下面實現
import Dep from 'Dep'
// Observer類用於給data屬性添加set&get方法
export default class Observer{
    constructor(value){
        this.value = value
        this.walk(value)
    }
    walk(value){
        Object.keys(value).forEach(key => this.convert(key, value[key]))
    }
    convert(key, val){
        defineReactive(this.value, key, val)
    }
}
export function defineReactive(obj, key, val){
    // 用於存放某個屬性的全部訂閱者
    var dep = new Dep()
    // 給當前屬性的值添加監聽
    var chlidOb = observe(val)
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        get: ()=> {
            console.log('get value')
            // 若是Dep類存在target屬性,將其添加到dep實例的subs數組中
            // target指向一個Watcher實例,每一個Watcher都是一個訂閱者
            // Watcher實例在實例化過程當中,會讀取data中的某個屬性,從而觸發當前get方法
            if(Dep.target){
                dep.addSub(Dep.target)
            }
            return val
        },
        set: (newVal) => {
            console.log('new value setted')
            if(val === newVal) return
            val = newVal
            // 對新值進行監聽
            chlidOb = observe(newVal)
            // 通知全部訂閱者,數值被改變了
            dep.notify()
        }
    })
}
export function observe(value){
    // 當值不存在,或者不是複雜數據類型時,再也不須要繼續深刻監聽
    if(!value || typeof value !== 'object'){
        return
    }
    return new Observer(value)
}

管理訂閱者

對訂閱者進行收集,存儲和通知代理

export default class Dep{
    constructor(){
        this.subs = []
    }
    addSub(sub){
        // 在收集訂閱者的時候,須要對subs中的訂閱者進行去重,這邊不詳細解析
        this.subs.push(sub)
    }
    notify(){
        // 通知全部的訂閱者(Watcher),觸發訂閱者的相應邏輯處理
        this.subs.forEach((sub) => sub.update())
    }
}

訂閱者

每一個watcher對象都是對data中每一個屬性的訂閱,是多對一的關係,每一個watcher只能對應一個data屬性,而一個data屬性能夠對應多個watchercode

import Dep from 'Dep'
export default class Watcher{
    constructor(vm, expOrFn, cb){
        this.vm = vm // 被訂閱的數據必定來自於當前Vue實例
        this.cb = cb // 當數據更新時想要作的事情
        this.expOrFn = expOrFn // 被訂閱的數據
        this.val = this.get() // 維護更新以前的數據
    }
    // 對外暴露的接口,用於在訂閱的數據被更新時,由訂閱者管理員(Dep)調用
    update(){
        this.run()
    }
    run(){
        const val = this.get()
        if(val !== this.val){
            this.val = val;
            this.cb.call(this.vm)
        }
    }
    get(){
        // 當前訂閱者(Watcher)讀取被訂閱數據的最新更新後的值時,通知訂閱者管理員收集當前訂閱者
        Dep.target = this
        const val = this.vm._data[this.expOrFn]
        // 置空,用於下一個Watcher使用
        Dep.target = null
        return val;
    }
}

實例

下邊咱們建立一個簡易的Vue來實際運行下對數據的監聽server

import Observer, {observe} from 'Observer'
import Watcher from 'Watcher'
export default class Vue{
    constructor(options = {}){
        // 簡化了$options的處理
        this.$options = options
        // 簡化了對data的處理
        let data = this._data = this.$options.data
        // 將全部data最外層屬性代理到Vue實例上
        Object.keys(data).forEach(key => this._proxy(key))
        // 監聽數據
        observe(data)
    }
    // 對外暴露調用訂閱者的接口,內部主要在指令中使用訂閱者
    $watch(expOrFn, cb){
        new Watcher(this, expOrFn, cb)
    }
    _proxy(key){
        Object.defineProperty(this, key, {
            configurable: true,
            enumerable: true,
            get: () => this._data[key],
            set: (val) => {
                this._data[key] = val
            } 
        })
    }
}
import Vue from './Vue';
let demo = new Vue({
    data: {
        'a': {
            'ab': {
                'c': 'C'
            }
        },
        'b': [
            'bb': 'BB',
            'bbb': 'BBB'
        ],
        'c': 'C'
    }
});
demo.$watch('c', () => console.log('c is changed'));
// get value
demo.$watch('a.ab', () => console.log('a.ab is changed'));
demo.$watch('b', () => console.log('b is changed'));
// get value
demo.c = 'CCC';
// new value setted
// get value
// c is changed
demo.a.ab = 'AB';
// get value
// new value setted
demo.b.push({'bbbb': 'BBBB'});
// get value

根據實例的輸出結果,咱們很奇怪的發現,只有對簡單的數據監聽才能實現數據雙向綁定。對象

  1. demo.$watch('a.ab', () => console.log('a.ab is changed'))註冊訂閱者並無調用getter
  2. demo.a.ab = 'AB'有監聽到數據的變化,並無調用對應的callback
  3. demo.b.push({'bbbb': 'BBBB'})對數值進行操做,並無調用對應的callback

這是爲何呢?由於咱們對數據的監聽的實現,目前僅限於簡單對應,對於某個屬性內部有更多複雜屬性時,就無能爲力了。接口

爲了實現進一步對數據和複雜對象的監聽,請戳Vue源碼解析---數組的雙向綁定Vue源碼解析---複雜隊形的雙向綁定get

相關文章
相關標籤/搜索