04-原始值的响应式方案

33 阅读3分钟

上一章学习了非原始值的响应式方案,本章学习的是原始值响应式方案,原始值指的是Boolean、Number、BigInt、String、Symbol、undefined和null等类型的值,在JS中原始值是值传递。在这种情况下,如果函数接收一个原始值参数则,形参与实参相互独立,无法相互引用,修改形参不会影响实参,而且Proxy无法代理原始值数据,原始值变为响应式数据,则需要使用ref。

1、ref包装

Proxy无法代理原始值数据,只能使用一个非原始值包裹原始值。但进行包裹会出现两个问题:

1、用户创建一个响应式的原始值,不得不顺带创建一个对象进行包裹

2、用户定义包裹对象,这意味着不规范,可以随意命名

解决方案:创建一个函数把包裹对象创建封装在函数中,同时还需要判断一个数据是否是ref,使用Object.defineProperty在wrapper对象上定义一个不可枚举的属性,并且值为true,代码如下:

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

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

  return reactive(wrapper)
}

2、响应丢失问题

什么是响应丢失?

响应丢失:指的是在需要返回的数据为响应式时,返回的却是普通对象,不具有响应式能力,暴露出后,模板使用不会重新触发渲染。

ref也可以解决响应丢失的问题

封装toRef方法,设置一个访问器属性value,当读取value的值时最终访问的是,响应式下同名属性值。toRef 函数接收两个参数,第一个参数 obj 是一个响应式数据,第二个参数是 obj 对象的一个键。代码如下:

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
}

如果obj的key非常多,则需要通过添加for in来解决

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

3、自动脱ref

toRefs解决了响应丢失的问题,但会增加用户负担,toRefs会把响应式数据的第一层转换为ref,用户必须通过value属性访问值。

解决方案:需要有自动脱ref的能力,即读取的属性是一个ref则把对应的value属性值直接返回。使用Proxy创建一个代理对象,通过代理实现最终目标,代码如下:

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)
    }
  })
}

代理对象拦截get、set操作,读取或者设置到一个ref时,直接返回或者设置ref的value属性值。

总结

本章学习了:

1、ref的概念,本质上是通过包裹对象实现响应式,因为JS的Proxy无法对原始值代理

2、ref可以解决响应丢失的问题,通过对响应式数据做了一层包装,实现了toRef和toRefs

3、自动脱ref,对暴露到模板中的响应式数据进行脱 ref 处理,减轻用户负担