vue3-core/packages/reactivity/src/ref.ts

508 lines
13 KiB
TypeScript
Raw Normal View History

import type { ComputedRef } from './computed'
2022-01-30 18:52:23 +08:00
import {
activeEffect,
getDepFromReactive,
2022-01-30 18:52:23 +08:00
shouldTrack,
trackEffects,
triggerEffects
} from './effect'
import { TrackOpTypes, TriggerOpTypes } from './operations'
import { isArray, hasChanged, IfAny, isFunction, isObject } from '@vue/shared'
import {
isProxy,
toRaw,
isReactive,
toReactive,
isReadonly,
isShallow
} from './reactive'
2021-10-10 06:06:12 +08:00
import type { ShallowReactiveMarker } from './reactive'
2019-10-22 23:26:48 +08:00
import { CollectionTypes } from './collectionHandlers'
2021-07-08 02:37:28 +08:00
import { createDep, Dep } from './dep'
2019-08-16 21:42:46 +08:00
declare const RefSymbol: unique symbol
export declare const RawSymbol: unique symbol
2019-10-10 02:01:53 +08:00
export interface Ref<T = any> {
value: T
/**
* Type differentiator only.
* We need this to be in public d.ts but don't want it to show up in IDE
* autocomplete, so we use a private Symbol instead.
*/
[RefSymbol]: true
}
type RefBase<T> = {
dep?: Dep
value: T
}
export function trackRefValue(ref: RefBase<any>) {
2022-01-30 18:52:23 +08:00
if (shouldTrack && activeEffect) {
ref = toRaw(ref)
if (__DEV__) {
2022-01-30 18:52:23 +08:00
trackEffects(ref.dep || (ref.dep = createDep()), {
target: ref,
type: TrackOpTypes.GET,
key: 'value'
})
} else {
2022-01-30 18:52:23 +08:00
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
}
export function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
if (__DEV__) {
triggerEffects(dep, {
target: ref,
type: TriggerOpTypes.SET,
key: 'value',
newValue: newVal
})
} else {
triggerEffects(dep)
}
}
2019-08-16 21:42:46 +08:00
}
/**
* Checks if a value is a ref object.
*
* @param r - The value to inspect.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#isref}
*/
export function isRef<T>(r: Ref<T> | unknown): r is Ref<T>
export function isRef(r: any): r is Ref {
return !!(r && r.__v_isRef === true)
}
/**
* Takes an inner value and returns a reactive and mutable ref object, which
* has a single property `.value` that points to the inner value.
*
* @param value - The object to wrap in the ref.
* @see {@link https://vuejs.org/api/reactivity-core.html#ref}
*/
export function ref<T extends Ref>(value: T): T
export function ref<T>(value: T): Ref<UnwrapRef<T>>
export function ref<T = any>(): Ref<T | undefined>
2020-01-28 02:13:38 +08:00
export function ref(value?: unknown) {
return createRef(value, false)
}
declare const ShallowRefMarker: unique symbol
export type ShallowRef<T = any> = Ref<T> & { [ShallowRefMarker]?: true }
/**
* Shallow version of {@link ref()}.
*
* @example
* ```js
* const state = shallowRef({ count: 1 })
*
* // does NOT trigger change
* state.value.count = 2
*
* // does trigger change
* state.value = { count: 2 }
* ```
*
* @param value - The "inner value" for the shallow ref.
* @see {@link https://vuejs.org/api/reactivity-advanced.html#shallowref}
*/
export function shallowRef<T>(value: MaybeRef<T>): Ref<T> | ShallowRef<T>
export function shallowRef<T extends Ref>(value: T): T
export function shallowRef<T>(value: T): ShallowRef<T>
export function shallowRef<T = any>(): ShallowRef<T | undefined>
export function shallowRef(value?: unknown) {
return createRef(value, true)
}
function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
2023-08-11 17:30:04 +08:00
constructor(
value: T,
public readonly __v_isShallow: boolean
) {
2022-01-18 09:17:22 +08:00
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
/**
* Force trigger effects that depends on a shallow ref. This is typically used
* after making deep mutations to the inner value of a shallow ref.
*
* @example
* ```js
* const shallow = shallowRef({
* greet: 'Hello, world'
* })
*
* // Logs "Hello, world" once for the first run-through
* watchEffect(() => {
* console.log(shallow.value.greet)
* })
*
* // This won't trigger the effect because the ref is shallow
* shallow.value.greet = 'Hello, universe'
*
* // Logs "Hello, universe"
* triggerRef(shallow)
* ```
*
* @param ref - The ref whose tied effects shall be executed.
* @see {@link https://vuejs.org/api/reactivity-advanced.html#triggerref}
*/
export function triggerRef(ref: Ref) {
triggerRefValue(ref, __DEV__ ? ref.value : void 0)
}
export type MaybeRef<T = any> = T | Ref<T>
export type MaybeRefOrGetter<T = any> = MaybeRef<T> | (() => T)
/**
* Returns the inner value if the argument is a ref, otherwise return the
* argument itself. This is a sugar function for
* `val = isRef(val) ? val.value : val`.
*
* @example
* ```js
* function useFoo(x: number | Ref<number>) {
* const unwrapped = unref(x)
* // unwrapped is guaranteed to be number now
* }
* ```
*
* @param ref - Ref or plain value to be converted into the plain value.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#unref}
*/
export function unref<T>(ref: MaybeRef<T> | ComputedRef<T>): T {
return isRef(ref) ? ref.value : ref
}
/**
* Normalizes values / refs / getters to values.
* This is similar to {@link unref()}, except that it also normalizes getters.
* If the argument is a getter, it will be invoked and its return value will
* be returned.
*
* @example
* ```js
* toValue(1) // 1
* toValue(ref(1)) // 1
* toValue(() => 1) // 1
* ```
*
* @param source - A getter, an existing ref, or a non-function value.
* @see {@link https://vuejs.org/api/reactivity-utilities.html#tovalue}
*/
export function toValue<T>(source: MaybeRefOrGetter<T> | ComputedRef<T>): T {
return isFunction(source) ? source() : unref(source)
}
const shallowUnwrapHandlers: ProxyHandler<any> = {
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)
}
}
}
/**
* Returns a reactive proxy for the given object.
*
* If the object already is reactive, it's returned as-is. If not, a new
* reactive proxy is created. Direct child properties that are refs are properly
* handled, as well.
*
* @param objectWithRefs - Either an already-reactive object or a simple object
* that contains refs.
*/
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
export type CustomRefFactory<T> = (
track: () => void,
trigger: () => void
) => {
get: () => T
set: (value: T) => void
}
class CustomRefImpl<T> {
public dep?: Dep = undefined
private readonly _get: ReturnType<CustomRefFactory<T>>['get']
private readonly _set: ReturnType<CustomRefFactory<T>>['set']
public readonly __v_isRef = true
constructor(factory: CustomRefFactory<T>) {
const { get, set } = factory(
() => trackRefValue(this),
() => triggerRefValue(this)
)
this._get = get
this._set = set
}
get value() {
return this._get()
}
set value(newVal) {
this._set(newVal)
}
}
/**
* Creates a customized ref with explicit control over its dependency tracking
* and updates triggering.
*
* @param factory - The function that receives the `track` and `trigger` callbacks.
* @see {@link https://vuejs.org/api/reactivity-advanced.html#customref}
*/
export function customRef<T>(factory: CustomRefFactory<T>): Ref<T> {
return new CustomRefImpl(factory) as any
}
export type ToRefs<T = any> = {
[K in keyof T]: ToRef<T[K]>
}
/**
* Converts a reactive object to a plain object where each property of the
* resulting object is a ref pointing to the corresponding property of the
* original object. Each individual ref is created using {@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) : {}
2019-08-20 21:38:00 +08:00
for (const key in object) {
ret[key] = propertyToRef(object, key)
2019-08-20 21:38:00 +08:00
}
return ret
}
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)
}
}
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>>
/**
* Used to normalize values / refs / getters into refs.
*
* @example
* ```js
* // 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)
* ```
*
* Can also be used to create a ref for a property on a source reactive object.
* The created ref is synced with its source property: mutating the source
* property will update the ref, and vice-versa.
*
* @example
* ```js
* const state = reactive({
* foo: 1,
* bar: 2
* })
*
* const fooRef = toRef(state, 'foo')
*
* // mutating the ref updates the original
* fooRef.value++
* console.log(state.foo) // 2
*
* // mutating the original also updates the ref
* state.foo++
* console.log(fooRef.value) // 3
* ```
*
* @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}
*/
export function toRef<T>(
value: T
): T extends () => infer R
? Readonly<Ref<R>>
: T extends Ref
? T
: Ref<UnwrapRef<T>>
export function toRef<T extends object, K extends keyof T>(
2019-08-20 21:38:00 +08:00
object: T,
key: K
): ToRef<T[K]>
export function toRef<T extends object, K extends keyof T>(
object: T,
key: K,
defaultValue: T[K]
): ToRef<Exclude<T[K], undefined>>
export function toRef(
source: Record<string, any> | MaybeRef,
key?: string,
defaultValue?: unknown
): Ref {
if (isRef(source)) {
return source
} else if (isFunction(source)) {
return new GetterRefImpl(source) as any
} else if (isObject(source) && arguments.length > 1) {
return propertyToRef(source, key!, defaultValue)
} else {
return ref(source)
}
}
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)
2019-08-16 21:42:46 +08:00
}
// corner case when use narrows type
// Ex. type RelativePath = string & { __brand: unknown }
// RelativePath extends object -> true
type BaseTypes = string | number | boolean
/**
* This is a special exported interface for other packages to declare
* additional types that should bail out for ref unwrapping. For example
* \@vue/runtime-dom can declare it like so in its d.ts:
*
* ``` ts
* declare module '@vue/reactivity' {
* export interface RefUnwrapBailTypes {
* runtimeDOMBailTypes: Node | Window
* }
* }
* ```
*/
export interface RefUnwrapBailTypes {}
export type ShallowUnwrapRef<T> = {
[K in keyof T]: T[K] extends Ref<infer V>
? V // if `V` is `unknown` that means it does not extend `Ref` and is undefined
: T[K] extends Ref<infer V> | undefined
2022-10-28 09:39:34 +08:00
? unknown extends V
? undefined
: V | undefined
: T[K]
}
export type UnwrapRef<T> = T extends ShallowRef<infer V>
? V
: T extends Ref<infer V>
2020-04-09 04:33:06 +08:00
? UnwrapRefSimple<V>
: UnwrapRefSimple<T>
2020-04-09 04:33:06 +08:00
export type UnwrapRefSimple<T> = T extends
| Function
| CollectionTypes
| BaseTypes
| Ref
| RefUnwrapBailTypes[keyof RefUnwrapBailTypes]
| { [RawSymbol]?: true }
2020-04-09 04:33:06 +08:00
? T
: T extends ReadonlyArray<any>
? { [K in keyof T]: UnwrapRefSimple<T[K]> }
: T extends object & { [ShallowReactiveMarker]?: never }
? {
[P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>
}
2021-07-20 06:24:18 +08:00
: T