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的值
}
相关推荐
掘金酱2 分钟前
😊 酱酱宝的推荐:做任务赢积分“拿”华为MatePad Air、雷蛇机械键盘、 热门APP会员卡...
前端·后端·trae
热爱编程的小曾13 分钟前
sqli-labs靶场 less 11
前端·css·less
丁总学Java19 分钟前
wget(World Wide Web Tool) 教程:Mac ARM 架构下安装与使用指南!!!
前端·arm开发·macos
总之就是非常可爱24 分钟前
🚀 使用 ReadableStream 优雅地处理 SSE(Server-Sent Events)
前端·javascript·后端
shoa_top35 分钟前
Cookie、sessionStorage、localStorage、IndexedDB介绍
前端
鸿蒙场景化示例代码技术工程师40 分钟前
实现文本场景化鸿蒙示例代码
前端
ᖰ・◡・ᖳ42 分钟前
Web APIs阶段
开发语言·前端·javascript·学习
stoneSkySpace1 小时前
算法——BFS
前端·javascript·算法
H5开发新纪元1 小时前
基于 Vue3 + TypeScript + Vite 的现代化移动端应用架构实践
前端·javascript
云原生应用市场1 小时前
一键私有化部署Dify,轻松搞定 AI 智能客服机器人
运维·前端·后端