preface
When I look at the source code of reactive function, I find that the dependencies are not collected by calling reactive function alone. So when will the dependencies be collected? I changed the previous demo and added watchEffect, and found that the attributes used in watchEffect will be collected. Let’s dive into the source code to find out why dependencies are collected after watchEffect is called.
Modified Demo:
<script> ... debugger watch((event) => { return data.a }, (newVal, Track () watchEffect(() => {/** * data.a // getter * data.count = // setter trigger() **/ data.count = data.a + 1 console.log('count: ', data.count) }) </script>Copy the code
Again, we enter in debug mode.
Let’s start
In the demo we added the watch function because both watch and watchEffect call the same function doWatch, but with different arguments.
watch & watchEffect
In both cases, you can see that the doWatch function is called and its return value is returned. Next, look directly at the doWatch function.
doWatch
DoWatch receives four parameters
source
To monitor the source of thecallback
Callback function that listens foroptions
Other options for listeningcurrentInstance
Current component instance
What’s the difference between watchEffect and when watch calls it
watchEffect
At the time of invocationcallback
The incoming isnull
watch
What is passed in when it is calledcallback
thecallback
Is alsowatch
A parameter to a function
Back to the doWatch function, we first do a development environment warning handling, and then define a warnInvalidSource warning handling method.
The getters are defined by the source type, reactive, array, and function, but the purpose of the getters remains the same:
-
The ref type gets the value through.value
const refVal = ref(0) watch(ref, (newVal, oldVal) => {}) Copy the code
-
Reactive type is directly source retrun
const data = reactive({ a: 1 }) watch(data, (newVal, oldVal) => {}) Copy the code
-
The array type is evaluated for the type of each item
Const refVal = ref(0) const data = reactive({}) const fun = () => 0 const arr = [refVal, // Recursively call the traverse function fun // Call the callWithErrorHandling function to execute source]Copy the code
The traverse function passes each item in the array, determines what type it is, calls it recursively, and returns the result.
The callWithErrorHandling function is called with the first argument, and the fourth argument, if any, is passed in when called.
-
The function type determines if there is a callback, and if there is, calls the callWithErrorHandling function to execute the source. Not it is watchEffect call, at that time the getter function if the current instance is unloaded the return, and then call the cleanup function, called callWithAsyncErrorHandling function performs the source. The cleanup function can have defined below, callWithAsyncErrorHandling function and the role of callWithErrorHandling function is the same, Much difference between callWithAsyncErrorHandling a capture of promise error. In the call callWithAsyncErrorHandling when introduced into the fourth parameter, arguments as onInvalidate function, can be explained in the call we watchEffect, inside the callback function will have a parameter, The parameter value is the onInvalidate function. See the documentation for the onInvalidate function.
The following is a compatibility for the 2. X version of the listener array.
Then it is determined that if the call is made by watch and the deep property is set to true, then deep listening is performed.
We define cleanup and onInvalidate,
let cleanup: () => void let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {cleanup = runner.options.onStop = () => {fn callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } }Copy the code
The onInvalidate function accepts an argument of type function (fn). The function registers fn to the onStop function of the current effect and assigns onStop to the onStop function. The benefit of this is that, Fn can also be executed when the cleanup is performed.
The getter is called directly when called by watchEffect, and the callback is called immediately when called by watch and immediate is true, since watch is lazy. If immediate is set to true, it must be returned immediately.
The following defines the job function that executes the watch and watchEffecct operations.
const job: SchedulerJob = () => { if (! Active) {return} if (cb) {// call by watch... } else {// call the watchEffect function runner() directly}}Copy the code
Inside the job if called by watch, call cleanup if newValue and oldValue are different, and call callback.
If it is called by watchEffect then it calls the function that was passed in directly, as described below in the Runner function.
There’s this line of code down here
job.allowRecurse = !! cbCopy the code
This line of code allows recursive calls, allowing the watch callback to change the value it is listening for, so that the current watch can hear that the value it is listening for has changed. That’s it
const num = ref(0)
watch(num, (newVal, oldVal) => {
if (newVal < 10) {
num.value = newVal + 1
}
})
Copy the code
Keep reading
let scheduler: If (flush === 'sync') {if (flush === 'sync') {if (flush === 'sync') gets called directly } else if (flush === 'post') { scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { // default: 'pre' scheduler = () => { if (! instance || instance.isMounted) { queuePreFlushCb(job) } else { // with 'pre' option, The first call must happen before // The component is mounted so it is called synchronously. // The instance exists but has not been mounted, Job job()}}Copy the code
This defines a scheduler. The scheduler uses flush to determine when to call the job defined above. Synchronous call when flush is equal to sync; Is equal to post, the job is added to the microtask queue, and the call time is handed over to the event loop; If the component instance does not exist or the instance has been mounted, the job should be added to the task queue. If the component instance exists but has not been mounted, the job should be executed directly.
And then we define the runner that we mentioned earlier, once we define the onStop function on the runner in the onInvalidate function, The other time is when the job is defined and the watchEffect call executes the Runner directly. So let’s see what is this runner
const runner = effect(getter, {
lazy: true,
onTrack,
onTrigger,
scheduler
})
Copy the code
The getter and scheduler defined above are finally used here. Enter effect function
export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> {// change fn to the original object if (isEffect(fn)) {fn = fn.raw} const effect = createReactiveEffect(fn, options) if (! options.lazy) { effect() } return effect }Copy the code
It has two judgments at the end of a song. If the fn passed in is an effect, change it to the original object; If you pass lazy false, you call the effect within the method, which is the return value of the createReactiveEffect function between the two judgments. Instead of looking at createReactiveEffect, make a note of it and come back to it after looking at doWatch.
After defining the runner and performed recordInstanceBoundEffect function, is the role of the function of runner push into the effects of the current component instance array. In other words, the current runner must be bound to the current component instance so that the component instance can be destroyed together.
If ‘watch’ is true, then ‘job’ is called immediately. If ‘watch’ is true, then ‘job’ is called immediately. If ‘watch’ is true, then ‘job’ is called immediately. So that later newvalue and oldValue can be compared.
If not, check to see if flush is equivalent to POST true and call queuePostRenderEffect to put Runner in the event loop for the event loop to fire.
If none of the above conditions are met, the Runner function is called directly.
Finally, return a function that calls stop and removes the runner on the current instance if the instance exists. This matches the definition that watchEffect returns a function to stop the current watchEffect.
createReactiveEffect
We called effect when we defined runner, and we called createReactiveEffect inside of effect, so let’s see what this function does.
function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { const effect = function reactiveEffect(): unknown { // ... } as ReactiveEffect effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = options return effect }Copy the code
This function takes two arguments, both passed in when the external effect function is called
fn
Corresponding externalgetter
options
Corresponding to external{ lazy: true, onTrack, onTrigger, scheduler }
We defined an effect inside the createReactiveEffect function, which had the value reactiveEffect, and then added many properties to the effect
effect.id = uid++ effect.allowRecurse = !! options.allowRecurse effect._isEffect = true effect.active = true effect.raw = fn effect.deps = [] effect.options = optionsCopy the code
And then effect return out.
Look at the reactiveEffect function
const effect = function reactiveEffect(): unknown { if (! effect.active) { return fn() } if (! EffectStack. Includes (effect)) {/ * * * here to empty dependency is to follow-up can only add the necessary reliance on * https://juejin.cn/post/6909698939696447496#heading-14 */ cleanup(effect) try {//... } finally { // ... } } } as ReactiveEffectCopy the code
This method calls fn if active is false and returns the result.
If the effectStack does not contain the effect, then the following code is executed. The effectStack is the array variable defined on the current page that holds the effect defined here.
This is followed by a cleanup, which was mentioned earlier, but this is not the same as this one
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
Copy the code
This cleanup passes in an effect that clears deps from the effect and later readds dependencies. I got this conclusion from reading this article.
Where does deps come from? You can see that there are many properties added to effect, one of which has deps. Active effe.deps.push (dep) activeeffe.deps.push (dep) is also mentioned in the track function of reactive source code.
Let’s start executing the code in the try
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn()
Copy the code
The enableTracking function is called, which pushes shouldTrack into the trackStack and sets shouldTrack to true. Both the trackStack and shouldTrack are variables declared on the current page. The trackStack is an array variable that holds the shouldTrack variable, which is a Boolean variable.
The effect is then pushed into the effectStack and assigned to the activeEffect. To reach this point, we can think of the initial judgment in the track function
if (! shouldTrack || activeEffect === undefined) { return }Copy the code
When watchEffect is called and executed at this point, dependencies collected by track will not be collected by return.
Back to the code in the try that assigns effect to activeEffect and calls fn and returns the value return.
There is finally so even return will be executed because fn has already been called, so the previous assignment is reset
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
Copy the code
The resetTracking function is used to delete the last one of the trackStack because enableTracking was pushed in.
And then we reset shouldTrack.
That’s all the code in createReactiveEffect.
Questions left over from reactive source
Q: When are dependencies collected
A: dependencies will be collected after watchEffect is called. Since activeEffect is assigned during code execution and is not returned in track, dependencies will be collected normally.
Refer to the link
createReactiveEffect
Functioncleanup
The reason why- Vue3 Watch /watchEffect