The Ref family is undoubtedly one of the most frequently used of Vue3’s new responsive apis, and computed properties is a familiar option from previous releases, but Vue3 also provides a separate API for creating computed values directly. In this article, I will show you how REF and computed work work, so let’s start this chapter together.
ref
When we have a single raw value, such as a string, and we want to make it reactive, we can create an object, put that string in the object as a key-value pair, and pass it to Reactive. Vue gives us an easier way to do this through ref.
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) / / 0
count.value++
console.log(count.value) / / 1
Copy the code
Ref returns a mutable reactive object that maintains its internal value as a reactive reference, which is where the ref name comes from. This object contains only one property named Value.
How exactly does ref work?
Ref source location in @ vue/reactivity of the rolls, the path is packages/reactivity/SRC/ref. Ts, then we will see together the realization of the ref.
export function ref<T extends object> (value: T) :ToRef<T>
export function ref<T> (value: T) :Ref<UnwrapRef<T>>
export function ref<T = any> () :Ref<T | undefined>
export function ref(value? : unknown) {
return createRef(value)}Copy the code
From the function signature of the REF API, you can see that the REF function takes a value of any type as its value argument and returns a value of type REF.
export interface Ref<T = any> {
value: T
[RefSymbol]: true_shallow? :boolean
}
Copy the code
From the type definition of the return value Ref, we can see that the return value of Ref has a value attribute, a private Symbol key, and an attribute that identifies whether or not it is shallowRef _shallow Boolean.
The createRef function returns the value directly in the body of the function.
createRef
function createRef(rawValue: unknown, shallow = false) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
Copy the code
CreateRef is also simple to implement, taking rawValue and shallow, the original value of the created ref recorded by rawValue, and shallow, the shallow responsive API that indicates whether or not it is a shallowRef.
The logic of the function is to use isRef first to determine whether it is rawValue, and if so, to return the ref object directly.
Otherwise return an instance object of the newly created RefImpl class.
RefImpl class
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow: boolean) {
// If the response is shallow, set _value to _rawValue directly; otherwise, process _rawValue with convert
this._value = _shallow ? _rawValue : convert(_rawValue)
}
get value() {
// Collect value dependencies through track before reading value
track(toRaw(this), TrackOpTypes.GET, 'value')
return this._value
}
set value(newVal) {
// Update if needed
if (hasChanged(toRaw(newVal), this._rawValue)) {
// Update _rawValue and _value
this._rawValue = newVal
this._value = this._shallow ? newVal : convert(newVal)
// Send value updates through trigger
trigger(toRaw(this), TriggerOpTypes.SET, 'value', newVal)
}
}
}
Copy the code
In the RefImpl class, there is a private variable _value that stores the latest value of ref; The common read-only variable __v_isRef is the same flag used to indicate that the object is a Ref-responsive object as ReactiveFlag is used when talking about reactive apis.
The RefImpl constructor accepts a private _rawValue variable that holds the old ref value. The common _shallow variable is used to distinguish shallow responses. Inside the constructor, it first checks whether _shallow is true or, if shallowRef, assigns the original value directly to _value, otherwise it is converted via convert and then assigned.
Inside the conver function, it checks whether the passed parameter is an object. If it is an object, it creates a proxy object through the Reactive API and returns it. Otherwise, it returns the original parameter.
When we read the value of the ref in the form of ref.value, the getter method of value will be triggered. In the getter, the value dependencies of the ref object will be collected through track first, and the value of the ref will be returned after collection.
When we modify the ref. Value, the setter method of value will be triggered to compare the old and new values. If the values are different and need to be updated, the old and new values will be updated first, and then the update of the value attribute of the ref object will be distributed through trigger. Let side effects functions that depend on the REF perform updates.
If you are confused about the reliance on track collection and the distribution of trigger updates, you are advised to read my last article first. I explained this process in detail in the last article, so far I have explained the implementation of REF clearly to you.
computed
In the documentation for computed API, you take a getter function and return an immutable, responsive ref object with the return value of the getter function. Or it can use objects with get and set functions to create a writable ref object.
The computed function
According to the API’s description, it’s obvious that computed takes a parameter of a function or object type, so let’s start with its function signature.
export function computed<T> (getter: ComputedGetter<T>) :ComputedRef<T>
export function computed<T> (
options: WritableComputedOptions<T>
) :WritableComputedRef<T>
export function computed<T> (
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
)
Copy the code
In overloaded computed functions, the first line of code that takes a parameter of type getter and returns a function signature of type ComputedRef is the first case described in the documentation that takes a getter function, And returns an immutable reactive ref object with the return value of the getter function.
In the second line of code, a computed function that takes an Options object and returns a writable ComputedRef type is the second case of the document, creating a writable REF object.
The third line of code is the broadest case of this function overloading, as indicated by the parameter name: getterOrOptions.
Take a look at the related type definitions in computed API:
export interface ComputedRef<T = any> extends WritableComputedRef<T> {
readonly value: T
}
export interface WritableComputedRef<T> extends Ref<T> {
readonly effect: ReactiveEffect<T>
}
export type ComputedGetter<T> = (ctx? :any) = > T
export type ComputedSetter<T> = (v: T) = > void
export interface WritableComputedOptions<T> {
get: ComputedGetter<T>
set: ComputedSetter<T>
}
Copy the code
According to the type definition, both WritableComputedRef and ComputedRef extend from Ref, which explains why the documentation says that computed returns a responsive object of Ref type.
Now look at the complete logic in a computed API function:
export function computed<T> (
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// If the argument getterOrOptions is a function
if (isFunction(getterOrOptions)) {
// Then this function must be the getter. Assign the function to the getter
getter = getterOrOptions
// A warning is issued if the setter is accessed in the DEV environment
setter = __DEV__
? () = > {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
// If the parameter is an options, get and set are assigned
getter = getterOrOptions.get
setter = getterOrOptions.set
}
return newComputedRefImpl( getter, setter, isFunction(getterOrOptions) || ! getterOrOptions.set )as any
}
Copy the code
In computed API, the first thing you do is determine whether the parameter passed is a getter function or an options object. If it is a function, the function must be a getter. Accessing the setter in the DEV environment will not succeed, and a warning will be issued. If the pass is not a function, computed treats it as an object with get and set attributes, assigning the get and set values to the corresponding getters and setters. Finally, after processing is complete, an instance object of the ComputedRefImpl class is returned, and computed API is done.
ComputedRefImpl class
This Class is similar to the RefImpl Class we introduced earlier, but the logic in the constructor is a little different.
First look at the member variables in the class:
class ComputedRefImpl<T> {
private_value! : Tprivate _dirty = true
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true;
public readonly [ReactiveFlags.IS_READONLY]: boolean
}
Copy the code
Compared to the RefImpl class, we added the _dirty private member variable, an effect read-only side effect function variable, and a __v_isReadonly flag.
Now look at the logic in the constructor:
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
this.effect = effect(getter, {
lazy: true.scheduler: () = > {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value')}}})this[ReactiveFlags.IS_READONLY] = isReadonly
}
Copy the code
In the constructor, a side effect function is created for the getter, set to delay execution in the side effects option, and a scheduler is added. The scheduler determines whether the this._dirty flag is false, and if so, sets this._dirty to true and dispatches updates using trigger. If you are confused about the timing of this side effect and when the scheduler executes it, you are advised to read the previous article to understand the effect side effect first and then to understand the other responsive apis.
get value() {
// This computed ref might be wrapped by other proxy objects
const self = toRaw(this)
if (self._dirty) {
// The getter executes the side effect function and dispatches updates that update dependent values
self._value = this.effect()
self._dirty = false
}
// Call track to collect dependencies
track(self, TrackOpTypes.GET, 'value')
// Returns the latest value
return self._value
}
set value(newValue: T) {
// Execute setter functions
this._setter(newValue)
}
Copy the code
In computed, when a value is obtained through a getter function, the side effect function is executed first, and the return value of the side effect function is assigned to _value and the value of _dirty is assigned to false. This ensures that if the dependencies in computed do not change, Therefore, the _dirty obtained in the getter is always false, and the side effect function does not need to be executed again, saving costs. Then track collects the dependencies and returns the value of _value.
In the setter, we just execute the setter logic that we passed in, and so far we’ve explained the implementation of computed API.
conclusion
In this article, we show you the implementation of ref and computed, two of the most commonly used apis in Vue3 response, based on the side effect functions described in the previous article and the dependency on collection and distribution of updates. Both apis return a class instance when created. The constructor in the instance and the GET and set on the value property do reactive tracing.
When we learn to use these at the same time, and can know how it will help us when using these apis play its biggest role, at the same time also can let you to write some code does not meet your expectations, rapid positioning problem, can do is write their own wrong, or API itself doesn’t support some call way.
If this article helps you understand the implementation of reactive API REF and computed in Vue3, I hope it gives you a thumbs up at ❤️. If you want to continue to follow the following articles, you can also follow my account, thank you again for reading so far.