Vue3源码解析(四):ref原理与原始值的响应式方案

本文介绍了vue3中的ref的实现原理,还介绍了响应丢失(toRef、toRefs)的情况,以及自动脱ref是如何实现的,参考《Vue.js设计与实现》 更多Vue源码文章:

1. Vue3 源码解析(一):响应式数据和副作用函数、计算属性原理、侦听器原理

2. Vue3源码解析(二):响应式原理,如何拦截对象

3. Vue3源码解析(三):响应式原理,如何拦截数组

4. Vue2源码解析(一):响应式原理,如何拦截对象

5. Vue2源码解析(二):响应式原理,如何拦截数组

6. Vue3源码解析(三):如何代理Set和Map数据结构

快速回答版

  1. 介绍vue当中的ref的作用和内部原理
  • 将原始数据类型如数字、字符串、布尔值等转化为响应式数据
  • 内部实现是用一个对象包裹原始数据,属性为value,将对象传递给reactive函数
  • ref能够解决响应丢失问题,toRefs和toRef函数内部的实现原理和ref一致,借助了get拦截器
  1. vue3如何处理自动脱ref
  • 自动脱ref的定义:当访问ref响应式数据时,希望能够直接使用数据,不通过.value,在模版中常见
  • 实现方式:
    • vue3封装了proxyRefs函数,判断数据如果是ref数据(借助__v_isRef标识),则直接返回他的.value的内容。
    • setup里面返回的变量会自动传递给这个函数
  • reactive也能够实现自动脱ref

1. ref原理

Proxy无法对原始数据类型(包括number string boolean null undefined bigInt symbol)做代理,所以ref的实现必须得嵌套一层对象。

其内部实现如下:

js 复制代码
function ref (val) {
    const wrapper = {
        value: val
    }
    Object.defineProperty(wrapper, '__v_ifRef', {
        value: true
    })
    return reactive(wrapper)
}
  • 第一,必须用wrapper包裹val的值,属性是value,将该值传递给reactive函数转化为响应式数据。这也是使用ref的数据必须用.value来访问的原因
  • 给wrapper创建了一个__v_ifRef属性,用来区分是原始数据类型还是引用数据类型

2. 解决响应丢失问题

如下解构obj对象的值赋值给newObj对象,并在副作用函数中访问,期待修改obj.foo为10以后副作用函数重新执行,但是如下并不能执行。因为newObj只是一个普通的对象,不会建立响应联系

js 复制代码
const obj = reactive({
    foo: 1,
    bar: 2
})
const newObj = {
    ...obj
}
effect(() => {
    console.log(newObj.foo, 'newObj.foo');
});
obj.foo = 10

toRef函数toRefs函数内部实现原理:

  • toRef的返回值就是ref,借助ref来实现响应丢失问题
  • toRefs是批量调用toRef
js 复制代码
function toRef(obj, key) {
    const wrapper = {
        get value () {
            // 访问器函数里面,访问obj[key],访问obj这个proxy对象的某个属性
            return obj[key]
        } 
    }
    return wrapper
}
function toRefs(obj) {
    const ret = {}
    // 针对每个属性调用toRef,整个对象都转化为响应式
    for (const key in obj) {
        ret[key] = toRef(obj, key)
    }
    return ret
}

此时修改数据能够触发响应

js 复制代码
const obj = reactive({
    foo: 1,
    bar: 2
})
const newObj = {
    ...toRefs(obj)
}
effect(() => {
    console.log(newObj.foo.value, 'newObj.foo');
});

3. 自动脱ref

定义:定义一个ref响应式数据,在某些场合希望能够使用数据不用通过.value

举例:

  • 模版当中,直接访问newObj.foo,而不是newObj.foo.value
  • reactive包裹一个ref数据,也能够给他脱ref

这样写就会很麻烦

html 复制代码
<p>{{ newObj.foo.value }}</p>

希望能够这样写

html 复制代码
<p>{{ newObj.foo }}</p>

reactive函数的脱ref能力:

js 复制代码
const count = ref(0)
const obj = reactive({ count })
obj.count // 0

实现原理:

  • vue3会把setup函数里面返回的响应式数据传递给proxyRefs函数,进行自动脱ref
  • 借助__v_isRef标识来判断是否是响应式数据
js 复制代码
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
        }
    })
}

4. 请看源码

diff 复制代码
/packages/reactivity/src/ref.ts
export function ref(value?: unknown) {
  return createRef(value, false)
}
function createRef(rawValue: unknown, shallow: boolean) {
  if (isRef(rawValue)) {
    return rawValue
  }
  return new RefImpl(rawValue, shallow)
}

class RefImpl<T = any> {
  _value: T
  private _rawValue: T

  dep: Dep = new Dep()

  public readonly [ReactiveFlags.IS_REF] = true
  public readonly [ReactiveFlags.IS_SHALLOW]: boolean = false

  constructor(value: T, isShallow: boolean) {
    this._rawValue = isShallow ? value : toRaw(value)
+    this._value = isShallow ? value : toReactive(value) // 这里调用了toReactive
    this[ReactiveFlags.IS_SHALLOW] = isShallow
  }
  ......省略下面的代码
}
js 复制代码
/packages/reactivity/src/reactive.ts
export const toReactive = <T extends unknown>(value: T): T =>
  isObject(value) ? reactive(value) : value

proxyRefs函数

diff 复制代码
export function proxyRefs<T extends object>(
  objectWithRefs: T,
): ShallowUnwrapRef<T> {
  return isReactive(objectWithRefs)
    ? objectWithRefs
+    : new Proxy(objectWithRefs, shallowUnwrapHandlers) // 请关注这个shallowUnwrapHandlers
}


const shallowUnwrapHandlers: ProxyHandler<any> = {
  get: (target, key, receiver) =>
    key === ReactiveFlags.RAW
      ? target
+      : unref(Reflect.get(target, key, receiver)), // 这里执行了unref方法
  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)
    }
  },
}

export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
+  return isRef(ref) ? ref.value : ref // 如果是ref数据,直接返回他.value的值
}
相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅7 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax