讀vue的變化偵測

由來

最近在看「深刻淺出vuejs」,第一篇變化偵測,想把本身的理解總結一下。vue

Object的變化偵測

總結一下我看了後的理解

  1. 將數據變成可響應式的,即將數據變成可監聽的。經過Observer類來實現
  2. 依賴是什麼?就是這個數據在哪裏用到了,至關於this當前的上下文;因此當數據變化時,咱們能夠通知他,觸發update,從而觸發渲染
  3. 那麼這個依賴,誰來收集存起來。經過Dep類來實現

先看Observer

class Observer {
    constructor(value) {
        this.value = value
        if(!Array.isArray(value) {
            this.walk(value)
        }
    }
    walk (obj) {
        const keys = Object.keys(obj)
        for(let i = 0; i < keys.length; i++) {
            definedReactive(obj, keys[i], obj[keys[i]])
        }
    }
}
function definedReactive(data, key, value) {
    if(typeof val === 'object') {
        new Observer(value)
    }
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumberable: true,
        configurable: true,
        get: function () {
            dep.depend()
            return value
        },
        set: function (newVal) {
            if(value === newVal) {    //這邊最好是value === newVal || (value !== value && newVal !== newVal)
                return 
            }
            value = newVal   //這邊新的newVal若是是引用類型也應該進行進行new Observer()
            dep.notify()
        }
    })
}

很容易看懂

  1. 將vue中的data對象進行遍歷設置其屬性描述對象
  2. get的設置就是爲了在數據被訪問時,將依賴dep.depend()進去,至於作了什麼看詳細看Dep類
  3. set的設置則是爲了判斷新值和舊值是否同樣(注意NaN),若不同,則執行dep.notify(),通知相應依賴進行更新變化

Dep類

class Dep {
    constructor () {
        this.subs = []    //存放依賴
    }
    addSub () {
        this.subs.push(sub)
    },
    remove () {
        remove(this.subs, sub) 
    },
    depend () {
        if(window.target) {
            this.addSub(window.target)   //window.target 是this,watcher的上下文
        }
    },
    notify () {
        const subs = this.subs.slice()
        for(let i = 0, l = subs.length; i < l; i ++) {
            subs[i].update()       //update這個方法來自watcher實例對象的方法
        }
    }
}
function remove(arr, item) {
    if(arr.length) {
        const index = arr.indexOf(item)
        if(index > -1) {
            return arr.splice(index, 1)
        }
        
    }
}

分析一下

  1. 主要就是對dep實例對象的增刪改查的操做
  2. window.target 這個依賴怎麼來,就看watcher實例對象了

Watcher類

第一版:數組

class Watcher {
    constructor (vm, expOrFn, cb) {
        this.vm = vm
        this.getter = parsePath(expOrFn)
        this.cb = cb
        this.value = this.get()
    }
    get() {
        window.target = this
        let value = this.getter.call(this.vm, this.vm)
        window.target = undefined
        return value
    }
    update() {
        const oldValue = this.value
        this.value = this.get()
        this.cb.call(this.vm, this.value, oldValue)
    }
}

分析

  1. 怎麼觸發?能夠利用
vm.$watch('data.a', function (newValue, oldValue) {
    //執行相關操做
})
  1. parsePath(expOrFn)作了什麼?從下面代碼中能夠看出做用就是返回一個函數,這個函數用來讀取value
const bailRE = /[^\w.$]/  //
function parsePath(path) {
    if(bailRE.test(path) {
        return         //當path路徑中有一個字符不知足正則要求就直接return
    }
    return function () {
        const arr = path.split('.')
        let data = this
        for(let i = 0, l = arr.length; i < l; i ++) {
            let data = data.arr[i]
        }
        return data
    }
}
  1. new Watcher時會執行this.value,從而執行this.get(),因此這時的window.target是當前watcher實例對象this;接着執行this.getter.call(this.vm, this.vm),觸發屬性描述對象的get方法,進行dep.depend(),最後將其window.target = undefined
  2. update的方法是在數據改變後觸發,但這邊有個問題就是會重複添加依賴

上面版本中比較明顯的問題

  1. 依賴被重複添加
  2. 只能對已有key進行監聽
  3. 刪除key-value不會被監聽
  4. 對數組對象,並無添加監聽
  5. 對於數據變化時,並無對新數據判斷是否須要進行Observer

Array的偵測

怎麼實如今數組發生變化時來觸發dep.notify(),以及如何收集數組的依賴

  1. 經過push, pop, shift, unshift, splice, sort, reverse這幾個方法的封裝來觸發dep.notify()
  2. 怎麼的封裝?分兩種;第一種對於支持_proto_屬性的,直接改寫原型鏈的這些方法;第二種對於不支持的,直接在實例對象上添加改變後的7個方法
const arrayProto = Array.prototype
const arrayMethods = Object.create(arrayProto) //新建對象,繼承Array的原型鏈
class Observer {
    constructor (value) {
        this.value = value
        this.dep = new Dep()      //在Observer中添加dep屬性爲了記錄數組的依賴
        def(value, "_ob_", this)  //在當前value上新增`_ob_`屬性,其值爲this,當前observer實例對象 
        if(Array.isArray(value) {
            const augment = hasProto ? protoAugment : copyAugment
            augment(value, arrayMethods, arrayKeys)
            this.observerArray(value)  //將數組內元素也進行Observer
        }else {
            this.walk(value)
        }
    }
    //新增
    observerArray (items) {
        for(let i = 0, l = items.length; i < l; i ++) {
            observe(items[i])
        }
    }
}
//做用就是爲obj,添加key值爲val  
function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}
function observe(value, asRootData) {
    if(!isObject(value)) {
        return 
    }
    let ob
    //判斷value是否已是Observer實例對象,避免重複執行Observer
    if(hasOwn(value, "_ob_") && value._ob_ instanceof Observer) {
        ob = value._ob_
    } else {
        ob = new Observer(value)
    }
    return ob
}
function definedReactive(data, key, value) {
    let childOb = observe(value)   //修改
    let dep = new Dep()
    Object.defineProperty(data, key, {
        enumberable: true,
        configurable: true,
        get: function () {
            dep.depend()
            if(childOb) {            //新增
                childOb.dep.depend()
            }
            return value
        },
        set: function (newVal) {
            if(value === newVal) {    //這邊最好是value === newVal || (value !== value && newVal !== newVal)
                return 
            }
            value = newVal   //這邊新的newVal若是是引用類型也應該進行進行new Observer()
            dep.notify()
        }
    })
}
//觸發數組攔截
;[
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(function (method) {
    const original = arrayProto[method]
    def(arrayMethods, method, function mutator() {
        const result = original.apply(this, args)
        const ob =  this._ob_   //this就是數據value
        let inserted
        //對於新增變化的元素頁進行observerArray()
        switch (method) {   //由於這幾個是有參數的
            case 'push':
            case 'unshift':       //由於push和unshift都是同樣的取args,因此push不須要加break了
                inserted = args
                break
            case 'splice':    //新增變化元素是從索引2開始的
                inserted = args.slice(2)
                break
        }
        ob.dep.notify()  //通知依賴執行update
        return result
    })
}

分析,已data = { a: [1, 2, 3] }爲例

  1. 首先對data對象進行Observer,將執行this.walk(data)
  2. 接着執行let childOb = observe(val),發現value是一個數組對象,進行Observer,主要進行是augment(value, arrayMethods, arrayKeys),將7個方法進行攔截,接着遍歷內部元素是否有引用數據類型,有繼續Observer,最後返回Observer實例對象ob
  3. 重點是get方法,當數據data被訪問時,首先執行dep.depend()這裏將依賴添加到datadep中;接着由於childObtrue因此執行childOb.dep.depend(),這裏是將依賴加入到observer實例對象的dep中,爲何,這個dep是給數組發生變化時執行this._ob_.dep.notify(),這個this就是value對象,由於def(value, "_ob_", this) ,因此能夠執行dep.notify()

這種數組變化偵測存在的問題

  1. 對於進行this.list.length = 0進行清空時,不會觸發它的依賴更新,也就不會觸發視圖的渲染更新
  2. 對於this.list[0] = 2,這種經過索引來改變元素值時頁同樣不會觸發更新
  3. 因此咱們儘可能避免經過這種方式來改變數據

還有vm.$watch,vm.$set,vm.$delete下篇中進行整理

掘金地址app

相關文章
相關標籤/搜索