• vue3的双向绑定原理分析


    谈到vue3的双向绑定原理,就得先知道,为什么vue2的双向绑定方式会被废弃?

    vue2的双向绑定

    Object.defineProperty

    Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象

    相关语法详见这篇文章Object.defineProperty

    实现监听器

    调用defineReactive,数据发生变化触发update方法,实现数据响应式

    // 遍历对象
    function observe(obj) {
        if (!obj || typeof obj !== 'object') {
            return
        }
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key])
        })
    }
    
    // 劫持对象
    function defineReactive(obj, key, val) {
        observe(val) // 存在嵌套对象的情况下,需要进行递归
        Object.defineProperty(obj, key, {
            get() {
                console.log(`get ${key}:${val}`);
                return val
            },
            set(newVal) {
                if (newVal !== val) {
                    val = newVal
                    observe(newVal) // 新值是对象的情况
                }
            }
        })
    }
    
    const obj = {
      name: 'initial name',
      age: 30
    }
    observe(obj)
    setTimeout(()=>{
      obj.age = 33
    },5000)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    实现监听器更详细的讲解可以阅读这篇文章从0开始实现简易版vue2

    局限性

    上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题

    对象

    现在对一个对象进行添加与删除属性操作,无法劫持到

    const obj = {
      name: 'initial name',
      age: 30
    }
    observe(obj)
    obj.school = 'HPU'
    delete obj.age
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    Object.defineProperty只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。

    Vue里,是通过递归以及遍历data对象来实现对数据的监控的,如果属性值也是对象那么需要深度监听,容易造成性能问题。

    显然如果能劫持一个完整的对象,不管是对操作性还是性能都会有一个很大的提升。

    数组

    在Vue中无法监控到数组下标的变化,导致直接通过数组的下标给数组设置值,无法实现数据响应式。

    const arrData = [1,2,3,4,5];
    observe(arrData)
    
    arrData.push()
    arrData.pop()
    
    • 1
    • 2
    • 3
    • 4
    • 5

    但是对已经劫持过的数组,原生js是能够实现对已有下标更新数据,实现数据响应式。这里vue是无法做到的,它对此进行了限制。

    arrData[0] = 'test'
    
    arrData[0] // get 0:test
    
    • 1
    • 2
    • 3

    所以在Vue2中,增加了$set$delete API,并且对数组api方法(push pop shift unshift splice sort reverse)进行了重写。。

    // 数组重写
    const originalProto = Array.prototype
    const arrayProto = Object.create(originalProto)
    ['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
      arrayProto[method] = function  {
        originalProto[method].apply(this.arguments)
        dep.notice()
      }
    });
    
    // set、delete
    
    // 对象
    Vue.set(obj, 'sex', 'man')
    Vue.delete(obj, 'sex')
    
    // 数组
    Vue.set(arrData, 0, 'test')
    Vue.delete(arrData, 0)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    经过vue内部处理后可以使用上述数组方法来实现数组的动态更新

    vue3的双向绑定

    proxy

    相关语法详见这篇文章Proxy

    实现监听器

    Proxy的监听是针对一个对象的,那么对这个对象的所有操作会进入监听操作,这就完全可以代理所有属性了。

    function reactive(obj) {
        if (!obj || typeof obj !== 'object') {
            return obj
        }
        // Proxy相当于在对象外层加拦截
        const observed = new Proxy(obj, {
            get(target, key, receiver) {
                const res = Reflect.get(target, key, receiver)
                console.log(`获取${key}:${res}`)
                return res
            },
            set(target, key, value, receiver) {
                const res = Reflect.set(target, key, value, receiver)
                console.log(`设置${key}:${value}`)
                return res
            },
            deleteProperty(target, key) {
                const res = Reflect.deleteProperty(target, key)
                console.log(`删除${key}:${res}`)
                return res
            }
        })
        return observed
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24

    测试一下对对象的操作,发现都能劫持

    const state = reactive({
        name: 'caoyuan'
    })
    // 1.获取
    state.name // 获取name:caoyuan
    // 2.设置已存在属性
    state.name = 'name changed' // 设置name:name changed
    // 3.设置不存在属性
    state.sex = 'man' // 设置sex:man
    // 4.删除属性
    delete state.sex // 删除sex:true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    也可以直接监听数组的变化(pushpopsplice等)

    const arr = [1,2,3]
    const proxtArr = reactive(arr)
    proxtArr.push(4)
    
    // 输出
    // 获取push:function push() { [native code] }
    // 获取length:3
    // 设置3:4
    // 设置length:4
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    再测试嵌套对象情况,这时候发现没有监听到更深层级的数据变化了

    const state = reactive({
        childObj: { 
            a: 1 
        }
    })
    
    // 读取嵌套对象属性
    state.childObj.a // 输出:获取childObj:[object Object] 没有具体到子对象的键名
    
    // 设置嵌套对象属性
    state.childObj.a = 10 // 无输出内容
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    如果要解决,需要进行递归处理

    function reactive(obj) {
        if (!obj || typeof obj !== 'object') {
            return obj
        }
        // Proxy相当于在对象外层加拦截
        const observed = new Proxy(obj, {
            get(target, key, receiver) {
                const res = Reflect.get(target, key, receiver)
                console.log(`获取${key}:${res}`)
                return typeof res === 'object' && res !== null ? reactive(res) : res
            },
            set(target, key, value, receiver) {
                const res = Reflect.set(target, key, value, receiver)
                console.log(`设置${key}:${value}`)
                return typeof res === 'object' && res !== null ? reactive(res) : res
            },
        })
        return observed
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    const state = reactive({
        childObj: { 
            a: 1 
        }
    })
    
    // 读取嵌套对象属性
    state.childObj.a 
    // 输出如下:
    // 获取childObj:[object Object]
    // 获取a:1
    
    // 设置嵌套对象属性
    state.childObj.a = 10 
    // 输出如下:
    // 获取childObj:[object Object]
    // 设置a:10
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    Proxy有多达13种拦截方法,不限于applyownKeysdeletePropertyhas等等,这是Object.defineProperty不具备的

    Proxy 不兼容IE,也没有 polyfill, 而defineProperty 能支持到IE9

  • 相关阅读:
    WhatsApp 群发了解这些能事半功倍!
    FCOS相关
    【深度学习】【三维重建】windows10环境配置tiny-cuda-nn详细教程
    小红书推广怎么做?小红书推广需要注意什么?
    Redis 7.x 入门和开发实战
    【AI作画】使用stable-diffusion-webui搭建AI作画平台
    文字识别ORC与公式识别
    2023最新SSM计算机毕业设计选题大全(附源码+LW)之java智慧社区家政服务系统80q7o
    数据结构总结
    数据结构入门-14-排序
  • 原文地址:https://blog.csdn.net/weixin_44116302/article/details/133050814