vue中的watch是怎么实现的?

47 阅读2分钟

vue中实现watch都是基于核心模块reactiveeffect,当响应式数据发生变更,对应effect会执行

实现常见的两种使用方式

直接使用

这种性能比较差一些,需要递归取值。

const state = reactive({name:"jym"})
watch(state,()=>{
    console.log('数据变化')
})

函数返回

const state = reactive({name:"jym"})
watch(state,()=>{
    console.log('数据变化')
})

实现

参数处理

import { isReactive } from "vue"

const traverse = (target,seen = new Set())=>{

     if (seen.has(target)) return;
     
    if(target !== null && typeof target === 'object'){
      seen.add(target);
      for (const key in value) {
        traverse(target[key], seen); // 递归遍历
      }
    }
    return target
}

function watch(source,cb){
    let getter;
    if(isReactive(source)){
        getter = ()=> traverse(source)
    } else if(typeof source === 'function') {
        getter = source
    }
}

使用effect来处理数据变更回调

export function watch(source, cb) {
    
  ...
   
  let oldValue;
  // 创建一个effect,对应wacth
  let outerCb = () => {
      let newValue = effect.run();
      cb(newValue, oldValue);
      oldValue = newValue
    }
  const effect = new ReactiveEffect(getter, outerCb);
  oldValue = effect.run() // 会执行getter,并且返回getter的返回值
}

思路梳理

  • 响应式数据,在取值的时候通过proxy代理拦截会发生依赖收集,哪个effect使用了该数据,那么对应的依赖dep就会收集这个effect
  • 当数据触发更新,会找到该数据对应属性的dep(里面存储所有依赖该属性的effect)取出所有effect挨个执行一次。
  • watch创建其对应的effect,让里面触发取值行为的数据都收集到此effect
  • 在数据变更时,会执行outerCb,里面包含用户传递进来的cb
  • 传递新老数据进去即可

问题:参数处理,为啥要递归遍历对象呢?

用户传递进来无论是对象,还是函数都会被包装成函数。那这样存在一个问题,传递进来的对象,在effect执行的时候。这个响应式对象并没有取值,也不会触发属性拦截器里面的依赖收集。这样就算state里面的数据变化,当前watch对应的effect并不会执行。所以需要在函数里面。

但是当我们遍历对象,发生取值行为,那么响应式数据对象,就会收集当前的依赖,在数据发生变更的时候,再次执行effect.run。即执行cb函数