Skip to content

to系列

toRef

用于修改响应式对象的值,但是对非响应式对象作修改其视图不会跟着变化。所以并没什么用,不过为接下来使用toRefs可以起到理解作用。

toRef会提供一个访问响应式对象的属性的ref,使得在prop的ref传递或者复合对象解构时可以保持响应式。

示例代码:

Vue
<template>
    <div>
        {{ rarrot }}
    </div>
    <hr>
    <div>
        {{ rarrot2 }}
    </div>
    <button @click="edit">修改</button>
</template>

<script setup lang='ts'>
import { toRef, reactive } from 'vue'

// toRef 只能修改响应式对象的值  
const rarrot = reactive({ name: 'rarrot', age: 66 })
const name = toRef(rarrot, 'name')

// toRef 修改非响应式对象其视图不会变化
const rarrot2 = { name: 'rarrot2', age: 99 }
const name2 = toRef(rarrot2, 'name')

const edit = () => {
    name.value = 'nihao'
    console.log("🚀  rarrot", rarrot)
    name2.value='nihao2'
    console.log("🚀  rarrot2", rarrot2)
}

</script>
<style scoped></style>


toRefs

toRefs可以用于对象解构,由于响应式对象解构之后的属性是不具有响应式的,所以需要使用到toRef进行保持响应式。

使用方式的示例代码:

Vue
<template>
    <div>
        {{ rarrot }}
    </div>
    <hr>
    <div>
        {{ name }}---{{ age }}
    </div>
    <button @click="edit">修改</button>
</template>

<script setup lang='ts'>
import { reactive,toRefs } from 'vue'

const rarrot = reactive({ name: 'rarrot', age: 66 })

let { name, age } = toRefs(rarrot)

// 像以下这种直接解构视图是不会变化的
// let {name,age}=rarrot

const edit = () => {
    name.value = 'nihao'
    console.log("🚀  rarrot", rarrot)
}

</script>
<style scoped></style>


简易的实现toRefs,看以下示例代码:

Vue
<template>
    <div>
        {{ rarrot }}
    </div>
    <hr>
    <div>
        {{ name }}---{{ age }}
    </div>
    <button @click="edit">修改</button>
</template>

<script setup lang='ts'>
import { toRef, reactive, toRaw } from 'vue'

const rarrot = reactive({ name: 'rarrot', age: 66 })

const toRefs = <T extends object>(object: T) => {
    const map: any = {}
    for (let key in object) {
        // const name=toRef(rarrot,'name')其实下面这行跟这一行一样,对比着来看
  // 真正的源码里面是调用的propertyToRef方法,然后在调用的ObjectRefImpl方法


        map[key] = toRef(object, key)
    }
    return map
}

// 适用于解构赋值,torefs就是按照传入参数一次性解构多个
let { name, age } = toRefs(rarrot)

const edit = () => {
    console.log("初始的🚀  rarrot", rarrot);

    name.value = 'nihao'
    age.value = 33
    // 以下这种方式也可以使得视图变化
    // rarrot.name='nihao'
    // rarrot.age=33
    console.log("🚀  rarrot", rarrot)

}

</script>
<style scoped></style>


toRaw

toRaw()可以从reactive()readonly()shallowReactive()或 shallowReadonly().创建的proxy返回原始对象。


具有两个使用场景:

  1. 在前面的ref源码中,就有将ref响应式对象转换为原始对象进行比较,然后观察属性值或引用是否有改变,有改变的话,就进行依赖的更新。
  2. 有些情况下我们需要临时获得原始对象的值,并不希望引起代理对象更改的副作用。比如说浏览器本地存储、 Cookies 等,它们不能存储 Proxy 对象,我们可以使用 toRaw() 来获取源对象并存储。

看示例:

Vue
<template>
    <div>
        {{ rarrot }}
    </div>
    <hr>

    <button @click="edit">修改</button>
</template>

<script setup lang='ts'>
import { reactive, toRaw } from 'vue'

const rarrot = reactive({ name: 'rarrot', age: 66 })

const edit = () => {
    console.log(rarrot,toRaw(rarrot));
    // toRaw其实是跟以下代码一样的,在源码中也是这样做的
    console.log(rarrot,rarrot['__v_raw']);
}

</script>
<style scoped></style>


源码解析

toRef

typescript
/**
 * Used to normalize values / refs / getters into refs.
 * 用于将值/引用/获取器规范化为引用。
 *
 * @example
 * 
 * // returns existing refs as-is
 * toRef(existingRef)
 *
 * // creates a ref that calls the getter on .value access
 * toRef(() => props.foo)
 *
 * // creates normal refs from non-function values
 * // equivalent to ref(1)
 * toRef(1)
 * 
 *
 * 还可用于为源反应对象上的属性创建引用。
 * 创建的引用与其源属性同步:改变源
 * 属性将更新引用,反之亦然。
 * 
 * @example
 * 前面给出了示例
 *
 * @param source - A getter, an existing ref, a non-function value, or a
 *                 reactive object to create a property ref from.
 * @param [key] - (optional) Name of the property in the reactive object.
 * @see {@link https://vuejs.org/api/reactivity-utilities.html#toref}
 */
// 当只传一个参数value时,若参数为普通值,返回包含这个值的Ref;若已经是Ref,返回自身
export function toRef<T>(
  value: T
): T extends () => infer R
  ? Readonly<Ref<R>>
  : T extends Ref
  ? T
  : Ref<UnwrapRef<T>>

// 传两个参数,第一个参数为引用类型,第二个参数为对象的键名,也是属性名
// 返回ToRef类型,包含指定对象原属性的Ref
export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K
): ToRef<T[K]>

// 传三个参数,第三个参数为默认值,返回包含默认值的ToRef
export function toRef<T extends object, K extends keyof T>(
  object: T,
  key: K,
  defaultValue: T[K]
): ToRef<Exclude<T[K], undefined>>

// 以下函数为实现代码,会根据参数不同返回不同结果,主要用于将对象的属性转换为ref对象
export function toRef(
  source: Record<string, any> | MaybeRef,
  key?: string,
  defaultValue?: unknown
): Ref {
  // 判断是否为Ref,是就直接返回源ref对象


  if (isRef(source)) {
    return source
  } else if (isFunction(source)) {
  // 判断是一个函数,就用GetterRefImpl类创建一个ref对象返回

  // 这个类实现了一个可以惰性获取值的ref


    return new GetterRefImpl(source) as any
  } else if (isObject(source) && arguments.length > 1) {
  // 判断是否为引用类型,是就调用以下函数


    return propertyToRef(source, key!, defaultValue)
  } else {
    return ref(source)
  }
}

以上代码中会去调用propertyToRef(source, key!, defaultValue)


GetterRefImpl(source)ToRef类型的源代码如下:

typescript
class GetterRefImpl<T> {
  public readonly __v_isRef = true
  public readonly __v_isReadonly = true
  constructor(private readonly _getter: () => T) { }
  get value() {
    return this._getter()
  }
}

export type ToRef<T> = IfAny<T, Ref<T>, [T] extends [Ref] ? T : Ref<T>>

propertyToRef(source, key!, defaultValue)的源代码如下:

typescript
function propertyToRef(
  source: Record<string, any>,
  key: string,
  defaultValue?: unknown
) {
  const val = source[key]
  return isRef(val)
    ? val
    : (new ObjectRefImpl(source, key, defaultValue) as any)
}

以上代码先从源对象source中获取key属性的值val,用val进行判断是否为ref对象,不是就调用ObjectRefImpl(source, key, defaultValue)


ObjectRefImpl(source, key, defaultValue)的源代码如下:

typescript
class ObjectRefImpl<T extends object, K extends keyof T> {
  public readonly __v_isRef = true

   constructor(
    private readonly _object: T,
    private readonly _key: K,
    private readonly _defaultValue?: T[K]
  ) { }

  get value() {
    const val = this._object[this._key]
    return val === undefined ? this._defaultValue! : val
  }

  set value(newVal) {
    this._object[this._key] = newVal
  }

  get dep(): Dep | undefined {
    return getDepFromReactive(toRaw(this._object), this._key)
  }
}

以下函数类似于ref中的RefImpl(),只不过RefImpl里面进行了收集依赖和更新依赖,而这里没有,这也是为什么普通对象不会有视图的变化,响应式对象使用reactive,reactive里面语句会进行收集依赖和更新依赖的操作。而这里实现了一个可以跟踪源对象属性变化的ref,并且也有get和set实现响应式。


toRefs

跟前面简易写一个toRefs做的操作是一样的,只不过会先调用propertyToRef(object, key)判断对象的每个属性是否为ref对象。再跟上面toRef一样的流程,只不过这里对每个解构的属性都进行了一遍。

typescript
/**
 *
 * 将响应式对象中每个属性都转换为普通对象,也就是解构
 * 结果对象是一个指向相应属性的引用
 * 原始对象。每个单独的引用都是使用 {@link toRef()} 创建的。
 *
 * @param object - Reactive object to be made into an object of linked refs.
 * @see {@link https://vuejs.org/api/reactivity-utilities.html#torefs}
 */
export function toRefs<T extends object>(object: T): ToRefs<T> {
  if (__DEV__ && !isProxy(object)) {
    console.warn(`toRefs() expects a reactive object but received a plain one.`)
  }
  const ret: any = isArray(object) ? new Array(object.length) : {}
  for (const key in object) {
    ret[key] = propertyToRef(object, key)
  }
  return ret
}

toRaw

用于转换为原始对象,实际就是通过将原始对象存入到proxy中,使用ReactiveFlags.RAW的__v_raw参数可取出原始参数。

typescript
/**
 * Returns the raw, original object of a Vue-created proxy.
 *
 * `toRaw()` can return the original object from proxies created by
 * {@link reactive()}, {@link readonly()}, {@link shallowReactive()} or
 * {@link shallowReadonly()}.
 *
 * This is an escape hatch that can be used to temporarily read without
 * incurring proxy access / tracking overhead or write without triggering
 * changes. It is **not** recommended to hold a persistent reference to the
 * original object. Use with caution.
 *
 * @example
 * 
 * const foo = {}
 * const reactiveFoo = reactive(foo)
 *
 * console.log(toRaw(reactiveFoo) === foo) // true
 * 
 *
 * @param observed - The object for which the "raw" value is requested.
 * @see {@link https://vuejs.org/api/reactivity-advanced.html#toraw}
 */
export function toRaw<T>(observed: T): T {
  const raw = observed && (observed as Target)[ReactiveFlags.RAW]
  return raw ? toRaw(raw) : observed
}

typescript
export const enum ReactiveFlags {
  SKIP = '__v_skip',
  IS_REACTIVE = '__v_isReactive',
  IS_READONLY = '__v_isReadonly',
  IS_SHALLOW = '__v_isShallow',
  RAW = '__v_raw'
}

Released under the MIT License.