Welcome to CoderStan’s handwritten Mini-Vue3 column and join me in writing your own Mini-Vue3. Ref and computed in the ReActivity module will be implemented simply in this chapter. (Thanks to Atri CXR mini-Vue)
If you feel good, please support it. If you want to see other parts of the article, you can pay attention to me or pay attention to my handwritten mini-vue3 column. If you want to see the annotated source code, welcome to visit GitHub warehouse, and please also point a star to support it.
3. Realize the reactivity
3.11 implementationref
Check the responsiveness API section of the Vue3 API documentation for an introduction to REF.
Takes an internal value and returns a reactive and mutable REF object. The ref object has a single property.value pointing to an internal value.
Example:
const count = ref(0) console.log(count.value) / / 0 count.value++ console.log(count.value) / / 1 Copy the code
If you assign an object to a REF value, you make it highly responsive by using reactive functions.
Type declaration:
interface Ref<T> { value: T } function ref<T> (value: T) :Ref<T> Copy the code
Sometimes we may need to specify complex types for the inner values of ref. To do this concisely, we can pass a generic parameter when calling ref to override the default inference:
const foo = ref<string | number> ('foo') / / foo types: Ref < string | number > foo.value = 123 // ok! Copy the code
If the type of the generic is unknown, it is recommended to convert ref
to ref
:
function useState<State extends string> (initial: State) { const state = ref(initial) as Ref<State> // state.value -> State extends string return state } Copy the code
① Implement the most basicref
To implement ref, first create the ref test file ref.spec.ts in the SRC /reactivity/__tests__ directory and add the following test code:
describe('reactivity/ref'.() = > {
it('should hold a value'.() = > {
// Create the ref object
const a = ref(1)
// The value property of the ref object is equal to the value passed in
expect(a.value).toBe(1)
// The value property of the ref object is mutable
a.value = 2
expect(a.value).toBe(2)
})
it('should be reactive'.() = > {
const a = ref(1)
let dummy
let calls = 0
effect(() = > {
calls++
dummy = a.value
})
expect(calls).toBe(1)
expect(dummy).toBe(1)
// ref objects are reactive
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)
// The set of the value property of the ref object is cached and does not trigger dependencies repeatedly
a.value = 2
expect(calls).toBe(2)
expect(dummy).toBe(2)})})Copy the code
To pass the above test, create a ref.ts file in the SRC /reactivity/ SRC directory, implement an incomplete ref and export it. During the implementation process, use the ref interface implementation class to encapsulate the operation:
// Interface of the ref object
interface Ref {
value
}
// The implementation class of the Ref interface that encapsulates the operation
class RefImpl {
private _value
constructor(value) {
// Assign the value passed to the instance's private property _value
this._value = value
}
// Get of value property returns the value of the private property _value
get value() {
// TODO collects dependencies
// Returns the value of the instance's private property _value
return this._value
}
// Set of value property modifies the private property _value value
set value(newVal) {
// TODO triggers dependencies
// Process the value of set and assign the result to the instance's private property _value
this._value = newVal
}
}
export function ref(value) :Ref {
// Return an instance of the RefImpl class, the ref object
return new RefImpl(value)
}
Copy the code
This enables an incomplete REF, that is, the ability to convert the passed value into a REF object. Then extract the track and trigger functions from the effect.ts file in the SRC /reactivity/ SRC directory and export isTracking, trackEffects, and triggerEffects functions:
export function track(target, key) {
// Return the dependency if it should not be collected
if(! isTracking()) {return
}
/* Other code */
trackEffects(dep)
}
// Used to determine whether dependencies should be collected
export function isTracking() {
returnshouldTrack && activeEffect ! = =undefined
}
// Used to add the currently executing instance of the ReactiveEffect class to the DEP and deP to the dePS property of the currently executing instance of the ReactiveEffect class
export function trackEffects(dep) {
if(dep.has(activeEffect!) ) {return} dep.add(activeEffect!) activeEffect? .deps.push(dep) }export function trigger(target, key) {
/* Other code */
triggerEffects(dep)
}
// A scheduler or run method that iterates through the DEP, calling each instance of the ReactiveEffect class
export function triggerEffects(dep) {
for (const reactiveEffect of dep) {
if (reactiveEffect.scheduler) {
reactiveEffect.scheduler()
} else {
reactiveEffect.run()
}
}
}
Copy the code
After that, create a private property DEP in the RefImpl class to hold the dependencies associated with the current ref object, collect the dependencies in the get of the value property, and trigger the dependencies in the set:
class RefImpl {
private _value
// Used to store dependencies associated with the current ref object
private dep
constructor(value) {
this._value = value
this.dep = new Set()}get value() {
if (isTracking()) {
// Collect dependencies
trackEffects(this.dep)
}
return this._value
}
set value(newVal) {
// If the value of set is the same as before, it is returned directly
if(! hasChanged(newVal,this._value)) {
return
}
this._value = newVal
// Trigger dependencies
triggerEffects(this.dep)
}
}
Copy the code
Run the yarn test ref command to run the test of ref. You can see that the test passes. This completes the basic implementation of ref.
(2) perfectref
If the value passed in is an object, you need to use Reactive to transform that object.
Add the following test code to ref’s test file ref.spec.ts:
describe('reactivity/ref'.() = > {
/* Other test code */
it('should make nested properties reactive'.() = > {
const a = ref({
count: 1
})
let dummy
effect(() = > {
dummy = a.value.count
})
expect(dummy).toBe(1)
// The value property of the ref object is a responsive object
a.value.count = 2
expect(dummy).toBe(2)})})Copy the code
To pass the above tests, the ref implementation needs to be refined. First, implement and export the toReactive function in the reactivity. ts file in the SRC /reactivity/ SRC directory:
// It is used to process values, and reactive is used to process objects, otherwise it is returned directly
export const toReactive = value= > (isObject(value) ? reactive(value) : value)
Copy the code
Then add a private property _rawValue to the RefImpl class to hold the value passed in and the value of the set, and use toReactive to process the value before assigning it to the instance’s private property _value:
class RefImpl {
// Used to hold the value passed in and the value of set
private _rawValue
private _value
private dep
constructor(value) {
// Assign the value passed to the instance's private property _rawValue
this._rawValue = value
// Process the value passed in and assign the result to the instance's private property _value
this._value = toReactive(value)
this.dep = new Set()}get value() {
if (isTracking()) {
trackEffects(this.dep)
}
return this._value
}
set value(newVal) {
// If the value of set is different from the previous value, modify and trigger the dependency
if (hasChanged(newVal, this._rawValue)) {
// Assign the value of set to the instance's private property _rawValue
this._rawValue = newVal
// Process the value of set and assign the result to the instance's private property _value
this._value = toReactive(newVal)
// Trigger dependencies
triggerEffects(this.dep)
}
}
}
Copy the code
Run the yarn test ref command to run the test of ref and see that the test passes, which further improves the implementation of ref.
3.12 implementationisRef
andunRef
Check the responsiveness API section of the Vue3 API documentation for descriptions of isRef and unRef.
isRef
Check if the value is a ref object.
unref
If the argument is a ref, the internal value is returned, otherwise the argument itself is returned. This is val = isRef(val), right? Val. value: The syntactic sugar function of val.
function useFoo(x: number | Ref<number>) { const unwrapped = unref(x) // Unwrapped must now be a numeric type } Copy the code
Before implementing isRef and unRef, first add isRef and unRef test code to ref. Spec. ts test file:
describe('reactivity/ref'.() = > {
it('isRef'.() = > {
expect(isRef(ref(1))).toBe(true)
expect(isRef(reactive({ foo: 1 }))).toBe(false)
expect(isRef(0)).toBe(false)
expect(isRef({ bar: 0 })).toBe(false)
})
it('unref'.() = > {
expect(unref(1)).toBe(1)
expect(unref(ref(1))).toBe(1)})})Copy the code
To pass the above test, add a common property __v_isRef to the RefImpl class to indicate that the instance is a REF object, and then implement isRef and unRef in the RefImpl /reactivity/ SRC file refref.
class RefImpl {
// Used to hold the value passed in and the value of set
private _rawValue
private _value
// Used to store dependencies associated with the current ref object
private dep
// Is used to indicate that the instance is a ref object
public __v_isRef = true
}
// Determine if a value is a ref object
export function isRef(value) :boolean {
return!!!!! value.__v_isRef }// Get the value of the ref object's value property
export function unref(ref) {
return isRef(ref) ? ref.value : ref
}
Copy the code
Run the yarn test ref command to run the test of ref and see that the test passes. In this way, isRef and unRef are implemented.
3.13 implementationproxyRefs
function
The proxyRefs function takes an object as an argument and returns an instance of Proxy that proxies the get and set of the object. If a property of the object is a ref object, You can directly obtain the incoming value of the REF object by obtaining the corresponding property value of proxy, and directly modify the corresponding property value of proxy to modify the incoming value of the REF object or replace the REF object.
Ref = ref. Spec. ts proxyRefs = ref.
describe('reactivity/ref'.() = > {
it('proxyRefs'.() = > {
const obj = {
foo: ref(1),
bar: 'baz'
}
const proxyObj = proxyRefs(obj)
expect(proxyObj.foo).toBe(1)
expect(proxyObj.bar).toBe('baz')
proxyObj.foo = 2
expect(proxyObj.foo).toBe(2)
proxyObj.foo = ref(3)
expect(proxyObj.foo).toBe(3)})})Copy the code
To pass the above test, implement and export the proxyRefs function in the ref.ts file under the SRC /reactivity/ SRC directory.
export function proxyRefs(objectWithRefs) {
// Return the instance of Proxy
return new Proxy(objectWithRefs, {
// Proxies get and set of the property of the passed object
get: function (target, key) {
// Get the property value of the passed object and call unref to process it
return unref(Reflect.get(target, key))
},
set: function (target, key, value) {
const oldValue = target[key]
// If the property of the object passed in is a ref object and the value of set is not a ref object, change the value of the ref object, otherwise change the value of the property directly
if(isRef(oldValue) && ! isRef(value)) { oldValue.value = valuereturn true
} else {
return Reflect.set(target, key, value)
}
}
})
}
Copy the code
Run the yarn test ref command to test ref and see that the test passes.
3.14 implementationcomputed
Check the responsive API section of the Vue3 API documentation for an introduction to computed.
Takes a getter function and returns an immutable reactive ref object based on the return value of the getter.
const count = ref(1) const plusOne = computed(() = > count.value + 1) console.log(plusOne.value) / / 2 plusOne.value++ / / error Copy the code
Alternatively, you can take an object with get and set functions to create a writable ref object.
const count = ref(1) const plusOne = computed({ get: () = > count.value + 1.set: val= > { count.value = val - 1 } }) plusOne.value = 1 console.log(count.value) / / 0 Copy the code
Type declaration:
/ / read-only function computed<T> (getter: () => T, debuggerOptions? : DebuggerOptions) :Readonly<Ref<Readonly<T>>> // writablefunction computed<T> ( options: { get: () => T set: (value: T) => void}, debuggerOptions? : DebuggerOptions) :Ref<T> interface DebuggerOptions { onTrack? : (event: DebuggerEvent) = >void onTrigger? : (event: DebuggerEvent) = >void } interface DebuggerEvent { effect: ReactiveEffect target: any type: OperationTypes key: string | symbol | undefined } Copy the code
① Implement the most basiccomputed
Before implementing computed, create a test file for computed, computed.spec.ts, in the SRC /reactivity/__tests__ directory, and add the following test code:
describe('reactivity/computed'.() = > {
it('should return updated value'.() = > {
const value = reactive({ foo: 1 })
// Take a getter to create a read-only responsive ref object,
const cValue = computed(() = > value.foo)
expect(cValue.value).toBe(1)
value.foo = 2
expect(cValue.value).toBe(2)})})Copy the code
To pass the above test, create a computed. Ts file in the SRC /reactivity/ SRC directory, implement and export a basic computed object, and use the implementation class of the Ref interface to encapsulate the operation during implementation. At the same time, the ReactiveEffect class removed from the implementation of Effect is used, so we need to export the ReactiveEffect class in the effect.ts file under the SRC /reactivity/ SRC directory:
// effect.ts
export class ReactiveEffect {
/* Implement */
}
// computed.ts
// The implementation class of the Ref interface
class ComputedImpl {
// Save an instance of the ReactiveEffect class
private _effect: ReactiveEffect
constructor(getter) {
Create an instance of the ReactiveEffect class using the getter function
this._effect = new ReactiveEffect(getter)
}
// Get of value property returns the return value of the run method that called the private property _effect, that is, the return value of the getter function
get value() {
return this._effect.run()
}
}
export function computed(getter) {
// Return an instance of the RefImpl class, the ref object
return new ComputedImpl(getter)
}
Copy the code
Run the YARN test computed command to run a test for computed, and you can see that the test passes, thus completing the most basic implementation of computed.
(2) perfectcomputed
Computed lazily executes getters, and the get of the value property of a responsive ref object is cached.
Add the following test code in computed’s test file, comput.spec.ts:
describe('reactivity/computed'.() = > {
it('should compute lazily'.() = > {
const value = reactive({ foo: 1 })
const getter = jest.fn(() = > value.foo)
const cValue = computed(getter)
// The getter is executed when the value of the ref object's value property is obtained
expect(getter).not.toHaveBeenCalled()
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(1)
// If the property value of the dependent responsive object is not updated, then fetching the value of the ref object's value property again does not repeat the getter
cValue.value
expect(getter).toHaveBeenCalledTimes(1)
Getters are not executed when modifying the property value of a dependent reactive object
value.foo = 1
expect(getter).toHaveBeenCalledTimes(1)
/ / in response to rely on the property of the objects of type values are updated, for ref value the value of the property of the object again getter
expect(cValue.value).toBe(1)
expect(getter).toHaveBeenCalledTimes(2)
cValue.value
expect(getter).toHaveBeenCalledTimes(2)})})Copy the code
To pass these tests, the implementation of computed needs to be improved.
class ComputedImpl {
private _effect: ReactiveEffect
// Save the result of the getter function
private _value
// This is used to record whether caching is not used
private _dirty = true
constructor(getter) {
// Create an instance of the ReactiveEffect class using a getter function and a method
this._effect = new ReactiveEffect(
getter,
// To turn off the cache
() = > {
this._dirty = true})}// Get of value property returns the return value of the run method that called the private property _effect, that is, the return value of the getter function
get value() {
if (this._dirty) {
// Call the run method of an instance of the ReactiveEffect class, that is, execute the getter function and assign the result to the _value property
this._value = this._effect.run()
this._dirty = false
}
return this._value
}
}
Copy the code
Execute the YARN test computed command to run a test for computed and see that the test passes, further perfecting the implementation of computed.