当前位置: 首页 > article >正文

《Vue进阶教程》(12)ref的实现详细教程

1 为什么需要ref

由于proxy只能代理引用类型数据(如: 对象, 数组, Set, Map...), 需要一种方式代理普通类型数据(String, Number, Boolean...)

设计ref主要是为了处理普通类型数据, 使普通类型数据也具有响应式

除此之外, 通过reactive代理的对象可能会出现响应丢失的情况. 使用ref可以在一定程度上解决响应丢失问题

2 初步实现

1) 包裹对象

既然proxy不能代理普通类型数据, 我们可以在普通类型数据的外层包裹一个对象

proxy代理包裹的对象(wrapper). 为了统一, 给包裹对象定义value属性, 最后返回wrapper的代理对象

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

  return reactive(wrapper)
}

测试用例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <script>
      function ref(value) {
        const wrapper = {
          value: value,
        }

        return reactive(wrapper)
      }

      // count是一个proxy对象
      const count = ref(1)
      effect(() => {
        // 访问proxy对象的属性 触发 getter 收集依赖
        console.log(count.value)
      })

      setTimeout(() => {
        count.value = 2
      }, 1000)
    </script>
  </body>
</html>

2) 添加标识

按照上面的实现, 我们就无法区分一个代理对象是由ref创建, 还是由reactive创建, 比如下面的代码

ref(1)
reactive({value: 1})

为了后续能够对ref创建的代理对象自动脱ref处理, 即不用.value访问.

考虑给ref创建的代理对象添加一个标识

示例

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

  // 给wrapper添加一个不可枚举, 不可写的属性__v_isRef
  Object.defineProperty(wrapper, '__v_isRef', {
    value: true,
  })

  return reactive(wrapper)
}

在Vue3源码中, 虽然不是按上述方式实现的, 但是可以这样去理解

3 响应丢失问题

reactive定义的代理对象赋值给其它变量时, 会出现响应丢失问题

赋值主要有如下三种情况:

  1. 如果将reactive定义的代理对象的属性赋值给新的变量, 新变量会失去响应性
  2. 如果对reactive定义的代理对象进行展开操作. 展开后的变量会失去响应性
  3. 如果对reactive定义的代理对象进行解构操作. 解构后的变量会失去响应性

1) 赋值操作

示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <script>
      const obj = reactive({ foo: 1, bar: 2 })

      // 将reactive创建的代理对象的属性赋值给一个新的变量foo
      let foo = obj.foo // foo此时就是一个普通变量, 不具备响应性

      effect(() => {
        console.log('foo不具备响应性...', foo)
      })

      foo = 2
    </script>
  </body>
</html>
  • obj.foo表达式的返回值是1
  • 相当于定义了一个普通变量foo, 而普通变量是不具备响应性的

2) 展开操作

示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <script>
      const obj = reactive({ foo: 1, bar: 2 })

      // 展开运算符应用在proxy对象上, 会从语法层面遍历源对象的所有属性
      // ...obj ===> foo:1, bar:2
      const newObj = {
        ...obj,
      }
      // 此时的newObj是一个新的普通对象, 和obj之间不存在引用关系
      console.log(newObj) // {foo:1, bar:2}
      effect(() => {
        console.log('newObj没有响应性...', newObj.foo)
      })

      // 改变newObj的属性值, 不会触发副作用函数的重新执行
      // 此时, 我们就说newObj失去了响应性
      newObj.foo = 2
    </script>
  </body>
</html>

说明

这里对proxy对象展开会经历如下过程

  1. proxy对象属于异质对象(exotic object)
  2. 当没有自定义proxy对象的[[GetOwnProperty]]内部方法时, 会使用源对象的方法, 获取所有属性
  3. 调用GetValue, 获取属性值

参考文献

ECMAScript规范2022-对象初始化

2) 解构操作

示例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <script>
      const obj = reactive({ foo: 1, bar: 2 })

      // 对proxy对象进行解构操作后, foo和bar就是普通变量, 也失去了响应性
      let { foo, bar } = obj

      effect(() => {
        console.log('foo不具备响应性', foo)
      })

      // 给变量foo赋值, 不会触发副作用函数重新执行
      foo = 2
      
    </script>
  </body>
</html>

proxy对象解构后, foo就是一个普通变量, 也失去了跟obj的引用关系.

因此, 对foo的修改不会触发副作用函数重新执行

4 toRef与toRefs

1) 基本使用

为了解决在赋值过程中响应丢失问题, Vue3提供了两个API

  • toRef: 解决赋值问题
  • toRefs: 解决展开, 解构问题

使用演示

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script>
  </head>
  <body>
    <script>
      const { reactive, effect, toRef, toRefs } = Vue
      // obj是reactive创建的响应式数据(proxy代理对象)
      const obj = reactive({ foo: 1, bar: 2 })

      effect(() => {
        console.log('obj.foo具有响应性:', obj.foo)
      })

      // 使用toRef定义, 取代基本赋值操作 foo = obj.foo
      const foo = toRef(obj, 'foo')
      effect(() => {
        console.log('foo.value具有响应性:', foo.value)
      })
    </script>
  </body>
</html>
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="https://unpkg.com/vue@3.2.41/dist/vue.global.js"></script>
  </head>
  <body>
    <script>
      const { reactive, effect, toRef, toRefs } = Vue
      // obj是reactive创建的响应式数据(proxy代理对象)
      const obj = reactive({ foo: 1, bar: 2 })

      // 使用toRefs解构赋值 取代 {foo, bar} = obj
      const { foo, bar } = toRefs(obj)
      effect(() => {
        console.log('bar.value具有响应性', bar.value)
      })
    </script>
  </body>
</html>

2) toRef的实现

基本实现

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
}

在Vue3中, 将wrapper抽象成了ObjectRefImpl类的实例, 大致的实现如下

class ObjectRefImpl {
  constructor(_obj, _key) {
    this._obj = _obj
    this._key = _key
    this.__v_isRef = true
  }
  get value() {
    return this._obj[this._key]
  }
  set value(newVal) {
    this._obj[this._key] = newVal
  }
}

function toRef(obj, key) {
  return new ObjectRefImpl(obj, key)
}

源码解读

  1. 源码中的toRef实现了默认值的功能
  2. 源码中的toRef对要转换的数据做了判断, 如果已经是ref类型就直接返回
class ObjectRefImpl {
  // 支持默认值
  constructor(_object, _key, _defaultValue) {
    this._object = _object
    this._key = _key
    this._defaultValue = _defaultValue
    this.__v_isRef = true
  }
  get value() {
    const val = this._object[this._key]
    return val === undefined ? this._defaultValue : val
  }
  set value(newVal) {
    this._object[this._key] = newVal
  }
}
// 1. 支持默认值
function toRef(object, key, defaultValue) {
  const val = object[key]
  // 2. 如果要转换的对象已经是ref类型, 直接返回
  //    eg: state = reactive({foo: ref(1)}) state.foo已经是ref类型, 直接返回ref(1)
  return isRef(val) ? val : new ObjectRefImpl(object, key, defaultValue)
}

测试用例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <script>
      // obj是响应式数据
      const obj = reactive({ foo: 1, bar: 2 })

      const foo = toRef(obj, 'foo')

      effect(() => {
        console.log('foo.value具备响应性:', foo.value)
      })
    </script>
  </body>
</html>

3) toRefs的实现

基本实现

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

原码解读

  1. 源码中对obj的类型做了判断
    1. 如果不是reactive类型的对象, 提示警告
    2. 支持代理是数组的情况
function toRefs(object) {
  // 如果传入的对象不具备响应性, 提示警告
  if (!isProxy(object)) {
    console.warn(
      `toRefs() expects a reactive object but received a plain one.`
    )
  }
  // 支持代理是数组的情况
  //   - 对象的情况: toRefs(reactive({foo: 1, bar: 2})) => {foo: ref(1), bar: ref(2)}
  //   - 数组的情况: toRefs(reactive(['foo', 'bar'])) => [ref('foo'), ref('bar')]
  const ret = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = toRef(object, key)
  }
  return ret
}

测试用例

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./reactive.js"></script>
  </head>
  <body>
    <script>
      // obj是响应式数据
      const obj = reactive({ foo: 1, bar: 2 })

      // 解构赋值
      const { foo, bar } = toRefs(obj)

      effect(() => {
        console.log('foo.value具备响应性:', foo.value)
      })
    </script>
  </body>
</html>

5 自动脱ref

1) 什么是自动脱ref

所谓自动脱ref, 就是不写.value

对于ref类型数据, 每次在访问时, 需要加.value才能触发响应式.

但是这样做无疑增加了心智负担, 尤其是在写模板时, 不够优雅

为此, Vue3提供一个API: proxyRefs

对传入的ref类型对象进行代理, 返回proxy对象

个人理解: 有点类似toRefs的逆操作??

2) 基本使用

使用演示

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="./vue.js"></script>
  </head>
  <body>
    <script>
      const { ref, proxyRefs, effect } = Vue
      const count = ref(0)

    	// 1.模拟setup的返回对象
      // setup函数返回的对象会经过proxyRefs处理
      // 这样在模板中就不用写.value了
      const setup = proxyRefs({
        count,
      })

    	// 2.模拟页面渲染
      effect(() => {
        console.log('不用通过.value访问', setup.count)
      })
    </script>
  </body>
</html>

3) proxyRefs的实现

基本实现

function proxyRefs(objectWithRefs) {
  return new Proxy(objectWithRefs, {
    get(target, key, receiver) {
      // 使用Reflect读取target[key]
      const obj = Reflect.get(target, key, receiver)
      // 如果obj是ref类型, 返回obj.value; 否则, 直接返回
      return obj.__v_isRef ? obj.value : obj
    },
    set(target, key, newVal, receiver) {
      const obj = target[key]
      if (obj.__v_isRef) {
        obj.value = newVal
        return obj
      }
      return Reflect.set(target, key, newVal, receiver)
    },
  })
}

源码解读

  1. 源码对传入参数加强了判断
    1. 如果objectWithRefs已经是reactive类型, 就直接使用
  1. 源码按功能进一步细化, 可读性更高
    1. unref函数可以复用: 如果是ref返回.value; 否则直接返回
    2. 将proxy的handler提取成shallowUnwrapHandlers函数
    3. 在set时, 加入了新旧值类型的判断, 更严谨
function unref(ref) {
  return isRef(ref) ? ref.value : ref
}
const shallowUnwrapHandlers = {
  get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
  set: (target, key, value, receiver) => {
    const oldValue = target[key]
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value
      return true
    } else {
      return Reflect.set(target, key, value, receiver)
    }
  },
}
function proxyRefs(objectWithRefs) {
  return isReactive(objectWithRefs)
    ? objectWithRefs
    : new Proxy(objectWithRefs, shallowUnwrapHandlers)
}

6 改造

按照vue的源码进行改造

function isObject(val) {
  return typeof val === 'object' && val !== null
}
function toReactive(value) {
  return isObject(value) ? reactive(value) : value
}
class RefImpl {
  constructor(value) {
    this.dep = new Set()
    this._rawValue = value
    this.__v_isRef = true
    this._value = toReactive(value)
  }
  get value() {
    trackEffects(this.dep)
    return this._value
  }
  set value(newValue) {
    if (this._rawValue != newValue) {
      this._rawValue = newValue
      this._value = toReactive(newValue)
      triggerEffects(this.dep)
    }
  }
}
function ref(val) {
  return new RefImpl(val)
}


http://www.kler.cn/a/453021.html

相关文章:

  • 有没有免费提取音频的软件?音频编辑软件介绍!
  • 大语言模型中的Agent;常见的Agent开发工具或框架
  • 【Linux】进程间通信 -> 匿名管道命名管道
  • Mybatis 小结
  • 全国青少年信息学奥林匹克竞赛(信奥赛)备考实战之分支结构(实战训练三)
  • StarRocks一次复杂查询引起的Planner超时异常
  • 高级网络工程师需要不断的学习和实践,保持对技术发展的敏锐性和洞察力,同时能够在复杂环境中解决问题和推动创新。
  • <代码随想录> 算法训练营-2024.12.19
  • uniapp+vue 前端防多次点击表单,防误触多次请求方法。
  • 解决uniapp APP端切换横竖屏,页面排版崩溃问题
  • 手机IP地址:定义、查看与切换方法
  • 地理数据库Telepg面试内容整理-分布式与高可用
  • 网络安全 | 云计算中的数据加密与访问控制
  • Java学习笔记(15)——面向对象编程
  • 一个基于Rust适用于 Web、桌面、移动设备等的全栈应用程序框架
  • YOLO11改进-注意力-引入多尺度卷积注意力模块MSCAM
  • Git:远程操作
  • 【STM32】F103ZET6开发板----笔记01
  • 图像修复和编辑大一统 | 腾讯北大等联合提出BrushEdit:BrushNet进阶版来了
  • mysql的备份和还原
  • java 核心知识点——JVM
  • 时间轮在 Netty , Kafka 中的设计与实现
  • 云原生后端开发(一)
  • 数字逻辑(六)——下载Digital软件
  • 计算机视觉目标检测-1
  • ffmpeg: stream_loop报错 Error while filtering: Operation not permitted