- A+
所属分类:Web前端
专栏分享:vue2源码专栏,vue3源码专栏,vue router源码专栏,玩具项目专栏,硬核?推荐?
欢迎各位ITer关注点赞收藏???
语法
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数
const x = ref(0) const y = ref(0) // 单个 ref watch(x, (newValue, oldValue) => { console.log(`x is ${newValue}`) }) // getter 函数 watch( () => x.value + y.value, (newValue, oldValue) => { console.log(`sum of x + y is: ${newValue}`) } ) // 多个来源组成的数组 watch([x, () => y.value], ([newX, newY]) => { console.log(`x is ${newX} and y is ${newY}`) })
第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组
第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
第三个可选的参数是一个对象,支持以下这些选项:
- immediate:在侦听器创建时立即触发回调。第一次调用时旧值是 undefined。
- deep:如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器。
- flush:调整回调函数的刷新时机。参考回调的刷新时机及 watchEffect()。
- onTrack / onTrigger:调试侦听器的依赖。参考调试侦听器。
源码实现
-
@issue1 深度递归循环时考虑对象中有循环引用的问题
-
@issue2 兼容数据源为响应式对象和getter函数的情况
-
@issue3 immediate回调执行时机
-
@issue4 onCleanup该回调函数会在副作用下一次重新执行前调用
/** * @desc 递归循环读取数据 * @issue1 考虑对象中有循环引用的问题 */ function traversal(value, set = new Set()) { // 第一步递归要有终结条件,不是对象就不在递归了 if (!isObject(value)) return value // @issue1 处理循环引用 if (set.has(value)) { return value } set.add(value) for (let key in value) { traversal(value[key], set) } return value } /** * @desc watch * @issue2 兼容数据源为响应式对象和getter函数的情况 * @issue3 immediate 立即执行 * @issue4 onCleanup:用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求 */ // source 是用户传入的对象, cb 就是对应的回调 export function watch(source, cb, { immediate } = {} as any) { let getter // @issue2 // 是响应式数据 if (isReactive(source)) { // 递归循环,只要循环就会访问对象上的每一个属性,访问属性的时候会收集effect getter = () => traversal(source) } else if (isRef(source)) { getter = () => source.value } else if (isFunction(source)) { getter = source }else { return } // 保存用户的函数 let cleanup const onCleanup = fn => { cleanup = fn } let oldValue const scheduler = () => { // @issue4 下一次watch开始触发上一次watch的清理 if (cleanup) cleanup() const newValue = effect.run() cb(newValue, oldValue, onCleanup) oldValue = newValue } // 在effect中访问属性就会依赖收集 const effect = new ReactiveEffect(getter, scheduler) // 监控自己构造的函数,变化后重新执行scheduler // @issue3 if (immediate) { // 需要立即执行,则立刻执行任务 scheduler() } // 运行getter,让getter中的每一个响应式变量都收集这个effect oldValue = effect.run() }
测试代码
循环引用
对象中存在循环引用的情况
const person = reactive({ name: '柏成', age: 25, address: { province: '山东省', city: '济南市', } }) person.self = person watch( person, (newValue, oldValue) => { console.log('person', newValue, oldValue) }, { immediate: true }, )
数据源
- 数据源为 ref 的情况,和 immediate 回调执行时机
const x = ref(1) watch( x, (newValue, oldValue) => { console.log('x', newValue, oldValue) }, { immediate: true }, ) setTimeout(() => { x.value = 2 }, 100)
- 兼容数据源为 响应式对象 和 getter函数 的情况,和 immediate 回调执行时机
const person = reactive({ name: '柏成', age: 25, address: { province: '山东省', city: '济南市', } }) // person.address 对象本身及其内部每一个属性 都收集了effect。traversal递归遍历 watch( person.address, (newValue, oldValue) => { console.log('person.address', newValue, oldValue) }, { immediate: true }, ) // 注意!我们在 watch 源码内部满足了 isFunction 条件 // 此时只有 address 对象本身收集了effect,仅当 address 对象整体被替换时,才会触发回调; // 其内部属性发生变化并不会触发回调 watch( () => person.address, (newValue, oldValue) => { console.log('person.address', newValue, oldValue) }, { immediate: true }, ) // person.address.city 收集了 effect watch( () => person.address.city, (newValue, oldValue) => { console.log('person.address.city', newValue, oldValue) }, { immediate: true }, ) setTimeout(() => { person.address.city = '青岛市' }, 100)
onCleanup
watch回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数(即我们的onCleanup)。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
const person = reactive({ name: '柏成', age: 25 }) let timer = 3000 function getData(timer) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(timer) }, timer) }) } // 1. 第一次调用watch的时候注入一个取消的回调 // 2. 第二次调用watch的时候会执行上一次注入的回调 // 3. 第三次调用watch会执行第二次注入的回调 // 后面的watch触发会将上次watch中的 clear 置为true watch( () => person.age, async (newValue, oldValue, onCleanup) => { let clear = false onCleanup(() => { clear = true }) timer -= 1000 let res = await getData(timer) // 第一次执行2s后渲染2000, 第二次执行1s后渲染1000, 最终应该是1000 if (!clear) { document.body.innerHTML = res } }, ) person.age = 26 setTimeout(() => { person.age = 27 }, 0)