vuejs设计与实现-原始值的响应式方案

87 阅读2分钟

原始值指booleannumberbigIntstringsymbolundefinednull等类型的值. 它们按值传递而不是按引用传递.

引入ref的概念

非原始值可以通过Proxy进行代理, 原始值可以通过创建一个对象进行代理. 封装ref函数实现此功能.

function ref(val){
    const wrapper = {
        value: val
    } 
    // 设置__v_isRef  用以区分是否为ref
    Object.defineProperty(wrapper, '__v_isRef', {
        value: true
    })
    return reactive(wrapper)
}

响应丢失问题

对响应式数据进行解构时, 会发生响应丢失的问题. 可以将响应式数据的每项转换为ref解决这个问题. 这就是toReftoefs方法.

const obj = reactive({ foo: 1 })

const newObj = { ...obj }
// newObj.foo -> 1 
// 解构后生成的 newObj 不再是一个响应式对象

const newObj = { ...toRefs(obj) }
// newObj.foo.value -> 1 
// 解构后生成的 newObj  它的每项都是一个ref数据


function toRef(obj, key){
    const wrapper = {
        get value(){
            return obj[key]
        },
        set value(val){
            obj[key] = val
        } 
    }
    
    Object.defineProperty(wrapper, '__v_isRef', {
        value: true
    })
    return wrapper
}

function toRefs(obj){
    const ret = {}
    for(const key of obj){
        ret[key] = toRef(obj, key)
    }
    return ret
}

自动脱ref

通过toRefs虽然解决了响应丢失问题, 但同时再次使用时需要.value增加心智负担. 我们需要在读取一个属性时, 如果其是ref, 则直接返回对应的.value, 实现自动脱ref.

const obj = reactive({ foo: 1 })

const newObj = proxyRefs({ ...toRefs(obj) })
// newObj.foo -> 1


function proxyRefs(target){
    return new Proxy(target, {
        // 拦截get操作, 当访问的值为ref 直接返回 .value
        get(target, key, receiver){
            const value = Reflect.get(target, key, receiver)
            return val.__v_isRef ? val.value : val
        },
        // 修改时, 如果是ref则通过.value修改
        set(target, key, newValue, receiver){
            const val = target[key]
            if(val.__v_isRef){
                val.value = newValue
                return true
            }
            return Reflect.set(target, key, newValue, receiver)
        }
    })
}

实际上setup函数返回的数据也会传递给proxyRefs进行处理. 因此在模版中不再需要.value.

总结

  • ref本质是一个原始值的包裹对象, 定义__v_isRef属性用以区分普通对象.
  • toReftoRefs解决了响应丢失问题.
  • 自动对暴露到模版的响应式数据进行脱ref处理.
  • reactive方法也有脱ref能力
const count = ref(0)
const obj = reactive({ count })
// obj.count -> 0