美文网首页
Vue响应式原理

Vue响应式原理

作者: 陈嘻嘻啊 | 来源:发表于2020-05-27 13:47 被阅读0次
    Reactive-in-Depth.png

    Vue数据劫持的实现,做一个自己的理解&简单总结。虽然Vue3.0即将到来,我想Vue2.x也不至于马上过时。

    今天就从Vue2.x 与 Vue.3.0 数据劫持如何实现数据双向绑定。

    数据劫持: 指的是在访问或者修改对象的某个属性时,通过一段代码拦截这个行为,进行额外的操作或者修改返回结果。

    Vue2.x 选择的 Object.defineProperty

    Object.defineProperty 对大家都来说应该不陌生了。算是面试的一道必考题?(细品:那掌握好了是不是就是一道送分题呢?)可以点击这里回顾一下 Object.defineProperty文档

    我们来认清Object.defineProperty的几个局限性

    • 兼容性是IE8+,这也就是为什么Vue不支持IE8及以下版本的原因
    • 不能监听数组的变化,Vue通过重写数组原型的方法来实现数据劫持。
    • 对于深层次嵌套对象需要做递归遍历。
    • 必须遍历对象的每个属性。如果要扩展该对象,就必须手动去为新的属性设置setter、getter方法。 这也就是为什么Vue开发中的不在 data 中声明的属性无法自动拥有双向绑定效果的原因。需要我们手动去调用Vue.set()

    我们做个类似Vue简易的数据劫持

    1. 视图更新触发的函数
    // 当我们监听的数据发生变化后调用改函数
    function update() {
        console.log('数据变化啦,更新视图')
    }
    
    
    1. 通过 Object.defineProperty 处理 data 中的每个属性
    // 通过 Object.defineProperty 处理 target 中的每个属性 key
    function defineReactive(target, key, value) {
        Object.defineProperty(target, key, {
            get() {
                return value;
            },
            set(val) {
                // 如果改变的数据和原来一样将不做任何处理
                if (val !== value) {
                     // 数据更新了,调用update
                     update();
                     value = val;
                } 
            }
        })
    }
    
    1. 监听data的函数
    function observer(target) {
        // 如果不是对象,直接返回;如果是null也直接返回
        if (typeof target !== 'object' || !target) return target;
        
        // 遍历对象obj的所有key,完成属性配置
        Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
    }
    
    1. 测试步骤1、2、3
    // 需要监听的data对象
    const data = {
        level: 1,
        info: {
            name: 'cc'
        }
    }
    
    // 调用监听函数监听 data
    observer(data)
    
    // 修改data的值 视图更新
    data.level = 2
    
    // 看到视图确实更新了
    
    // 我们不妨尝试了一下data深层次对象的修改
    data.info.name = 'yy'
    
    // 控制台什么都是没有
    
    
    1. 想必你也发现了,监听data只到了对象的第一层。data深层次的数据,并没有被监听。所以我们需要对data做一个逐层遍历(递归),直到把每个对象的每个属性都调用 Object.defineProperty() 为止。
    // 改改步骤二的代码
    function defineReactive(target, key, value) {
        // 在这里新增代码
        // 当value为object我们再做一次数据监听,直到value不是object为止
        if (typeof value === 'object') {
            observer(value)
        }
        
        // 以下代码和步骤2没有区别
        Object.defineProperty(target, key, {
            get() {
                return value;
            },
            set(val) {
                // 如果改变的数据和原来一样将不做任何处理
                if (val !== value) {
                     // 数据更新了,调用update
                     update();
                     value = val;
                } 
            }
        })
    }
    
    1. 再对步骤5的修改做一次测试
    const data = {
        level: 1,
        info: {
            name: 'cc'
        },
        a: {
            a: {
                a: {
                    a: 1
                }
            }
        }
    }
    
    // 我们尝试改变data.info.name的值
    data.info.name = 'xy'  // 视图更新了!
    
    // 我们尝试跟深层次的修改
    data.a.a.a.a = 2  // ok 视图也更新了
    
    // 那么我再试试其他方式
    // 先修改data.info的值
    data.info = { name: 'cc' } // 没毛病,视图更新了,但此时data.info的指向已经发生了变化
    // 然后再修改data.info.name
    data.info.name = 'xy' // emmmmmm... 又是什么都没有
    
    1. 我们针对步骤5再做一次修改
    // 修改步骤5的代码
    function defineReactive(target, key, value) {
        if (typeof value === 'object') {
            observer(value)
        }
        Object.defineProperty(target, key, {
            get() {
                return value;
            },
            set(newVal) {
                // 如果改变的数据和原来一样将不做任何处理
                if (newVal !== value) {
                    // 在这里新增代码
                    // 如果设置newVal是object,对newVal做监听
                    if (typeof newVal === 'object') {
                        observer(newVal)
                    }
                     // 数据更新了,调用update
                     update();
                     value = newVal;
                } 
            }
        })
    }
    
    1. 再对步骤7的修改做一次测试
    const data = {
        level: 1,
        info: {
            name: 'cc'
        }
    }
    
    // 先修改data.info的值
    data.info = { name: 'cc' } // 没毛病,视图更新了
    // 然后再修改data.info.name
    data.info.name = 'xy' // 也没毛病,视图更新了
    
    1. 我们都知道typeof 数据返回的也是object
    const data = {
        arr: []
    }
    
    // 尝试对数组做更改
    arr.push(1); // 然鹅,并没有任何输出
    
    1. 前面有说明Object.defineProperty 对数组是起不到任何作用的。那Vue如何实现的呢? Vue是通过修改数组的原型方法来实现数据劫持(做一些视图更新、渲染的操作)。
    const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
    
    // 遍历methods数组
    methods.forEach(method => {
        // 原生Array的原型方法
        const originalArray = Array.prototype[method]
        
        // 重写Array原型上对应的方式
        Array.prototype[method] = function() {
            // 做视图更新或者渲染操作
            update();
            
            // 视图更新了,调用对应的原生方法
            // arguments 将该有的参数也传进来
            originalArray.call(this, ...arguments);
        }
    })
    
    1. 又到了验证一下步骤10的时候啦!
    const data = {
        arr: []
    }
    
    data.arr.push(1) // 视图更新了
    
    1. 看了上面的代码,可能就有疑问了。我们明显直接修改的是 Array.prototype的方法。这样会导致一个问题。没有被监听的数组,也会触发update()。如下:
    var normalArray = [];
    
    normalArray.push(1); // wtf 竟然也触发了视图更新
    

    结果明显不是我们想要的。我们希望的是:Array原有的方法保持不变,但是又要引用到原来的方法的实现。

    我们可以简单地处理下啦。

    ①先修改步骤10的代码

    const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
    const arrayList = [] 
    
    // 遍历methods数组
    methods.forEach(method => {
        // 原生Array的原型方法
        const originalArray = Array.prototype[method]
        
        // 重写Array原型上对应的方式
        arrayList[method] = function() {
            // 做视图更新或者渲染操作
            update();
            
            // 视图更新了,调用对应的原生方法
            // arguments 将该有的参数也传进来
            originalArray.call(this, ...arguments);
        }
    })
    

    ②再修改步骤7的代码

    function defineReactive(target, key, value) {
        if (typeof value === 'object') {
            // 通过链去找我们定义好的方法
            if (Array.isArray(value)) {
                value.__proto__ = arrayList
            }
            observer(value)
        }
        Object.defineProperty(target, key, {
            get() {
                return value;
            },
            set(val) {
                // 如果改变的数据和原来一样将不做任何处理
                if (val !== value) {
                    // 在这里新增代码,如果设置val是object,对val做监听
                    if (typeof val === 'object') {
                        // 通过链去找我们定义好的方法
                        if (Array.isArray(val)) {
                val.__proto__ = arrayList
              }
                        observer(val)
                    }
                     // 数据更新了,调用update
                     update();
                     value = val;
                } 
            }
        })
    }
    
    1. 完整代码
    const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
    const arrayList = []
    
    // 遍历methods数组
    methods.forEach(method => {
        // 原生Array的原型方法
        const originalArray = Array.prototype[method]
    
        // 重写Array原型上对应的方式
        arrayList[method] = function() {
            // 做视图更新或者渲染操作
            update();
    
            // 视图更新了,调用对应的原生方法
            // arguments 将该有的参数也传进来
            originalArray.call(this, ...arguments);
        }
    })
    
    
    // 当我们监听的数据发生变化后调用改函数
    function update() {
        console.log('数据变化啦,更新视图')
    }
    
    function observer(target) {
        // 如果不是对象,直接返回
        if (typeof target !== 'object' || !target) return target;
    
        // 遍历对象obj的所有key,完成属性配置
        Object.keys(target).forEach(key => defineReactive(target, key, target[key]))
    }
    
    
    function defineReactive(target, key, value) {
        if (typeof value === 'object') {
            if (Array.isArray(value)) {
                value.__proto__ = arrayList
            }
            observer(value)
        }
        Object.defineProperty(target, key, {
            get() {
                return value;
            },
            set(newVal) {
                // 如果改变的数据和原来一样将不做任何处理
                if (newVal !== value) {
                    // 在这里新增代码,如果设置newVal是object,对newVal做监听
                    if (typeof newVal === 'object') {
                        if (Array.isArray(newVal)) {
                newVal.__proto__ = arrayList
              }
                        observer(newVal)
                    }
                     // 数据更新了,调用update
                     update();
                     value = newVal;
                }
            }
        })
    }
    
    
    const data = {
      level: 1,
      info: {
        name: 'cc'
      },
      arr: []
    }
    
    observer(data)
    
    // 自行打开注释行测试即可
    
    // ①
    // data.level = 2
    
    // ②
    // data.info.name = 'xy'
    
    // ③
    /*
    data.info = {name: 'cc'}
    data.info.name = 'xy'
    */
    
    // ④
    // data.arr.push(1)
    
    // ⑤
    /*
    data.arr = []
    data.arr.push(1)
    */
    
    
    

    值得注意的是:数组不支持长度的修改,也不支持通过数组的索引进行更改。例如以下方式是不会触发视图更新,只有上面列举的7个方式或者直接替换一个新的数组才会触发视图更新。数组更新检测

    data.arr.length = 3
    data.arr[1] = 1
    

    Vue3.0 选择的 Proxy

    Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

    function update() {
      console.log('数据变化啦,更新视图')
    }
    
    const data = {
      level: 1,
      info: {
        name: 'cc'
      },
      arr: []
    }
    
    const handler = {
      get(target, property) {
        // 如果值为对象,在对该值进行数据劫持
        if (typeof target[property] === 'object' && target[property] !== null) {
          return new Proxy(target[property], handler)
        }
        return Reflect.get(target, property)
      },
    
      set(target, property, value) {
        if (property === 'length') {
          return true
        }
        update()
        return Reflect.set(target, property, value)
      }
    }
    
    const proxy = new Proxy(data, handler)
    
    proxy.level = 2
    proxy.info.name = 'yy'
    proxy.arr.push(1)
    proxy.arr[1] = 1
    

    Proxy最大的问题应该就是兼容性了,但是3.0都准备发布了,我们值得简单一试~

    相关文章

      网友评论

          本文标题:Vue响应式原理

          本文链接:https://www.haomeiwen.com/subject/bqpvahtx.html