本文只针对vue3的设计与实现进行展开。后续称
vue
watch的本质就是观察一个响应式数据,当数据发生变化的时候,通知并执行相应的回调函数。
watch(obj,()=> {
console.log('数据变了')
})
obj++ // 数据的修改,会导致回调函数的执行。
实际上,watch的实现,本质上就是利用了effect和options.scheduler的选项。
如以下代码:
effect(()=> {
console.log(obj.foo)
}, {
scheduler() {
// 当obj.foo改变的时候,会执行scheduler函数
}
})
对调度函数实现不了解到,可参考上一篇文章前端面试-vue(系列二);
那么我们就先实现一个简单的watch函数
function watch(source, cb) {
effect(() => source.foo, // 触发读取操作,从而建立联系。
{
scheduler(cb) {
cb() // 当数据改变的时候执行cb回调函数。
}
}
)
}
const data = {foo: 1}
const obj = new Proxy(data, {...})
watch(obj, ()=> {
console.log(‘数据变了’)
})
obj.foo++;
以上watch只能监听obj.foo的改变,需要进一步改造成,监听对象每一个属性改变。
function watch(source, cb) {
effect(() => traverse(source), // 递归读取source的属性
{
scheduler(cb) {
cb() // 当数据改变的时候执行cb回调函数。
}
}
)
}
function traverse(value, seen = new Set()) {
// 如果读取的数据是原始值,或者是已经被读取过的,那么就什么都不做
if(typeof value !=='object' || value === null || seen.has(value)) return
seen.add(value)
for(const k in value) {
traverse(value[k], seen)
}
return value
}
watch函数除了可以监听响应式数据,还可以接受一个getter函数:
watch(
// getter函数,指定监听obj.foo数据
() => obj.foo,
// 回调函数
()=> {}
)
只有obj.foo改变的时候,才会触发回调函数。需要对之前的wacth进行改造
function watch(source, cb) {
// 定义getter
let getter
// 如果source是一个函数,说明用户传递的是一个getter,所以直接把source赋值给getter
if(typeof source === 'function') {
getter = source
} else {
// 否则按照原来的实现,递归读取
getter = () => traverse(source)
}
effect(() => getter(), // 执行getter
{
scheduler(cb) {
cb() // 当数据改变的时候执行cb回调函数。
}
}
)
}
通常我们在使用watch的时候,可以在回调函数中拿到新值和旧值:
function watch(source, cb) {
// 定义getter
let getter
// 如果source是一个函数,说明用户传递的是一个getter,所以直接把source赋值给getter
if(typeof source === 'function') {
getter = source
} else {
// 否则按照原来的实现,递归读取
getter = () => traverse(source)
}
// 定义旧值和新值。
let oldValue, newValue
// effect返回的函数执行结果为getter的值。
const effectFn = effect(
() => getter(), // 执行getter
{
lazy: true,
scheduler(cb) {
newValue = effectFn()
cb(newValue, oldValue) // 当数据改变的时候执行cb回调函数。
oldValue = newValue
}
}
)
// 手动调用副作用函数,拿到的值就是旧值。
oldValue = effectFn
}
默认情况下,watch只有在数据改变的时候才会触发,但是也可以通过参数immediate来指定回调函数立即执行。
function watch(source, cb, options={}) {
// 定义getter
let getter
// 如果source是一个函数,说明用户传递的是一个getter,所以直接把source赋值给getter
if(typeof source === 'function') {
getter = source
} else {
// 否则按照原来的实现,递归读取
getter = () => traverse(source)
}
// 定义旧值和新值。
let oldValue, newValue
const job = () => {
newValue = effectFn()
cb(newValue, oldValue) // 当数据改变的时候执行cb回调函数。
oldValue = newValue
}
// effect返回的函数执行结果为getter的值。
const effectFn = effect(
() => getter(), // 执行getter
{
lazy: true,
scheduler: job
}
)
if(options.immediate) {
job()
} else {
oldValue = effectFn
}
}
还有一个flush选项可以指定执行时机,是在组件更新前(pre)还是更新后(post)。
function watch(source, cb, options={}) {
// 定义getter
let getter
// 如果source是一个函数,说明用户传递的是一个getter,所以直接把source赋值给getter
if(typeof source === 'function') {
getter = source
} else {
// 否则按照原来的实现,递归读取
getter = () => traverse(source)
}
// 定义旧值和新值。
let oldValue, newValue
const job = () => {
newValue = effectFn()
cb(newValue, oldValue) // 当数据改变的时候执行cb回调函数。
oldValue = newValue
}
// effect返回的函数执行结果为getter的值。
const effectFn = effect(
() => getter(), // 执行getter
{
lazy: true,
scheduler: () => {
// 如果是post,就将任务放置于微任务队列。
if(options.flush === 'post') {
const p = Promise.resolve()
p.then(() => {
job()
})
} else {
job()
}
}
}
)
if(options.immediate) {
job()
} else {
oldValue = effectFn
}
}
以上便是vue关于watch实现的原理。