Write small mentioned before
About my front-end work experience: NOW I am a senior in college. In my junior year, I worked as an intern for about four months and participated in the defense of becoming a full-time employee. In autumn, I participated in interviews with many companies and landed successfully. In preparing for the interview, Vue3 responsiveness is the focus of the framework. With the help of online article tutorials and source code learning, and a simple Vue3 Reactivity, only to achieve the main function, finally can also cope with the interview (in fact, the interview asked not much X). But always feel that their grasp is not good, scattered knowledge, easy to forget. After thinking about the reason, when I read this piece of code, I mostly read other people’s articles. I didn’t take the initiative to learn, and I didn’t think about why the code would be implemented like this. The Feynman method of learning has the highest retention when you teach someone else.
Now that it’s 2022, Vue3 Reactivity and even Vue3 source code parsing are numerous articles and courses. Recently, Vue3 has also been renamed to Core, and Vue3 will become the default. Now their own Vue3 Reactivity source code analysis article seems a bit outdated.
But in line with the principle of self-interest and altruism, or want to write this article, in the process of writing their own articles, on the “output force input” also have deep experience. I hope my first external article can be a good start for my output plan.
An overview
Vue3 looks large because it is designed to be stable and functional in real business applications, while adding debug code that is easy to develop. Implementing a simple Vue3 Reactivity is pretty simple, if you don’t consider different scenarios and boundary conditions. Uvu also has a video of himself implementing mini-Vue, you can check it out if you are interested. Vue Mastery – Vue Mastery bilibili
Vue3 has three core modules: ReActivity Module, Render Module, and Compiler Module. Vue3 uses Monorepo for code organization and management, reactivity as a separate module, which helps us to understand the implementation of reactivity more centrally, regardless of runtime and render.
Reactive really emphasizes the idea of automatic renewal, keeping things in sync.
For example, if we declare an object target in our code, when the target changes, a responsive system can automatically do something like upload a change log to keep the data and log synchronized. In a Vue3 responsive system it would be written like this:
import {reactive , effect} from 'vue';
const target = { info : 'vue3'}
const state = reactive(target)
effect(() = > {
console.log('info changed', state.info) // Simulate the upload change log function
})
state.info = 'core'
Copy the code
In Vue3 responsive system, the original object (target) is converted into responsive object (State), and then the responsive object is operated. We pass to Effect functions that keep operations in sync, such as uploading change logs, rendering pages, etc., so that Effect knows which values the function uses and can execute the function when the value changes. We call effect(FN) the side effect function.
In JavaScript, the operation is intercepted by Proxy and the side effect function is executed. At the same time Reflect to complete the default behavior, the original object normal changes, is the basis of the implementation of responsive.
To implement this feature, you need to do two things:
- Trace and observe responsive objects
- Reactive object changes, performing side effect functions
The whole process is as follows: 1. Initialize the reactive object – 2. Execute the side effect function, read the reactive object, and establish the dependency between the reactive object and the side effect function – 3. A responsive object changes and executes the corresponding side effect function. And then 2 and 3.
Now let’s see how this is implemented in code. Okay
reactive
First, let’s look at the implementation of Reactive, which completes the first step: initializing the reactive object.
By the way, readOnly, shallowReactive, shallowReadonly. Readonly converts an object/reactive object /ref to a read-only object that cannot be changed. Because properties of objects can be nested, proxy operations by Readonly and Reactive are deep. Shallow: shallowReadonly and shallowReactive correspond to read-only and reactive proxies that are only effective for the first layer properties of an object.
Above, we found that there are multiple types of objects in a responsive system, which can be distinguished by the following identifiers
exportinterface Target { [ReactiveFlags.SKIP]? : boolean// If true, reactive operations can be skipped[ReactiveFlags.IS_REACTIVE]? : boolean// is true as a responsive object[ReactiveFlags.IS_READONLY]? : boolean// Is a true read-only object[ReactiveFlags.RAW]? : any// Is true for the original object
}
Copy the code
Reactive (readonlyObj) has a judgment that does not allow reactive(readonlyObj) behavior.
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
Copy the code
The core of Reactive is to obtain a Proxy object of target, and the handler object passed in during Proxy instantiation can intercept the operation, and combine with Effect to achieve responsiveness.
function createReactiveObject(target, isReadOnly, baseHandlers, collectionHandlers, proxyMap){
// Only objects are accepted
if(! isObject(target)){return target
}
// Readonly (reactive) is enabled if target is already a Proxy.
if( target[ReactiveFlags.RAW] && ! (isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) {return target
}
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// instantiate the Proxy
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
Copy the code
Handler is the entry point to implement interception. BaseHandlers and collectionHandlers are written to distinguish between different types of objects and are divided into two main categories, COMMON and COLLECTION. When the Proxy is instantiated, the type of target is determined and different handlers are passed in. We can just focus on baseHandlers.
function targetTypeMap(rawType: string) {
switch (rawType) {
case 'Object':
case 'Array':
return TargetType.COMMON
case 'Map':
case 'Set':
case 'WeakMap':
case 'WeakSet':
return TargetType.COLLECTION
default:
return TargetType.INVALID
}
}
Copy the code
baseHandlers
And then baseHandlers.
The responsive-type object mainly uses five interceptors for target agent: GET, set, deleteProperty, HAS and ownKey, covering common scenarios of reading and modifying object property values. Get and set
get
get -> createGetter
function createGetter(isReadonly = false, shallow = false) {
return function get(target, key, receiver) {
A / / details
if (key === ReactiveFlags.IS_REACTIVE) {
return! isReadonly }else if (key === ReactiveFlags.IS_READONLY) {
return isReadonly
} else if (key === ReactiveFlags.IS_SHALLOW) {
return shallow
} else if (
key === ReactiveFlags.RAW &&
receiver ===
(isReadonly
? shallow
? shallowReadonlyMap
: readonlyMap
: shallow
? shallowReactiveMap
: reactiveMap
).get(target)
) {
return target
}
const targetIsArray = isArray(target)
// Array method interception
if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
}
// reflect. get completes the default operation and gets the result
const res = Reflect.get(target, key, receiver)
// If it is not readonly, track
if(! isReadonly) { track(target, TrackOpTypes.GET, key) }if (shallow) {
return res
}
//
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
constshouldUnwrap = ! targetIsArray || ! isIntegerKey(key)console.log('ref',res,shouldUnwrap, ! targetIsArray ,! isIntegerKey(key))return shouldUnwrap ? res.value : res
}
// Detail 3: recursive response
if (isObject(res)) {
// Convert returned value into a proxy as well. we do the isObject check
// here to avoid invalid value warning. Also need to lazy access readonly
// and reactive here to avoid circular dependency.
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
Copy the code
Detail 1: To implement isReadonly, isReactive, and isProxy, read value [reactiveFlags.is_reactive], Value [reactiveFlags. IS_REACTIVE], value [reactiveFlags. RAW] The get intercept operation is performed and the result is returned using isReadonly. This does not add additional attributes to the reactive object, which is very clever.
Detail 2: If the value retrieved is ref, return ref.value directly. You can see this issue for specific reasons.
Detail 3: If the property value is an object, continue to readonly or reactive the fetched object. Different from Vue2, Vue2 performs recursive tracking on the object at the very beginning. Here, there is a get attribute value, that is, the attribute value is accessed, and then the proxy operation is performed on the result of the attribute value. Without the attribute value of Access, there is no need to perform proxy operation on the result, so the performance is better. Note, too, that if it is shallow, it returns directly after the first attribute value is fetched, without brokering the deep attribute.
The specific implementation of track function, we will leave in detail later. All we need to know for now is that when we execute the side effect function, we get the responsive object, trigger the GET intercept, and execute the track function. Within the track function, you can establish dependencies between reactive objects and side effects functions.
set
Set -> CreateSetter
function createSetter(shallow = false) {
return function set(target, key, value, receiver){
let oldValue = target[key]
if(isReadonly(oldValue) && isRef(oldValue) && ! isRef(value)) {return false
}
// If the key is greater than array length, it is added
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
if (target === toRaw(receiver)) {
if(! hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) }else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
Copy the code
The trigger function is executed when we modify the reactive object by firing the set operation. Note that there are multiple trigger action types: set, add,clear,delete; Corresponding to attribute change, add, clear and delete four operations.
The trigger function will be explained later, but for now we just need to know that in the trigger function, the side effects collected during track will be executed.
effect
Let’s look at Effect. Effect (fn, options) takes the function fn and options objects as arguments. Options can also be used as configuration information.
Without passing options, Effect will execute FN immediately to read the responsive object and trigger get interception operation on the responsive object, so as to execute track function and establish dependency relationship.
Put it into the source code:
export function effect(fn,options){
if (fn.effect) {
fn = fn.effect.fn
}
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
if(! options || ! options.lazy) { _effect.run() }const runner = _effect.run.bind(_effect)
runner.effect = _effect
return runner
}
Copy the code
export class ReactiveEffect {
active = true
deps = [] // effect dependency array
constructor(fn,scheduler,scope) {
recordEffectScope(this, scope)
}
run() {
/ * * * /
}
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false}}}Copy the code
In Effect, fn is converted into a stateful ReactiveEffect instance through the ReactiveEffect class, and two methods run and stop are implemented.
With no options or options.lazy set to false, execute _effect.run, set activeEffect = this, execute fn.
Let’s look at the run method on ReactiveEffect:
The following run method is a version prior to 3.2 optimization, leaving out the bit optimization and making it easier to understand.
run() {
if (!this.active) {
return this.fn()
}
if(! effectStack.includes(this)) {
activeEffect = this
try {
effectStack.push(this)
cleanupEffect(this)
return this.fn()
} finally {
effectStack.pop()
const n = effectStack.length
activeEffect = n > 0 ? effectStack[n-1] :undefined}}}Copy the code
As you can see, after executing the run method, you set the current instance ReactiveEffect to activeEffect, add it to the effectStack, execute the FN method, and finally remove the effectStack to update the activeEffect.
Why set the global variable activeEffect?
After setting the global variable activeEffect, fn will be executed immediately. In FN, the key-value of the responsive object property will be obtained, and the intercept operation of GET will be triggered, and the track function will be executed.
track
Let’s see what the track function does.
ecport function track(){
if(! isTracking()) {return
}
let depsMap = targetMap.get(target)
if(! depsMap) { targetMap.set(target, (depsMap =new Map()))}let dep = depsMap.get(key)
if(! dep) { depsMap.set(key, (dep = createDep())) } trackEffects(dep) }Copy the code
In the track function, isTracking is evaluated first, and if not, it returns directly. Then there are two Map structures, targetMap and depsMap. We use a diagram to show the relationship between them:
The DEP for the key is obtained via depsMap, and then the trackEffects function puts activeeffects into the DEP, so that the key can be mapped to the FN as a dependency.
So, if we do not set activeEffect, we cannot know which effect the current key corresponds to.
And now we’re done with step two. Now let’s look at the third step, reactive object change, and execute the corresponding side effect function.
trigger
State.info = ‘core’ triggers the set intercept to execute the trigger function.
In the trigger function, you can see that deps are operated on differently depending on TriggerOpTypes, and finally the side effects function is executed uniformly through triggerEffects.
export function trigger(
target,type,key,newValue) {
const depsMap = targetMap.get(target)
if(! depsMap) {return
}
let deps = []
if (type === TriggerOpTypes.CLEAR) {
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) = > {
if (key === 'length' || key >= newValue) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if(key ! = =void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if(! isArray(target)) { deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))}break
case TriggerOpTypes.DELETE:
if(! isArray(target)) { deps.push(depsMap.get(ITERATE_KEY))if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break}}if (deps.length === 1) {
if (deps[0]) {
triggerEffects(deps[0])}}else {
const effects = []
for (const dep of deps) {
if(dep) { effects.push(... dep) } } } }Copy the code
Executing the side effect function triggers track, re-establishing the dependency, and so on.
Why cleanup the dependencies of the side effect function, known as cleanupEffect, before each execution of the side effect function?
For example, in this scenario:
const ok = reactive({value:true})
const msg = reactive({value: 'vue3'})
effect(() = > {
if(ok.value){
console.log('msg value', msg.value)
}else{
console.log('false branch')}})Copy the code
The first time effect is executed, both OK and MSG will be dependent on this effect, and the value of MSG will be printed.
Then change ok.value = false, and executing trigger will execute effect.run again, which clears the effect dependencies. After fn is executed, Effect only collects OK as a dependency. MSG is no longer a dependency on the side effect function. If msg. value is changed, no effect will be executed. However, if not cleared, MSG will still be dependent on effect. If MSG changes, effect will still be executed, resulting in false brach minutes, which is not expected. So before we execute the side effect function the second time, we need to cleanup the dependencies with cleanupEffect.
About the implementation of Cleanupffect
function cleanupEffect(effect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0}}Copy the code
We can recall that when ReactiveEffect is instantiated, a DEP array is initialized. When track is instantiated, a deP dependency between key and effect is established. This dependency is bidirectional. We also push dePs into the DEPS array of ReactiveEffects. In this way, the DEP relationship established by last track can be cleared in cleanupEffect.
However, inconsistent dependence of effect is still rare, so Vue3.2 is optimized with bit arithmetic.
Why do you need a global variable effectStatck and design it as a stack?
The main consideration is for effect nesting, such as the following scenario:
const counter = reactive({
num1: 0.num2: 0
})
function logCount1() {
effect(logCount2)
console.log('num1:', counter.num1)
}
function logCount2() {
console.log('num2:', counter.num2)
}
effect(logCount)
counter.num++
Copy the code
If we only assign ReactiveEffect to activeEffect every time we execute effect, then for this nested scenario, after effect(logCount2) is executed, ActiveEffect is still effect(logCount2), so num1 relies on collecting the corresponding effect(logCount2) when accessing counter. Num. But the effect for num1 is actually effect(logCount1). Num1 is not logCount1, but logCount2. The final output is as follows, which is not as expected.
// bad
num2: 0
num: 0
num2: 0
// expected
num2: 0
num: 0
num2: 0
num: 1
Copy the code
Therefore, for nested effect scenarios, we can not simply assign an activeEffect value. We should consider that the execution of the function itself is a stack operation, so we can also design an effectStack, so that every time the reactiveEffect function is entered, it is pushed first. ActiveEffect points to the reactiveEffect function, exits the stack after fn completes, and points activeEffect to the last element in the effectStack. This is the reactiveEffect corresponding to the outer effect function.
The example reference: pull hook education – Huang Yi-vue. Js 3.0 core source code analysis
But in the latest Vue, the stack structure has also been optimized, if you are interested, you can check it out.
Array processing
Let’s take a look at the process of using a Proxy to process arrays and what the problems are.
First, let’s take a look at a couple of demos and observe the print results
const raw = [1.2.3.4]
const result = new Proxy(raw, {
get(target, key) {
console.log('get',key, Reflect.get(target, key))
return Reflect.get(target, key)
},
set(target, key,value) {
console.log('set', key, value)
return Reflect.set(target, key, value)
}
})
result[0]
// Print the result
// get 0
result.push(5)
// Print the result
// get push f push {}
// get length 4
// set 4 5
// set length 4
Copy the code
We can see that when we access an array by subscript, key is the array subscript; When we change the subscript by push, there are two get and two set.
So let’s put it in the reactive, ignore the current code for now
if(! isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {return Reflect.get(arrayInstrumentations, key, receiver)
}
Copy the code
const raw = [1]
const result = reactive(raw)
effect(() = >{
result.forEach((i) = >{
console.log('Pretend the render function triggers an array collection dependency',i)
})
})
result.push(2)
Copy the code
When effect is executed for the first time, three gets are actually triggered, with the key forEach, Length, and subscript 0. Same thing with push.
So our final depsMap collection looks like this:
When we want to push, in fact, we cannot track in the first two get of push because the activeEffect is undefined. Then there are two sets, and we find in set 1 that key is the new type identifier passed to trigger Add.
// If the key is greater than length, the value is new
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
Copy the code
Next comes the logic of trigger. When you enter the branch of Add, the new index does not have a depsMap and therefore does not have a DEP. Therefore, the effect corresponding to length will be used, which is the one obtained by track in forEach just now. Therefore, when set resulu[1] = 2, effect will be re-executed. In this way, the array can sense changes and realize responsiveness by using the method of push.
And you might wonder why you’re using the side effect function for length in the case of array Add. The array methods push, POP, shift, and unshift are all methods that change the array length and affect the result of the traversal function. The forEach, map, and for looping functions all get Length and collect dependencies. So we use the length key as a bridge to achieve synchronization.
Normally, we would iterate over several sets of rendered pages.
Let’s talk about why we have the code we just ignored.
If the array is not read-only, two array methods are intercepted: includes, indexOf, and lastIndexOf, and push, POP, Shift, unshift, and splice.
Let’s start with the second type of array methods. Why do you want to intercept these array methods when you can do reactive,
The answer can be found in this issue. When debugging the Vue source code, we can first comment out the code intercepted by the array method to interrupt the observation result.
Such a scenario is shown to us in issue
const arr = reactive([])
watchEffect(() = >{
arr.push(1)
})
watchEffect(() = >{
arr.push(2)})Copy the code
We can see that if there are two effects, pushing or changing the length of the responsive array will cause an infinite loop between the two effects.
Let’s dig a little deeper. Since watchEffect is in Runtime-core and not in reactivity, I’ll replace it with Effect. WatchEffect also loops indefinitely because it involves the scheduling of effect, and while the replacement does not loop indefinitely, you can still see confusing results.
const arr = reactive([])
// effect1
effect(() = >{
arr.push(1)})// effect2
effect(() = >{
arr.push(2)})Copy the code
Get push collects effect1 and Get Length collects Effect1. Set arr[0] = 1 (activeEffect === effect1) Set length is not executed because hasChanged(value, oldValue) === false.
When effect2 is executed, there are two sets of get and set. Get push collects effect2 and Get Length collects effect2. Lengh has effect1 and effect2. When set arr[1] = 2, the effct corresponding to Length is added, and effect1 is executed again when the effect array is traversed.
Similarly, when effect1 is executed, effect2…… is executed again
So we can see that the root cause is because when you push, you get lengh, you collect dependencies. The use of issue itself is problematic. The effect rule is that the effect side function cannot change the dependent value.
Let’s look at a concrete implementation of the interception method:
; (['push'.'pop'.'shift'.'unshift'.'splice'] as const).forEach(key= > {
instrumentations[key] = function (this: unknown[], ... args: unknown[]) {
pauseTracking()
const res = (toRaw(this) as any)[key].apply(this, args)
resetTracking()
return res
}
})
Copy the code
The key is pauseTracking and resetTracking. If shouldTrack is set to false in pauseTracking, it will not be dependent on length track.
Therefore, when set, the effect of push method is not triggered. After push, resetTracking is set to shouldTrack to true
Since the includes, indexOf, and lastIndexOf methods accept index values as arguments, if they are executed in an Effect wrapper function, they need to be executed again when any index value or length changes. Therefore, by using the for loop +track, Effect can track changes based on index values and length.
; (['includes'.'indexOf'.'lastIndexOf'] as const).forEach(key= > {
instrumentations[key] = function (this: unknown[], ... args: unknown[]) {
const arr = toRaw(this) as any
for (let i = 0, l = this.length; i < l; i++) {
track(arr, TrackOpTypes.GET, i + ' ')}// we run the method using the original args first (which may be reactive)
constres = arr[key](... args)if (res === -1 || res === false) {
// if that didn't work, run it again using raw values.
returnarr[key](... args.map(toRaw)) }else {
return res
}
}
})
Copy the code
ref
Ref (target) takes an internal value and returns a responsive and mutable REF object. Refs are often used to convert primitives such as string and number. Reactive can only be used to convert objects, but converting primitives to objects can be stiff and cumbersome. Ref solves this problem.
Take a look at the internal implementation of ref:
class RefImpl{
_value = undefined
_rawValue = undefined
dep= undefined
__v_isRef = true
constructor(value,__v_isShallow) {
this._rawValue = __v_isShallow ? value : toRaw(value)
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
trackRefValue(this)
return this._value
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = this.__v_isShallow ? newVal : toReactive(newVal)
triggerRefValue(this, newVal)
}
}
}
Copy the code
Proxy can only replace objects, so ref no longer uses Proxy. Instead, it selects value as the default property and combines getter and setter accessor properties to complete track and trigger operations.
The dependency between REF and reactiveEffect is directly mounted on the DEP attribute of the REF instance, without constructing a Map separately.
computed
Computed attributes are used when calculations depend on other states. It takes a getter and returns an immutable reactive REF object for the value returned by the getter, or it can use an object with get and set functions to create a writable REF object.
First, a quick look at its usage:
The first scenario:
const count = ref(1)
const plusOne = computed(() = > count.value + 1)
conut.value++
count.value++
count.value++
console.log('plusOne.value', plusOne.value) // 5 (prints only once)
plusOne.value++ // error: This is because the set function was not passed in, so it cannot be changed directly
Copy the code
Count. Value changed three times, and plusone. value printed the last result 5
The second scenario:
const count = ref(1)
// plusOne -> computedRef
const plusOne = computed(() = > count.value + 1) // computed(getter)
// computedEffect
effect(() = >{
console.log('count value', plusOne.value)
})
conut.value++
count.value++
Copy the code
The first time effect is executed, it prints pluone. value with a value of 2. When the value of count changes, the effect associated with plusone. value is also executed and plusone. value is printed with a value of 3. When the value of count changes again, the effect associated with plusOne is also executed and plusone. value is printed with a value of 4.
We can see that computed also returns a REF. When we continuously change the value of count, the getter function is not continuously executed. The only time plusOne is accessed through the value property is when the getter function is executed to calculate the correct value. But we saw the second case where, since plusOne relies on count, changing the value of count triggers the pluOne side effect function.
So we can guess how computed works: Computed getter functions are executed only when accessed through the value property, but computed depends on value changes that trigger computed side effects functions, such as plusOne’s side effects function when count changes here.
Take a look at the implementation inside the source code
export class ComputedRefImpl{
dep = undefined
_value = undefined
effect = undefined
__v_isRef = true
_dirty = true
constructor(getter) {
this.effect = new ReactiveEffect(getter, () = > {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)}})this.effect.computed = this
}
get value() {
const self = toRaw(this)
trackRefValue(self)
if(self._dirty || ! self._cacheable) { self._dirty =false
self._value = self.effect.run()
}
return self._value
}
set value(newValue) {
this._setter(newValue)
}
}
Copy the code
We can see that computed is really a modified REF. When dirty is true, it indicates that the dependent value has changed and the getter function needs to be executed again. If not, use the _value value of the property on the instance. It’s kind of a caching mechanism, you don’t have to repeat the getter function.
Back to the question just mentioned: Why does a change in the value of a computed dependency trigger a computed side effect function?
We combine scenario 2 for analysis, and we call the effect that reads plusOne uniformly computedEffect. In fact, when a computed instance is initialized, the getter function instantiates the ReactiveEffect.
When the value of computedRef(plusOne) is first obtained, the computedEffect is collected and the ReactiveEffect (getter) function executes. In the execution of ReactiveEffect (getter), the value of count is read, so the getter also becomes a count dependent effect.
So when count changes, trigger is triggered. For ReactiveEffect (getter), schedule function is executed because there is a scheduler parameter. In schedule function, Dirty is set to true, and in addition to setting dirty, the computedEffect collected by the previous computedRef is executed.
The last
Analysis of Vue3 Reactivity principle for the moment to write this, I hope to help you.
This is the first time FOR me to export articles. Comments and suggestions are welcome.
Refs
- Pull hook education – vue. js 3.0 core source code analysis
- More on vue.js 3.2’s optimization of responsive parts
- Vue3 source analysis (1) : responsive principle