读vue的变化侦测

栏目: JavaScript · 发布时间: 5年前

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

由来

最近在看“深入浅出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 的方法是在数据改变后触发,但这边有个问题就是 会重复添加依赖

上面版本中比较明显的问题

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

掘金地址


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

深入浅出HTML与CSS、XHTML

深入浅出HTML与CSS、XHTML

[美] 弗里曼 Freeman.E. / 东南大学出版社 / 2006-5 / 98.00元

《深入浅出HTML与CSS XHTML》(影印版)能让你避免认为Web-safe颜色还是紧要问题的尴尬,以及不明智地把标记放入你的页面。最大的好处是,你将毫无睡意地学习HTML、XHTML 和CSS。如果你曾经读过深入浅出(Head First)系列图书中的任一本,就会知道书中展现的是什么:一个按人脑思维方式设计的丰富的可视化学习模式。《深入浅出HTML与CSS XHTML》(影印版)的编写采用了......一起来看看 《深入浅出HTML与CSS、XHTML》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

RGB转16进制工具
RGB转16进制工具

RGB HEX 互转工具

HSV CMYK 转换工具
HSV CMYK 转换工具

HSV CMYK互换工具