前言
本文学习编写 watch 功能函数。首先,先去使用下官方的 watch 做一些简单的小功能测试。
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
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>computed</title> </head> <body> <div id="app"></div> <script src="../../node_modules/vue/dist/vue.global.js"></script> <script> const { reactive, watch } = Vue; const state = reactive({ name: "jw", address: { num: 1 } }); watch(state, (newValue, oldValue) => { console.log(newValue, oldValue); });
watch( () => state.address.num, (newValue, oldValue) => { console.log(newValue, oldValue); } );
watch(state.address.num, (newValue, oldValue) => { console.log(newValue, oldValue); });
setTimeout(() => { state.address.num = 123; }, 1000); </script> </body> </html>
|
上面尝试了 3 中方式,结果发现第三种不触发打印,因为你监听的是数值 1,一个常量就不会有改变的情况。
另外如果你监听一个对象,例如state.address
这样的,那也不会触发打印。因为这个对象是引用类型,也就是在内存中的地址也没改变,你就是改变了对象属性的值,他监听不到改变。
第二种写法是比较优的方式。
编写 watch
在上述官方的使用过程中,watch 接受了 2 个参数。
- 用户需要监听的参数,可能是函数也可能是一个 reactive 对象
- 用户传入的回调函数
当需要监听的数据改变的时候,触发回调函数。那么实际上就相当于给监听的数据的属性绑定一个 effect,做一个依赖收集,这样当数据改变,就触发用户的回调函数。
那么第一步对传入的数据做判断,如果是 reacvtive 就遍历数据对象,然后每个属性做依赖收集
如果是函数的话,就不用遍历。
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
| function traversal(value, set = new Set()) { if (isObject(value)) return value;
if (set.has(value)) return value; set.add(value);
for (let key in value) { traversal(value[key], set); }
return value; }
export function watch(source, cb) { let getter; if (isReactive(source)) { getter = () => traversal(source); } else if (isFunction(source)) { getter = source; } else { return; } let oldValue; let cleanup; const job = () => { const newValue = effect.run(); cb(newValue, oldValue, onCleanup); oldValue = newValue; }; const effect = new ReactiveEffect(getter, job);
oldValue = effect.run(); }
|
这就是一个简单的 watch 函数。在 wathc 官方用法中,还遇到这样一种情况。例如 input 上面你输入文字然后进行接口调用搜索关键字,你输入了 2 个子。那么当第一个接口的延迟是 2 秒中之后返回数据,第二个文字接口在 500 毫秒返回数据。由于接口的调用是并行的。那么最终会才用第一个接口的数据,第二个比第一个快,导致被覆盖了,这是不正确的。
正常的做法有用防抖来做,那么 vue 官方提供了一个 onCleanup 回调函数。
当数据改变引起变化的时候,会调触发上一个 watch 回调的 oncleanup。这样就能通过一些操作来渲染正确的数据。
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
| const { reactive, watch } = VueReactivity; const state = reactive({ name: "jw", address: { num: 1 } }); let getMoreData = (time) => { return new Promise((resolve, reject) => { setTimeout(() => { resolve(state.address.num); }, time); }); };
watch( () => state.address.num, async (newValue, oldValue, onCleanup) => { let clean = false; onCleanup(() => { clean = true; }); let i = Math.random() * 10000; console.log(i); let text = await getMoreData(i); if (!clean) { document.getElementById("app").innerHTML = text; } } ); state.address.num = 456; state.address.num = 123; state.address.num = 678; state.address.num = 999;
|
上面的分析,就是在触发回调函数的时候触发上一个 watch 中的 onCleanup 函数。
对 watch 增加一些功能,记录一下用户的 onCleanup 函数内容。
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
| import { ReactiveEffect } from "./effect"; import { isReactive } from "./reactive"; import { isFunction, isObject } from "@vue/shared";
function traversal(value, set = new Set()) { if (isObject(value)) return value;
if (set.has(value)) return value; set.add(value);
for (let key in value) { traversal(value[key], set); }
return value; }
export function watch(source, cb) { let getter; if (isReactive(source)) { getter = () => traversal(source); } else if (isFunction(source)) { getter = source; } else { return; } let oldValue; let cleanup; const onCleanup = (fn) => { cleanup = fn; };
const job = () => { if (cleanup) cleanup(); const newValue = effect.run(); cb(newValue, oldValue, onCleanup); oldValue = newValue; }; const effect = new ReactiveEffect(getter, job);
oldValue = effect.run(); }
|
针对 props 中数组的监听
在实际项目中常常需要编写组件,其中组件需要通过 props 接受参数。然后会遇到监听这些参数的改变做一些操作。那么就会遇到数组这种结构。
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
| let props = defineProps({ list: { type: Array, default: () => [], }, });
watch( () => props.list, (newVal, oldVal) => { console.log(newVal); } );
watch(props.list, (newVal, oldVal) => { console.log(newVal); });
watch( () => [...props.list], (newVal, oldVal) => { console.log(newVal); } );
|
如果传入的数组使用reactive
包裹的会出现写法一和写法三无法触发。
但是如果是ref
包裹的,那么这段代码中,无法更改的是写法二,因为在 watch API 中,它只接受一个响应式对象作为第一个参数,而不能是一个 getter 函数。因此,写法一和写法三都是可以正常工作的,它们都会在 props.list 变化时触发 watch 回调函数并打印新的值。但是,写法三使用了一个新的数组,而不是原始的 props.list 数组,这意味着在数组中添加、删除或替换元素时,watch 回调函数将会触发。相比之下,写法一和写法二将只在 props.list 的引用更改时触发。
1
| git:[@github/MicroMatrixOrg/vue3-plan/tree/watch]
|