专栏分享: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)
1.本站内容仅供参考,不作为任何法律依据。用户在使用本站内容时,应自行判断其真实性、准确性和完整性,并承担相应风险。
2.本站部分内容来源于互联网,仅用于交流学习研究知识,若侵犯了您的合法权益,请及时邮件或站内私信与本站联系,我们将尽快予以处理。
3.本文采用知识共享 署名4.0国际许可协议 [BY-NC-SA] 进行授权
4.根据《计算机软件保护条例》第十七条规定“为了学习和研究软件内含的设计思想和原理,通过安装、显示、传输或者存储软件等方式使用软件的,可以不经软件著作权人许可,不向其支付报酬。”您需知晓本站所有内容资源均来源于网络,仅供用户交流学习与研究使用,版权归属原版权方所有,版权争议与本站无关,用户本人下载后不能用作商业或非法用途,需在24个小时之内从您的电脑中彻底删除上述内容,否则后果均由用户承担责任;如果您访问和下载此文件,表示您同意只将此文件用于参考、学习而非其他用途,否则一切后果请您自行承担,如果您喜欢该程序,请支持正版软件,购买注册,得到更好的正版服务。
5.本站是非经营性个人站点,所有软件信息均来自网络,所有资源仅供学习参考研究目的,并不贩卖软件,不存在任何商业目的及用途
暂无评论内容