2

由来

最近在看“深入浅出vuejs”,第一篇变化侦测,想把自己的理解总结一下。

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下篇中进行整理

掘金地址


Infinity
293 声望9 粉丝

学前端中,只想激情优雅的写代码