前端面试-vue(系列三)watch的实现

81 阅读3分钟

本文只针对vue3的设计与实现进行展开。后续称vue

watch的本质就是观察一个响应式数据,当数据发生变化的时候,通知并执行相应的回调函数。


watch(obj,()=> {
    console.log('数据变了')
})

obj++ // 数据的修改,会导致回调函数的执行。

实际上,watch的实现,本质上就是利用了effectoptions.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实现的原理。