前端vue高频面试题 watch实现原理
本文收录专栏vue.js源码解读,对vue3 proxy原理和副作用函数还没有了解的读者可以先阅读本文,前置知识-vue3响应式原理与副作用函数
1.回顾什么是watch
watch本质上就是观测一个响应式数据,当数据发生变化时,通知并执行相应的回调函数
例如:
watch(obj,()=>{
console.log('obj property is changed')
})
假设obj是一个响应式数据,当他的key发生变化时,watch会观测到他的变化,并执行相应的回调函数
仔细观察这种形式:是不是很像我们之前写过的副作用函数?通过proxy和副作用函数检测属性值的变化并更新相应视图
其实watch就是在副作用函数effect的形式上进一步封装,watch利用了effect的options.scheduler调度器
如以下代码所示
(这里再提醒一下,如果没看过前一篇文章尽量去看,本文的前置知识依赖它,直接读可能会看不懂某些代码)
function watch(source, cb) {
effect(
() => source.foo,
{
scheduler() {
cb()
}
}
)
}
我们可以看到,目前使用副作用函数effect封装了一个简易版的effect,现在来分析一下它的运行流程
- 首先effect的第一个函数参数中读取了source的一个key,那么与这个key关联的副作用函数就会被关联到weakmap(桶)中
- 紧接着我们在options的scheduler选项中执行用户传进来的回调函数cb,这样当数据发生变化时,调度器就会执行cb函数
我们来使用一下这个watch函数
const data1 = { foo: 1 }
const obj1 = new Proxy(data1, {
get(target, key) {
track(target, key)
return target[key]
},
set(target, key, newVal) {
target[key] = newVal
trigger(target, key)
}
})
watch(obj1, () => {
console.log(data1)
})
document.querySelector('button').onclick = function () {
obj1.foo++
}
我们可以看到,当点击按钮修改代理对象obj1的foo属性时,浏览器执行了回调函数,打印出原始对象
上面的代码能正常工作,但是我们写死了source的属性,目前的代码只能检测source.foo这一个属性。为了让watch函数具有通用性,我们需要封装一个通用的读取操作。
我们编写一个traverse函数,它的目标就是读取一个对象里的所有属性,这样就可以在副作用函数中将副作用函数与对象的每一个key建立联系,从而实现观测对象所有属性变换的目的
function traverse(value, seen = new Set()) {
//如果读取的数据是原始值,或者已经读取过了,那么旧直接返回
if (typeof value !== 'object' || value == null || seen.has(value)) return
//将数据添加到seen中,代表遍历地读取过了,避免循环引用引起的死循环
seen.add(value)
//暂时不考虑数组
//假设value是一个对象,使用for in读取对象的每一个值,并且递归的调用traverse进行处理
for (const k in value) {
traverse(value[k], seen)
}
return value
}
简单的修改一下watch函数,使用traverse
function watch(source, cb) {
effect(
() => traverse(source),
{
scheduler() {
cb()
}
}
)
}
但是我们平时在使用watch的经验告诉我们,watch不仅仅可以只接收一个对象,它还可以接收一个getter函数,这个函数里使用的对象发生变化时会导致回调函数的执行
function watch(source, cb) {
let getter
//如果source是函数,则说明用户传递的是getter,所以直接把source赋值给getter
if (typeof source === 'function') {
getter = source
} else {
//否则就执行之前的逻辑
getter = () => traverse(source)
}
effect(
() => getter(),//执行getter
{
scheduler() {
cb()
}
}
)
}
现在我们的watch还缺少一个重要的能力,即在回调函数中还拿不到新值和旧值
//待更新 2023/4/2