Vue3数据绑定的实现

Vue3的数据绑定

不同于Vue2中的defineProperty方法,Vue3中采用proxy实现对数据的绑定,这种方式有以下好处:

  • 无需重写getter和setter对数据进行劫持
  • 无需实现 s e t 和 set和 setdelete方法实现对新增和删除属性的监控
  • 不许对数组进行单独处理

Vue3通过reactive模块来实现数据绑定,通过effect模块实现数据的更新

 const {effect,reactive } = Vue
        //对数据进行绑定
        const obj = {
            name:'sx',
            age:13,
            address:{
                num:30
            },
            flag:true
        }
        //这里只能传入对象,因为proxy只支持对象格式
        const state = reactive(obj)
        //数据响应
        //effect函数会默认执行一次,后续数据发生变化会重新执行effect函数
        effect(()=>{
            app.innerHTML = state.name+'今年'+state.age+'岁了门牌号是'+state.address.num
        })
        setTimeout(()=>{
            state.age++
        },1000)
reactive模块的实现

根据其借助proxy的原理,可以实现reactive模块中的数据劫持

  • 新建reactive.ts文件,用于实现主要逻辑
import {isObject} from "@vue/shared"

export function reactive(target){
    //传入的值需要是一个对象,如果不是,则不需要进行绑定
    if(!isObject(target)){
        return target
    }
}
  • 原index.ts负责导出reactive模块
export {reactive} from "./reactive"
  • 在reactive中实现初步的proxy代理
//实现最初的proxy
    const proxy = new Proxy(target,{
        get(target,key,receiver){
            console.log('这个属性被取到了')
            return target[key]
        },
        set(target,key,value,receiver){
            console.log('这个属性改变了')
            target[key] = value;
            return true
        }
    })
    return proxy

对数据进行绑定的意义,在于当数据变化时可以随时更新页面,这里通过get方法,当数据被访问时,就可以和effect绑定,当该属性触发set函数时,同样触发effect更新即可实现数据的劫持
但是这种设置却有很大的问题,那就是对象的this指向问题

  • 对象一
let person = {
    name:'sx',
    age(){
        return 18
    }
}
const proxy = new Proxy(person,{
    get(target,key,receiver){
        console.log('这个属性被取到了')
        return target[key]
    },
    set(target,key,value,receiver){
        console.log('这个属性改变了')
        target[key] = value;
        return true
    }
})

proxy.name
proxy.age

可以看到,这种对象用proxy可以顺利绑定每一个属性,但是下面这个却不能

  • 对象二
let person = {
    name:'sx',
    get age(){
        return this.name
    }
}
const proxy = new Proxy(person,{
    get(target,key,receiver){
        console.log('这个属性被取到了')
        console.log(key)
        return target[key]
    },
    set(target,key,value,receiver){
        console.log('这个属性改变了')
        target[key] = value;
        return true
    }
})


proxy.age

这里访问proxy.age,按照设想和代码逻辑,当触发get之后,age属性会和effect进行绑定,当age属性值变化后,set中会让effect进行更新

但是这里的age依赖的却是name,也就是说,当name发生改变时,实际上age的返回值也会发生改变,但是proxy却监听不到,也就无法触发effect对age进行更新

这里的原因就是,age中的this指的是person对象,当通过proxy.age访问时,只会触发age属性的get,而不会触发name属性的get,自然当name变化时,effect不会触发更新

要解决这个办法,只需要把this的指向改为proxy对象即可,而Reflect对象即可完成这件事

let person = {
    name:'sx',
    get age(){
        return this.name
    }
}
const proxy = new Proxy(person,{
    get(target,key,receiver){
        console.log('这个属性被取到了')
        console.log(key)
        return Reflect.get(target,key,receiver)
    },
    set(target,key,value,receiver){
        console.log('这个属性改变了')
        target[key] = value;
        return Reflect.set(target,key,value,receiver)
    }
})


proxy.age

此时访问proxy.age,会同时触发age的get和name的get
事实上,每次进行Proxy绑定对象属性也会对性能有所消耗,因此要尽可能减少proxy的使用,reactive接收一个对象,如果用户多次输入同一个对象,则没有必要每次对对象进行Proxy,只需要从缓存里拿就好

  • 情况一:用户输入同一数据对象
    • 新建weakMap对象,判断是否存在该数据对象即可
     const existing = reactiveMap.get(target)
      if(existing){
          return existing
      }
    
    • 在创建好proxy之后,存入weakMap中
      reactiveMap.set(target,proxy)
    
  • 情况二:用户第二次传入数据对象对应的proxy
 const state = reactive(obj)
 const state2 = reactive(state)

如果第二次传入的是proxy实例,则只需要触发其get方法验证,如果get能触发,则说明是proxy,直接返回即可

  • 新建枚举对象
  export const enum ReactiveFlags {
      IS_REACTIVE = '__v_isReactive'
  }
  • 触发get
if(target[ReactiveFlags.IS_REACTIVE]){
      return target
  }
  • get中进行判断
 if(key === ReactiveFlags.IS_REACTIVE){
              return true
          }

自此实现了Vue3中的数据绑定

Logo

前往低代码交流专区

更多推荐