4.1 Vue中watch函数的实现原理

watch函数的基本使用

Vue中的watch作为侦听属性,他的作用为侦听一个数据的变化,若数据变化则触发相应的回调函数。

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

如上述代码所示,如果obj发生变化,则触发回调。当然watch函数并不仅仅是这些功能,这些功能其实副作用函数也能实现,watch函数可以获取变化前后的值,接收getter函数,获取变化前后的值,通过调度器调度回调函数的执行时机等。这些功能都会在下文进行详细的叙述。

watch的基本实现

watch函数的实现实质上就是使用effectscheduler实现的,只是对effect函数的封装。当触发副作用函数时如果scheduler存在就会直接执行scheduler,而我们将回调函数放入scheduler中,执行时就会将scheduler中的回调函数执行,这其实就是watch函数的执行流程,最简单的代码实现如下:

// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb) {

    effect(
        // 触发读取操作,从而建立联系
        () => source.foo,
        {
            scheduler: () => {
                // 当数据变化时,调用cb函数
                cb()
            }
        }
    )
}

可以这样使用

const data = {foo: 1}
const obj = new Proxy(data, {
    get: (target, key, receiver) => {
        track(target, key, receiver)
        return Reflect.get(target, key, receiver)
    },
    set: (target, key, value, receiver) => {
        Reflect.set(target, key, value, receiver)
        trigger(target, key)
        return true
    }
});

watch(obj, () => {console.log('change')})
obj.foo++

这样一个最简单的watch函数就实现了,同时也可以运行结果,但是实际上我们上面代码的watch函数只是针对了obj.foo做侦听,实际上我们可能对整个对象做侦听,所以在访问对象元素做关联的部分需要完善

// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb) {
    effect(
        // 触发读取操作,从而建立联系
        () => traverse(source),
        {
            scheduler: () => {
                // 当数据变化时,调用cb函数
                cb()
            }
        }
    )

    // seen防止交叉引用形成死循环
    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(() => obj.foo, () => {console.log('change')})

所以我们在处理的时候需要将传入的两种情况兼容一下,其实也很简单,我们可以统一使用getter的方式来进行处理。

// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb) {
    let getter
    if(typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }

    effect(
        // 触发读取操作,从而建立联系
        () => getter(),
        {
            scheduler: () => {
                // 当数据变化时,调用cb函数
                cb()
            }
        }
    )

    // 省略代码 ......
}

获取watch函数的新值和旧值

获取watch函数的新值和旧值主要是依靠的lazy机制,在effect当中副作用函数传入的是getter函数,所以只要我们每次执行就可以获取到值,使用了lazy标记之后,每次会返回一个待执行的getter,这样在获取新值时就可以直接执行getter函数获取值。

const effectStack = []
function effect(fn, options = {}) {
    const effectFn = () => {
      activeEffect = effectFn
      //先清除再执行,自然就形成了分支切换
      cleanup(effectFn)

      // 用栈记录下当前的辅作用函数  
      effectStack.push(effectFn)

      const res = fn()
      effectStack.pop()
      // 递归出来时改变activeEffect指向
      activeEffect = effectStack[effectStack.length-1]

      return res
    }

    effectFn.options = options
    effectFn.deps = []

    if(options.lazy)
        return effectFn
    effectFn()
}

// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb) {
    let getter
    if(typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }

    let newValue, oldValue
    const effectFn = effect(
        // 触发读取操作,从而建立联系
        () => getter(),
        {
            // 打上lazy标记,在需要获取的地方再执行
            lazy: true,
            scheduler: () => {
                // 调用effectFn获取到改变后的值
                newValue = effectFn()
                // 当数据变化时,调用cb函数
                cb(oldValue, newValue)
                // 回调结束后更新旧值
                oldValue = newValue
            }
        }
    )
    // 第一次手动调用effectFn,相当于直接执行getter,获取到初始值
    oldValue = effectFn()

	// 省略代码 ......
}

watch(() => obj.foo, (oldValue, newValue) => {console.log(oldValue, newValue)})
obj.foo++

watch函数的执行时机

watch函数实质上是对effect函数的封装,上面章节中对effect函数做了调度,但是对于watch函数还没做调度,一般来说watch函数的调度有两种

  1. 立即执行的调度,在创建时就执行(不需要第一次发生修改)。
  2. 设置watch函数的执行时机。

首先要实现的是创建时就立即执行的标记,还是正常使用前面章节中提到的调度器,加入一个immediate字段作为标记,代码如下:

// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb, options = {}) {
    let getter
    if(typeof source === 'function') {
        getter = source
    } else {
        getter = () => traverse(source)
    }

    let newValue, oldValue

    // 将调度函数封装成一个独立的job函数
    const job = () => {
        // 调用effectFn获取到改变后的值
        newValue = effectFn()
        // 当数据变化时,调用cb函数
        cb(oldValue, newValue)
        // 回调结束后更新旧值
        oldValue = newValue
    }
    const effectFn = effect(
        // 触发读取操作,从而建立联系
        () => getter(),
        {
            // 打上lazy标记,在需要获取的地方再执行
            lazy: true,
            scheduler: job
        }
    )
    // 当immediate为true的时候立即执行job,从而触发回调
    if(options.immediate) {
        job()
    } else {
        oldValue = effectFn()
    }
    // 第一次手动调用effectFn,相当于直接执行getter,获取到初始值
    oldValue = effectFn()
}

第二种调度是在Vue3中新增的,设置回调函数的执行时机,一般通过flush字段控制,可选择'post'/'pre'/'sync'

例如对post的实现,这表示回调函数会在dom更新之后执行,实现方法是将回调函数放入微任务队列中,这里使用的是promise来实现的

// 接收两个参数,source是响应式数据,cb是回调函数
function watch(source, cb, options = {}) {
    // 省略代码 ......
    
    const effectFn = effect(
        // 触发读取操作,从而建立联系
        () => getter(),
        {
            // 打上lazy标记,在需要获取的地方再执行
            lazy: true,
            scheduler: () => {
                if(options.flush === 'post') {
                    const p = Promise.resolve()
                    p.then(job)
                } else {
                    job()
                }
            }
        }
    )
    // 当immediate为true的时候立即执行job,从而触发回调
    if(options.immediate) {
        job()
    } else {
        oldValue = effectFn()
    }
    
// 省略代码.....
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Vue,可以使用watch来监视data数据的变化。当被监视的数据发生变化时,watch定义的相关方法会被调用。有两种方法来使用watch。 第一种方法是不需要配置项,只需一个handler。如下所示: ```javascript watch: { key: function(newVal, oldVal) { // 监听key属性的数据变化 }, "obj.key": function(newVal, oldVal) { // 监听obj.key属性的数据变化 } } ``` 其,key为所要监视的属性名,名称要与data的属性对应。newVal为当前属性改变后的值,oldVal为当前属性改变前的值。 第二种方法是需要配置项。如下所示: ```javascript watch: { key: { deep: false, handler: function(newVal, oldVal) { // 监听key属性的数据变化 } }, "obj.key": { deep: false, handler: function(newVal, oldVal) { // 监听obj.key属性的数据变化 } } } ``` 其,deep表示是否深度监听,默认为false。handler为处理函数,当数据发生变化时会调用该函数。 另外,还可以使用immediate选项来实现立即处理,即在进入页面时就触发watch方法。如下所示: ```javascript watch: { num: { handler: function(newVal, oldVal) { // 监听num属性的数据变化 }, immediate: true } } ``` 如果想要监听对象的所有属性,可以使用deep选项来进行深度监听。如下所示: ```javascript watch: { obj: { deep: true, handler: function() { // 监听obj对象所有属性的数据变化 } } } ``` 可以通过设置deep为true来实现对对象所有属性的监听。 总之,Vuewatch提供了多种方法来监听数据的变化,可以根据具体需求选择合适的方式来实现。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *3* [Vuewatch(监听)](https://blog.csdn.net/qq_44998582/article/details/122333475)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] - *2* [vuewatch的详解](https://blog.csdn.net/fu983531588/article/details/89454446)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值