Vue.js设计与实现读书笔记六 第六章 原始值的响应式方案

本文深入探讨Vue3中原始值的响应式方案,包括ref、reactive等核心概念的实现原理,以及toRefs和toRef的功能与应用场景,最后介绍了自动脱ref的实现方式。

第六章主要讲解的是原始值的响应式方案,reactive,ref,toRef,toRefs之间的关系,以及他们的作用,简单来说,ref是由ractive构建的,toRefs是由toRef构建的

6.1 引入ref的概念

1.原始值是指 Nubmer,Boolean,BigInt,String,Symbol,undefined,null,即非对象类型
2.proxy是无法代理原始值的,只能代理对象(函数也是对象)
3.要区分原始值和非原始值的响应
如何解决上面的问题,其实就是将原始值包裹一层非原始值即可,然后用reactive来创建非原生值响应,那么如何确定非原生值和原始值响应呢?我们给创建的非原始值,设置一个不可遍历的属性即可,打个标记’__v_isRef,如下代码

function ref(val) {
  const wrapper = {
    value: val
  }

  Object.defineProperty(wrapper, '__v_isRef', {
    enumerable:false,
    value: true
  })

  return reactive(wrapper)
}

function isRef(val) {
  return val.__v_isRef === true
}

const refVal = ref(1)

console.log(isRef(refVal))

effect(() => {
  console.log(refVal.value)
})

refVal.value = 2

那么ref可以使用非原生值吗?

由源码看是可以的,ref默认使用 深响应,可读属性,如果非原生值,相当于
reactive({ value: obj, __v_isRef:true}) 这种形式

6.2 响应丢失问题

1.响应丢失主要是说,用…操作符,扩展响应数据的结果会丢失响应,

const obj= reactive({foo:1,bar:2})
const newobj={ ...obj} // 相当于 const newobj={ foo:1,bar:2} 变成普通变量

解决方法就是将newobj的访问的值用 obj的替换,代码如下:

const obj = reactive({ foo: 1, bar: 2 })

// const newObj = {
//   ...obj
// }

// effect(() => {
//   console.log(newObj.foo)
// })

// obj.foo = 100 

// 将响应是对象全部key,全部提前到对象中
function toRefs(obj) {
  const ret = {}
  for (const key in obj) {
    ret[key] = toRef(obj, key)
  }
  return ret
}
// 将响应式对象的某个key,提取出来变成响应式
function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key]
    },
    // 扩展出来的响应值也可以修改
    set value(val) {
      obj[key] = val
    }
  }
  // 把obj的key都转成ref,不管他是不是原始值,所以打上标记
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true
  })

  return wrapper
}

const refFoo = toRef(obj, 'foo')
const refBar = toRef(obj, 'bar')

console.log(refFoo.value)
console.log(refBar.value)

const newObj = { ...toRefs(obj) }
console.log(newObj.foo.value)
console.log(newObj.bar.value)

refFoo.value = 100
console.log(refFoo.value)

toRefs() 参数是一个ractive响应对象,返回的式一个{ 响应式对象key的响应对象},也就是说toRefs本身式普通对象,但是他的key全部都是响应式对象
toRef(obj,key) 表示分离出obj的key,为响应式对象

注意这返回的结果,的响应式对象都需要用xx.value访问,因为不知道 obj的是不是原生值,都转成ref的形式,

那么toRefs和toRef的第一个参数可以是ref吗?
由源码可以知道,toRefs会遍历obj,而如果是ref,结构是reactive({ value: obj, __v_isRef:true}) 导致ret的结构是{ value: toRef(obj,value )}
如果
const str=ref("abcd") const strs=toRefs(str)

那么要访问到abcd 则是:strs.value.value

但是vue3源码如下

  function toRefs(object) {
      if (!isProxy(object)) {
          console.warn(`toRefs() expects a reactive object but received a plain one.`);
      }
      const ret = isArray(object) ? new Array(object.length) : {};
      for (const key in object) {
          ret[key] = toRef(object, key);
      }
      return ret;
  }
  function isProxy(value) {
      return isReactive(value) || isReadonly(value);
  }

toRaw的实现原理
这里提一下toRaw这个方法,他是返回响应式对象的原始值,其实之前的代码中就有,当访问响应式对象的raw就返回target ,而vue3源码如下,

function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
    if (!shared.isObject(target)) {
        {
            console.warn(`value cannot be made reactive: ${String(target)}`);
        }
        return target;
    }
    // target is already a Proxy, return it.
    // exception: calling readonly() on a reactive object
    if (target["__v_raw" /* RAW */] &&
        !(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
        return target;
    }
    // target already has corresponding Proxy
    const existingProxy = proxyMap.get(target);
    if (existingProxy) {
        return existingProxy;
    }
    // only specific value types can be observed.
    const targetType = getTargetType(target);
    if (targetType === 0 /* INVALID */) {
        return target;
    }
    const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
    proxyMap.set(target, proxy);
    return proxy;
}


function toRaw(observed) {
    const raw = observed && observed["__v_raw" /* RAW */];
    return raw ? toRaw(raw) : observed;
}

当访问__v_raw的时候就返回原生对象target

6.3 自动脱ref

这个是要解决,上面toRefs把reactive的属性都变成ref,ref都需要用.value访问,我们要改成ref对象能直接访问属性,让newObj.foo.value 不用.value就能直接访问到最终的可代理对象,实现如下:

const obj = reactive({ foo: 1, bar: 2 })

// const newObj = {
//   ...obj
// }

// effect(() => {
//   console.log(newObj.foo)
// })

// obj.foo = 100 


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

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 proxyRefs(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      return value.__v_isRef ? value.value : value
    },
    set(target, key, newValue, receiver) {
      const value = target[key]
      if (value.__v_isRef) {
        value.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue, receiver)
    }
  })
}

const newObj = proxyRefs({ ...toRefs(obj) })



console.log(newObj.foo)
console.log(newObj.bar)

newObj.foo = 100
console.log(newObj.foo)

proxyRefs的参数就是{…toRefs(obj) } ,注意不是ref对象,是{ ref }
如果 参数对象的属性是ref,就直接返回他的value,如果不是就返回属性本身
设置的时候,如果属性是ref,将值赋值给他的value,否则直接返回

vue的setup返回的值都会自动脱钩,执行proxyRefs 所以在模板中不用使用.vule
returen { newObj.foo }

ref中可以嵌套ref吗?proxyRefs还可以脱ref吗?
ref中嵌套ref是没有效果的,在vue3中会判断ref传入的值是不是ref

  function ref(value) {
      return createRef(value, false);
  }
  
  function createRef(rawValue, shallow) {
      if (isRef(rawValue)) {
          return rawValue;
      }
      return new RefImpl(rawValue, shallow);
  }

问题总结:

第六章 原始值的响应式方案
(二十二)如何实现原始值的响应式?响应丢失是什么情况,如何解决?为什么要自动脱ref?
(1)可以利用非原始值响应式方案,用一个对象包裹原始值,然后给对象上定义一个_v_isRef表示不可枚举属性

function ref(val) {
  const wrapper = {
    value: val
  }
  Object.defineProperty(wrapper, '__v_isRef', {
     enumerable:false,
    value: true
  })
  return reactive(wrapper)
}
function isRef(val) {
  return val.__v_isRef === true
}

(2)响应丢失是指如果使用展开运算符…在 reactive对象,结果会失去响应式,会返回普通对象值,可以封装一toRef他可以为…的结果创建一个新响应式的值,toRefs可以批量将响应对象扩展的值全部变成ref形式,这样使用…的结果就不会丢失响应式了
(3)因为在模板中访问数据,如何还是xx.value的话,给用户增加心智,所以所有的setup中的ref都会自动脱ref

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值