前端vue高频面试题 watch实现原理

62 阅读3分钟

前端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,现在来分析一下它的运行流程

  1. 首先effect的第一个函数参数中读取了source的一个key,那么与这个key关联的副作用函数就会被关联到weakmap(桶)中
  2. 紧接着我们在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属性时,浏览器执行了回调函数,打印出原始对象

image.png

上面的代码能正常工作,但是我们写死了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