11_原始值的响应式方案-ref

目录

引入 ref

在之前实现的 reactive 方法,其代理的目标必须是一个非原始值才行,例如:

javascript 复制代码
let str = 'vue'
// 无法拦截 str 的修改
str = 'vue3'

上述这个例子表达的意思就是,我们还缺少一个能够对原始值实现响应式数据的手段。

对于这个问题,基于现有的 reactive 就一定毫无办法吗,其实也不尽然,也有一些曲线救国的手段,如下:

javascript 复制代码
const wrapper = {
  value: 'vue'
}

// 使用 proxy 代理 wrapper,间接实现对原始值的拦截
const name = reactive(wrapper)
name.value // vue
// 修改值-触发响应拦截
name.value = 'vue3'

这样确实是可以进行拦截,但是它存在两个问题:

  • 用户为了创建一个响应式对象的原始值,不得不顺带创建一个包裹对象,使用不方便
  • 包裹对象由用户定义,而这意味着不规范,用户可以随意命名,例如:wrapper.value、wrapper.v、wrapper.v,甚至如果用户愿意,可以写成语义错乱的情况,比如:wrapper.size。

而为了解决以上两个问题,我们需要封装一个函数,来进行规范,如下:

javascript 复制代码
// 封装一个 ref 函数
function ref(val){
  // 在 ref 函数内创建包裹对象
  const wrapper = {
    value: val
  }
  // 将包裹对象变成响应式数据
  return reactive(wrapper)
}

这样就解决了上述的两个问题,我们来添加一段测试代码如下:

javascript 复制代码
const refVal = ref(1)

effect(() => {
  console.log('effect-ref:', refVal.value)
})

refVal.value++

结果如图:

这段代码可以按照我们预期的工作,他还存在诸多的问题,第一个面临的就是如何区分这是一个 ref 对象,即如何区分 refVal 到底是原始值的包裹对象,还是一个非原始值的响应式数据,如如下代码所示:

javascript 复制代码
const refVal1 = ref(1)
const refVal2 = reactive({value: 1})

如果放入 effect 函数中执行,这两个代码都是一样的效果。但是我们有必要区分一个数据到是是不是 ref,因为这对于后续的自动脱 ref 能力至关重要。

而想区分也并不难,在 reactive 中我们是在 get 拦截器中通过一个内置的 Symbol 为 key 来实现的,这里我们可以使用类似的方式,不过由于没有使用代理,我们可以使用 Object.defineProperty,如下:

javascript 复制代码
function ref(val) {
  const wrapper = {
    value: val
  }

  // 添加一个特殊属性,用于标识这个对象是否是一个 ref 对象
  //  - 且这个属性是不可枚举的,防止被遍历到
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true
  })

  return reactive(wrapper)
}

通过这个属性就可以实现是否是一个 ref 对象

解决响应丢失的问题

ref 除了能够用于原始值的响应式方案之外,还可以用来解决响应式丢失的问题,首先,我们来看一下什么叫响应式丢失,在编写 vue.js 组件时,我们通常把数据暴露到模板中使用,例如:

javascript 复制代码
export default {
  setup(){
    // 响应式数据
    const obj = reactive({ foo: 1, bar: 2 })
    
    // 将数据暴露到模板中
    return {
      ...obj
    }
  }
}

然后在模板中可以访问从 setup 中暴露的数据,如下:

html 复制代码
<template>
	<p> {{ foo }} / {{ bar }} </p>  
</template>

然而,这样书写就会失去响应式,当我们修改数据时,不会触发渲染,这是为什么?因为我们使用了展开运算符,而经过展开之后,等价于:

javascript 复制代码
return {
  foo: 1,
  bar: 2
}

而这,就是一个普通对象,自然不具备响应式能力,我们也可以通过我们熟悉的 effect 案例来描述这个例子,如下:

javascript 复制代码
const obj = reactive({ foo: 1, bar: 2 })

// 将响应式数据战展开到一个新的对象 newObj
const newObj = {
  ...obj
}

effect(()=>{
  // 在副作用函数内使用新的对象 newObj 读取 foo 属性值
  console.log(newObj.foo)
})

// 修改-无法触发响应
obj.foo = 100

如何解决这个问题呢?或者说:在副作用函数中,即时通过普通对象 newObj 来访问属性值,也能够建立响应联系,如下:

javascript 复制代码
const obj = reactive({ foo: 1, bar: 2 })

// newObj 具备 obj 的同名属性,并且每个属性值都是一个对象,这个对象具备一个访问器属性 value,当读取 value 属性,其实读取的就是 obj 下对应的属性值
const newObj = {
  foo: {
    get value(){
      return obj.foo
    }
	},
  bar: {
    get value(){
      return obj.bar
    }
	}
}

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

// 可以触发响应
obj.foo = 100

这样就可以实现响应的触发了,仔细观察就不难发现 foo 和 bar 的处理都是一致的,对于这种我们可以通过封装一个函数来提高效率,如下:

javascript 复制代码
function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key]
    }
  }

  return wrapper
}

有了这个函数之后,我们的 newObj 就可以大大简化了,如下:

javascript 复制代码
const newObj = {
  foo: toRef(obj, 'foo'),
  bar: toRef(obj, 'bar')
}

但是如果这样 foo 的属性非常多,处理起来也显得麻烦,我们可以在编写一个 toRefs 函数来完成,如下:

javascript 复制代码
function toRefs(obj) {
  const ret = {}
  // 遍历对象中的每一个属性
  for (const key in obj) {
    // 调用 toRef 函数完成转换
    ret[key] = toRef(obj, key)
  }
  return ret
}

现在,我们的使用会更加的简洁,如下:

javascript 复制代码
const newObj = { ...toRefs(obj) }

而由上述可得 toRef 会将一个数据转为 ref 数据,所以还需要为 toRef 函数补充一点,如下:

javascript 复制代码
function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key]
    }
  }

  // 添加一个特殊属性,用于标识这个对象是否是一个 ref 对象
  //  - 且这个属性是不可枚举的,防止被遍历到
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true
  })

  return wrapper
}

此时的 toRef 还只是只读的,因此我们还需要为其加上 setter,如下:

javascript 复制代码
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
}

自动脱 ref

toRefs 解决了响应式丢失问题,也带了新的问题,由于 toRefs 会把第一层属性值转为 ref,因此必须通过 value 属性来访问,这样就会导致用户的心智负担,如下:

html 复制代码
<!-- good -->
<div>{{ foo }} / {{ bar }}</div>

<!-- bad -->
<div>{{ foo.value }} / {{ bar.value }}</div>

因此,我们需要脱 ref 能力,所谓自动脱 ref,就是指的属性访问行为,即 ref 数据无需通过 xx.value 来访问。

而实现这个功能其实也不难,如下:

javascript 复制代码
function proxyRefs(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      // 自动脱 ref 实现:如果 value 是一个 ref 对象,则返回它的 value 属性
      return isRef(value) ? value.value : value
    }
  })
}

可以看到,我们利用 proxy 的 get 拦截器作为插入代码逻辑的地方,在这里我们通过判断一个值是否是 ref 数据,如果是在自动的在这里读取 refVal.value。

而实际上,在编写 vue.js 组件的时候,组件中的 setup 函数所返回的数据会传递给 proxyRefs 来处理:

javascript 复制代码
const myComp = {
  setup(){
		const count = ref(0)
    // 返回的这个对象会交给 proxyRefs 来处理
    return { count }
  }
}

这样在模板中,我们就无需通过 xx.value 来访问属性值。而可以读取,自然也需要添加修改,如下:

javascript 复制代码
function proxyRefs(target) {
  return new Proxy(target, {
    get(target, key, receiver) {
      const value = Reflect.get(target, key, receiver)
      return isRef(value) ? value.value : value
    },
    set(target, key, newValue, receiver) {
      const oldValue = target[key]
      // 如果是 ref,则设置其对应的 value 属性值
      if (isRef(oldValue)) {
        oldValue.value = newValue
        return true
      }
      return Reflect.set(target, key, newValue, receiver)
    }
  })
}

有了自动脱 ref 的能力之后,就可以降低用户在使用时的心智负担,无需关心那个属性是 ref 那个属性是普通数据或者是 reactive。

相关推荐
Cshaosun12 分钟前
js版本之ES5特性简述【String、Function、JSON、其他】(二)
前端·javascript·es
__WanG15 分钟前
Flutter将应用打包发布到App Store
前端·flutter·ios
leluckys18 分钟前
flutter 专题十七 Flutter Flar动画实战
前端·flutter
豆包MarsCode34 分钟前
我用豆包MarsCode IDE 做了一个 CSS 权重小组件
开发语言·前端·javascript·css·ide·html
22x艾克斯43 分钟前
Web Notifications API-让网页也能像QQ一样实现消息通知
前端
X 西安1 小时前
第十章JavaScript的应用
开发语言·javascript·ecmascript
想你的风吹到了瑞士1 小时前
变量提升&函数提升
前端·javascript·vue.js
生椰拿铁You1 小时前
12 —— Webpack中向前端注入环境变量
前端
Huazzi.2 小时前
免费好用的静态网页托管平台全面对比介绍
前端·网络·github·web
吃土少女古拉拉2 小时前
前端和后端
前端·学习笔记