The so-called watch, its essence is to observe a responsive data, notify and execute the corresponding callback function when the data changes. In fact, the essence of watch implementation is to use the effect and options.scheduler options. As shown in the following example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
As shown in the code, source is responsive data, and cb is the callback function. If there is a scheduler option in the side effect function, when the responsive data changes, the execution of the scheduler function will be triggered instead of directly triggering the execution of the side effect function. From this perspective, the scheduler scheduling function is equivalent to a callback function, and the implementation of watch takes advantage of this.
The data source to listen to can be an array, as shown in the following function signature:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
You can also use an array to listen to multiple sources at the same time, as shown in the following function signature:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
The data source to listen to is a ref type data or Is a getter function with a return value, as shown in the following function signature:
1 2 3 4 5 6 7 8 9 10 11 |
|
The listening data source is a responsive obj object, as shown in the following function signature:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
As you can see, the watch function receives 3 parameters, namely: source listening data source, cb callback function, options listening options.
It can be known from the function overloading of watch that when listening to a single source, the source can be a ref type data or a getter function with a return value. , or it can be a responsive obj object. When listening to multiple sources, source can be an array.
In the cb callback function, the developer is provided with the latest value, the old value and the onCleanup function for cleaning side effects. As shown in the following type definition:
1 2 3 4 5 |
|
options Options can control the behavior of the watch. For example, the option parameter immediate of options can be used to control whether the callback of the watch is executed immediately. The option parameter controls whether the watch callback function is executed synchronously or asynchronously. The type definition of options parameter is as follows:
1 2 3 4 5 6 7 |
|
You can see the type definition of options. WatchOptions inherits WatchOptionsBase. That is to say, in addition to the two unique parameters of immediate and deep in the options of watch, all parameters in WatchOptionsBase can also be passed to control the behavior of side effect execution.
The doWatch function is called in the function body of watch. Let’s take a look at its implementation.
In fact, whether it is the watch function or the watchEffect function, the doWatch function is ultimately called during execution.
1 2 3 4 5 |
|
The function signature of doWatch is basically the same as the function signature of watch, and also receives three parameters. In order to facilitate the use of the options option, the doWatch function deconstructs it.
First, get the current component instance through component, and then declare three different variables. One of the functions is called a getter, which is passed as an argument to the side-effect function. The variable forceTrigger is a Boolean value that indicates whether the side-effect function needs to be forced. The isMultiSource variable is also a Boolean value, used to mark whether the listening data source is a single source or multiple sources passed in in the form of an array. The initial value is false, indicating that the listening data source is a single source. As shown in the following code:
1 2 3 4 5 6 |
|
Next, initialize these three variables according to the listening data source.
The listening data source is a ref type data
When the listening data source is a ref type data, initialize by returning source.value Getter, that is to say, when the getter function is triggered, the actual listening data will be obtained through source.value. Then use the isShallow function to determine whether the listened data source is a shallow response, and assign the result to forceTrigger to complete the initialization of the forceTrigger variable. As shown in the following code:
1 2 3 4 5 6 |
|
The listening data source is a responsive data
When the listening data source is a responsive data, directly Return source to initialize the getter, that is, when the getter function is triggered, it directly returns the listening data source. Since the responsive data may be an object object, set deep to true, and the object's attribute values can be read recursively when the getter function is triggered. As shown in the following code:
1 2 3 4 5 |
|
The listening data source is an array
When the listening data source is an array, that is, multiple listening data sources are listened to at the same time source. At this time, directly set the isMultiSource variable to true, indicating that multiple sources are being listened to. Then use the some method of the array to detect whether there are responsive objects in the multiple listening sources, and assign the results to forceTrigger. Traverse the array and complete the initialization of the getter function based on the type of each source. As shown in the following code:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
The listening data source is a function
当侦听的数据源是一个具有返回值的 getter 函数时,判断 doWatch 函数的第二个参数 cb 是否有传入。如果有传入,则处理的是 watch 函数的场景,此时执行 source 函数,将执行结果赋值给 getter 。该情况仅适用于 watchEffect 函数未接收到参数的情况。如果组件实例已被卸载,则直接返回而不执行 source 函数,根据该场景进行处理。如果未能执行成功,则执行清除依赖的代码并调用source函数,将返回结果赋值给getter。如下面的代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
如果侦听的数据源是一个响应式数据,需要递归读取响应式数据中的属性值。如下面的代码所示:
1 2 3 4 5 6 |
|
在上面的代码中,doWatch 函数的第二个参数 cb 有传入,说明处理的是 watch 中的场景。deep 变量为 true ,说明此时侦听的数据源是一个响应式数据,因此需要调用 traverse 函数来递归读取数据源中的每个属性,对其进行监听,从而当任意属性发生变化时都能够触发回调函数执行。
声明 cleanup 和 onCleanup 函数,并在 onCleanup 函数的执行过程中给 cleanup 函数赋值,当副作用函数执行一些异步的副作用时,这些响应需要在其失效是清除。如下面的代码所示:
1 2 3 4 5 6 7 |
|
为了便于控制 watch 的回调函数 cb 的执行时机,需要将 scheduler 调度函数封装为一个独立的 job 函数,如下面的代码所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
|
在 job 函数中,判断回调函数 cb 是否传入,如果有传入,那么是 watch 函数被调用的场景,否则就是 watchEffect 函数被调用的场景。
如果是 watch 函数被调用的场景,首先执行副作用函数,将执行结果赋值给 newValue 变量,作为最新的值。然后判断需要执行回调函数 cb 的情况:
如果侦听的数据源是响应式数据,需要深度侦听,即 deep 为 true
如果需要强制触发副作用函数执行,即 forceTrigger 为 true
如果新旧值发生了变化
如果存在上述三种情况之一,就必须执行 watch 函数的回调函数 cb。如果回调函数 cb 是再次执行,在执行之前需要先清除副作用。然后调用 callWithAsyncErrorHandling 函数执行回调函数cb,并将新值newValue 和旧值 oldValue 传入回调函数cb中。在回调函数cb执行后,更新旧值oldValue,避免在下一次执行回调函数cb时获取到错误的旧值。
如果是 watchEffect 函数被调用的场景,则直接执行副作用函数即可。
设置 job 函数的 allowRecurse 属性根据是否传递回调函数 cb 来进行。这个设置非常关键,因为它可以使作业充当监听器的回调,这样调度程序就能够知道它是否允许调用自身。
1 2 3 4 |
|
在调用 watch 函数时,可以通过 options 的 flush 选项来指定回调函数的执行时机:
当 flush 的值为 sync 时,代表调度器函数是同步执行,此时直接将 job 赋值给 scheduler,这样调度器函数就会直接执行。
当 flush 的值为 post 时,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行。
当 flush 的值为 pre 时,即调度器函数默认的执行方式,这时调度器会区分组件是否已经挂载。如果组件未挂载,则先执行一次调度函数,即执行回调函数cb。在组件挂载之后,将调度函数推入一个优先执行时机的队列中。
// 这里处理的是回调函数的执行时机
let scheduler: EffectScheduler if (flush === 'sync') { // 同步执行,将 job 直接赋值给调度器 scheduler = job as any // the scheduler function gets called directly } else if (flush === 'post') { // 将调度函数 job 添加到微任务队列中执行 scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { // default: 'pre' // 调度器函数默认的执行模式 scheduler = () => { if (!instance || instance.isMounted) { // 组件挂载后将 job 推入一个优先执行时机的队列中 queuePreFlushCb(job) } else { // with 'pre' option, the first call must happen before // the component is mounted so it is called synchronously. // 在 pre 选型中,第一次调用必须发生在组件挂载之前 // 所以这次调用是同步的 job() } } }
初始化完 getter 函数和调度器函数 scheduler 后,调用 ReactiveEffect 类来创建一个副作用函数
1 2 |
|
在执行副作用函数之前,首先判断是否传入了回调函数cb,如果有传入,则根据 options 的 immediate 选项来判断是否需要立即执行回调函数cb,如果指定了immediate 选项,则立即执行 job 函数,即 watch 的回调函数会在 watch 创建时立即执行一次。如果不这样做,就需要手动调用副作用函数,将其返回值赋值给oldValue作为旧值。如下面的代码所示:
1 2 3 4 5 6 7 8 9 10 |
|
如果 options 的 flush 选项的值为 post ,需要将副作用函数放入到微任务队列中,等待组件挂载完成后再执行副作用函数。如下面的代码所示:
1 2 3 4 5 6 7 |
|
其余情况都是立即执行副作用函数。如下面的代码所示:
1 2 3 4 |
|
最终,doWatch函数返回了一个匿名函数,该函数用于取消对数据源的监听。因此在调用 watch 或者 watchEffect 时,可以调用其返回值类结束侦听。
1 2 3 4 5 6 7 |
|
The above is the detailed content of What is the implementation principle of Vue3 listener watch. For more information, please follow other related articles on the PHP Chinese website!