专栏分享:vue2源码专栏,vue3源码专栏,vue router源码专栏,玩具项目专栏,硬核💪推荐🙌
欢迎各位ITer关注点赞收藏🌸🌸🌸
语法
侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数
const x = ref(0)const y = ref(0)// 单个 refwatch(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 = personwatch( 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 收集了 effectwatch( () => 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 = 3000function getData(timer) { return new Promise((resolve, reject) => { setTimeout(() => { resolve(timer) }, timer) })}// 1. 第一次调用watch的时候注入一个取消的回调// 2. 第二次调用watch的时候会执行上一次注入的回调// 3. 第三次调用watch会执行第二次注入的回调// 后面的watch触发会将上次watch中的 clear 置为truewatch( () => 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 = 26setTimeout(() => { person.age = 27}, 0)
人间不正经生活手册