首页 > 其他分享 >11_原始值的响应式方案-ref

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

时间:2024-10-23 12:20:49浏览次数:3  
标签:11 obj value return 响应 wrapper const ref

目录

引入 ref

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

let str = 'vue'
// 无法拦截 str 的修改
str = 'vue3'

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

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

const wrapper = {
  value: 'vue'
}

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

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

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

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

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

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

const refVal = ref(1)

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

refVal.value++

结果如图:

在这里插入图片描述

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

const refVal1 = ref(1)
const refVal2 = reactive({value: 1})

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

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

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

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

  return reactive(wrapper)
}

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

解决响应丢失的问题

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

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

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

<template>
	<p> {{ foo }} / {{ bar }} </p>  
</template>

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

return {
  foo: 1,
  bar: 2
}

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

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

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

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

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

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

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 的处理都是一致的,对于这种我们可以通过封装一个函数来提高效率,如下:

function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key]
    }
  }

  return wrapper
}

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

const newObj = {
  foo: toRef(obj, 'foo'),
  bar: toRef(obj, 'bar')
}

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

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

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

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

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

function toRef(obj, key) {
  const wrapper = {
    get value() {
      return obj[key]
    }
  }

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

  return wrapper
}

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

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 属性来访问,这样就会导致用户的心智负担,如下:

<!-- good -->
<div>{{ foo }} / {{ bar }}</div>

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

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

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

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 来处理:

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

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

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。

标签:11,obj,value,return,响应,wrapper,const,ref
From: https://blog.csdn.net/qq_53109172/article/details/143143894

相关文章

  • 新升级Windows11 24H2后,前置的窗口边缘不停闪烁解决办法
    最近升级了Windows1124H2,有个小bug非常烦人,就是app的窗口前置时,边缘不停闪烁,时有时无,时快时慢,狗眼都闪瞎了,非常烦人。先是找了显卡驱动问题,发现并不是。据观察,如果有两个或者多个窗口堆叠时,则窗口围成的外轮廓不停的闪烁。于是尝试去Windows自带的“视觉效果”里找,顺利解决,以下......
  • 腾讯地图web端请求报错113.该功能未授权
    问题描述:请求地址:https://apis.map.qq.com/jsapi?qt=geoc&addr=%2C%2C%2C&key=你的key&output=jsonp&pf=jsapi&ref=jsapi&cb=qq.maps._svcb3.geocoder0报错:qq.maps._svcb3.geocoder0&&qq.maps._svcb3.geocoder0({"status":113,"me......
  • 极狐GitLab 发布安全版本16.10.10, 16.9.11, 16.8.10, 16.7.10, 16.6.10, 16.5.10, 16
    近期,极狐GitLab针对16.x版本正式推出安全版本16.10.10,16.9.11,16.8.10,16.7.10,16.6.10,16.5.10,16.4.7,16.3.9,16.2.11,16.1.8,16.0.10,用来减缓安全漏洞CVE-2024-45409带来的安全风险。极狐GitLab正式推出针对GitLabCE老旧版本免费用户的GitLab专业升级服......
  • 创新!高级!【日前、日内非滚动、日内滚动调度以及实时修正】考虑需求侧响应的智慧楼宇多
     ......
  • Oracle11g一键巡检脚本(输出HTML格式)
    脚本内容:#!/bin/bash#设置Oracle环境变量exportORACLE_HOME=/u01/app/oracle/product/11.2.0/db_1exportORACLE_SID=orcl11gexportPATH=$ORACLE_HOME/bin:$PATHfunctionseparator(){localLine=Title=Bytes=Xlength=Title="$*"Line='......
  • MySQL【知识改变命运】11
    联合查询6.⼦查询6.1语法6.2单⾏⼦查询6.3多⾏⼦查询6.4多列⼦查询6.5在from⼦句中使⽤⼦查询7.合并查询7.1创建新表并初始化数据7.2Union7.3Unionall8.插⼊查询结果8.1语法8.2⽰例6.⼦查询⼦查询是把⼀个SELECT语句的结果当做别⼀个SELECT语句的......
  • 玄机蓝队靶场_应急响应_61:windows实战-emlog
    win+rmstsc连接不能联网,不然直接河马查杀一下把www文件夹压缩下载下来用河马在线网站也是直接得到冰蝎木马文件:/WWW/content/plugins/tips/shell.php在www文件夹直接搜索shell文件(盲猜)找到连接密码flag{rebeyond}直接找apache目录看日志phpstudy_pro/Extensions/Apache......
  • Android 11.0 系统屏幕灭屏时当收到通知时亮屏功能实现
    1.前言在11.0的系统rom定制化开发中,在对于设备灭屏时,收到通知短信功能系统默认是不亮屏的,但是由于产品开发需要要求在收到短信和通知的时候要求亮屏处理,接下来就来实现这个功能2.系统屏幕灭屏时当收到通知短信时亮屏功能实现的核心类frameworks/base/core/java/android/ap......
  • 例题2.11
    例题2.11代码importstringimportrandomx=string.ascii_letters+string.digitsy=''.join([random.choice(x)foriinrange(1000)])d=dict()forchiny:d[ch]=d.get(ch,0)+1fork,vinsorted(d.items()):print(k,':',v)......
  • 例题2.11_2
    例题2.11_2代码importstring,random,collectionsx=string.ascii_letters+string.digitsy=''.join([random.choice(x)foriinrange(1000)])count=collections.Counter(y)fork,vinsorted(count.items()):print(k,':',v)......