「Vue3学习篇」-watch()

『引言』

watch更多的是观察的作用,无缓存性, 类似于某些数据的监听回调 ,每当监听的数据变化时,都会执行回调进行后续操作。

『回顾』

通过一个简单示例回忆一下,vue2中watch的使用。

『示例』

xml 复制代码
<template>
  <div>
    <div>Hello,大家好,我的名字叫:{{ info.name }}</div>
    <div>我的爱好有:{{ info.hobbies.join('、') }}</div>
    <div>很高兴认识大家!</div>
    <button @click="changeName">点击按钮修改信息</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      info:{
          name: 'pupu',
          hobbies: ['唱歌', '画画']
      } 
    }
  },

  methods: {
    changeName() {
        this.info.name = 'wnxx'
        this.info.hobbies = ['打羽毛球', '旅游']
    }
  },

  watch: {
    'info.name': {
      handler(newVal,oldVal){
        console.log('原来的值是:' + oldVal)
        console.log('改后的值是:' + newVal)
      },
      immediate: true,
      deep: true
    },
    'info.hobbies': {
      handler(newVal,oldVal){
        console.log('原来的值是:' + oldVal)
        console.log('改后的值是:' + newVal)
      },
      immediate: true,
      deep: true
    }, 
  }
}
</script>

『效果展示』

『watch()』

『定义』

【官方解释】 侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。

  • 详细信息

    watch() 默认是懒侦听的,即仅在侦听源发生变化时才执行回调函数。

    第一个参数是侦听器的。这个来源可以是以下几种:

    • 一个函数,返回一个值
    • 一个 ref
    • 一个响应式对象
    • ...或是由以上类型的值组成的数组

    第二个参数是在发生变化时要调用的回调函数。这个回调函数接受三个参数:新值、旧值,以及一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。

    当侦听多个来源时,回调函数接受两个数组,分别对应来源数组中的新值和旧值。

    第三个可选的参数是一个对象,支持以下这些选项:

    • immediate :在侦听器创建时立即触发回调。第一次调用时旧值是 undefined
    • deep :如果源是对象,强制深度遍历,以便在深层级变更时触发回调。参考深层侦听器
    • flush :调整回调函数的刷新时机。参考回调的刷新时机watchEffect()
    • onTrack / onTrigger :调试侦听器的依赖。参考调试侦听器

    watchEffect() 相比,watch() 使我们可以:

    • 懒执行副作用;
    • 更加明确是应该由哪个状态触发侦听器重新执行;
    • 可以访问所侦听状态的前一个值和当前值。

『用法』

watch(WatcherSource, Callback, [WatchOptions])

wacth(value,(newValue,oldValue)=>{}[,options])

一次监听多个响应式的值: wacth([value1,value2],([newValue1,newValue2],[oldValue1,oldvalue2])=>{}[,options])

『watch参数说明』:

  • WatcherSource: 想要监听的响应式数据。
  • Callback: 回调函数,参数(newValue, oldValue)。
  • [WatchOptions]:deep、immediate、flush可选。

『[WatchOptions]参数说明』:

  • deep:当需要对对象等引用类型数据进行深度监听时,设置deep: true,默认值是false。
  • immediate:默认是false,初始化的时候不执行回调函数。如果是true,watch会初始化的时候就会执行一次回调函数。
  • flush:控制回调函数的执行时机,。它可设置为 pre、post 或 sync。
    • pre:默认值,当监听的值发生变更时,优先执行回调函数(在dom更新之前执行)。
    • post:dom更新渲染完毕后,执行回调函数。
    • sync:一旦监听的值发生了变化,同步执行回调函数(建议少用)。

官网示例🌰

『注意』

watch在可配置为空时:

  • 运行的时候,不会立即执行。
  • 需要添加监听的属性。
  • 回调函数内会返回最新值和修改之前的值。
  • 配置项可补充 watch 特点上的不足。

『示例🌰:监听ref定义的数据』

xml 复制代码
<template>
  <div>
    <h1>人物简介</h1>
    <p>姓名:{{name}}</p>
    <p>年龄:{{age}}岁</p>
    <p>爱好:{{info.hobbies.join('、')}}</p>
    <p>地址:{{info.address.provice}} - {{info.address.city}}</p>
    <p>最喜欢的颜色:{{info.favoriteColor.coloeOne}} & {{ info.favoriteColor.colorTwo }}</p>
    <p>描述:{{info.description}}</p>
    <button @click="modifyInfo">
      修改数据
    </button>
  </div> 
</template>

<script setup>
import { ref,watch } from 'vue'
    
    const name = ref('pupu')
    const age = ref(10)
    const info = ref({
      hobbies: ['唱歌','画画'],
      address: {
          provice: '浙江省',
          city: '杭州市'
      },
      favoriteColor: {
        coloeOne: '绿色',
        colorTwo: '蓝色'
      },
      description: '一点也不可爱,不喜欢吃蜂蜜!',
    })

    const modifyInfo = () => {
        name.value = 'wnxx'
        age.value = 3 
        info.value.hobbies = ['打羽毛球','旅游']
        info.value.address.provice = '云南省'
        info.value.address.city = '丽江市'
        info.value.description = '非常的可爱,特别喜欢吃蜂蜜!'
        info.value.favoriteColor.coloeOne = '薄荷绿'
        info.value.favoriteColor.colorTwo = '天蓝色'   
    } 
    
    // 情况1:监听ref所定义的一个响应式数据
    watch(name, (newValue,oldValue) => {
      console.log(newValue, oldValue, 'name' )
    }),
    // 情况2:监听ref所定义的多个响应式数据
    watch([name,age],(newValue,oldValue) => {
      console.log( newValue, oldValue, 'name-age')
    }),
    // 情况3:监听ref所定义的一个引用类型响应式数据
    watch(info, (newValue,oldValue) => {
      console.log(newValue, oldValue, 'info引用类型' )
    }),
    // 情况4:针对监听ref所定义的一个引用类型响应式数据之一
    watch(info.value, (newValue,oldValue) => {
      console.log(newValue, oldValue, 'info.value' )
    }),
    // 情况5:针对监听ref所定义的一个引用类型响应式数据之二
    watch(info.value, (newValue,oldValue) => {
      console.log(newValue, oldValue, 'info深度监听无效' )
    },
    {deep: false}
    ),
    // 情况6:针对监听ref所定义的一个引用类型数据之三:直接深度监听
    watch(info, (newValue,oldValue) => {
      console.log(newValue, oldValue, 'info直接深度监听' )
    },
    {deep: true}
    )
    // 情况7:针对监听ref所定义的一个引用类型数据之四,深拷贝深度监听
    watch(() => ({...info.value}), (newValue,oldValue) => {
      console.log(newValue, oldValue, 'info深拷贝深度监听' )
    },
    {deep: true}
    )
</script>

『效果展示』

『代码解析』

上述代码中列举了几种使用ref监听不同数据的方式和情况。

  • 情况1:监听ref所定义的一个响应式数据,可以获取到新旧值。

  • 情况2:监听ref所定义的多个响应式数据,可以获取到新旧值,多个参数是数组,数组形式返回。

  • 情况3:监听ref所定义的一个引用类型响应式数据,不会获取到新旧值。监听引用类型数据的内部某一项发生变化,不会被监听到,watch中的代码不会执行,控制台也并未看到【'info引用类型'】的打印信息。

  • 情况4:针对监听ref所定义的一个引用类型响应式数据之一,使用【info.value】,可以获取到新值。【info.value】其实是一个对象的引用地址,新旧值拿到的值是一样的。

  • 情况5:针对监听ref所定义的一个引用类型响应式数据之二。如果写了.value那么它对Proxy的监听默认是深度监听,且无法关闭,配置deep:false无效。控制台打印信息和情况4一样。

  • 情况6:针对监听ref所定义的一个引用类型数据之三:直接深度监听。如果不写.value,写上deep:true,实现深度监听。但是深度监听的是这个引用数据类型自身,而不是其中的属性。这样只能获取到新值,而获取不到旧的值。

  • 情况7:针对监听ref所定义的一个引用类型数据之四,深拷贝深度监听。通过监听数据的getter函数,这样『newValue和oldValue』得到的对象是正确的。

提问 🚩:首先看到ref,就会想到模板语法中不用value,js中操作数据需要带上value。那为什么watch的时候没有写value❓🤔🤔

答案 📒:首先ref定义的数据,得到的是一个RefImpt:引入对象的实现对象的实例对象,简称引用对象。

然后,监听ref定义的基本类型的数据,不需要value

最后,监听ref定义的对象类型的数据,需要value,因为监听对象时,要对这个对象的Proxy对象生效,对象类型的数据经过ref函数加工会变成引用对象,而该对象的value属性才是这个对象的Proxy。所以如果需要监视Proxy对象中的数据则需要监视的是.value的结构。

『示例🌰:监听reactive定义的数据』

xml 复制代码
<template>
  <div>
    <h1>人物简介</h1>
    <p>姓名:{{person.name}}</p>
    <p>年龄:{{person.age}}岁</p>
    <p>爱好:{{info.hobbies.join('、')}}</p>
    <p>地址:{{info.address.provice}} - {{info.address.city}}</p>
    <p>最喜欢的颜色:{{info.favoriteColor.coloeOne}} & {{ info.favoriteColor.colorTwo }}</p>
    <p>描述:{{info.description}}</p>
    <button @click="modifyInfo">
      修改数据
    </button>
  </div> 
</template>

<script setup>
import { reactive, watch } from 'vue'
    
    const person = reactive({
      name: 'pupu',
      age: 10
    })
    const info = reactive({
      hobbies: ['唱歌','画画'],
      address: {
          provice: '浙江省',
          city: '杭州市'
      },
      favoriteColor: {
        coloeOne: '绿色',
        colorTwo: '蓝色'
      },
      description: '一点也不可爱,不喜欢吃蜂蜜!',
    })
    
    const modifyInfo = () => {
        person.name = 'wnxx'
        person.age = 3 
        info.hobbies = ['打羽毛球','旅游']
        info.address.provice = '云南省'
        info.address.city = '丽江市'
        info.favoriteColor.coloeOne = '薄荷绿'
        info.favoriteColor.colorTwo = '天蓝色'
        info.description = '非常的可爱,特别喜欢吃蜂蜜!'
        
    } 
    // 情况1:监听reactive所定义的一个响应式数据中的某个属性
    watch(()=>person.name, (newValue,oldValue) => {
      console.log(newValue, oldValue, 'name' )
    }),
    // 情况2:监听reactive所定义的一个响应式数据中的某些属性
    watch([()=>person.name,()=>person.age],(newValue,oldValue) => {
      console.log( newValue, oldValue, 'name-age')
    }),
    // 情况3:监听reactive所定义的引用类型响应式数据的某个属性
    watch(()=>info.hobbies, (newValue,oldValue) => {
      console.log(newValue, oldValue, '爱好' )
    }),
    // 情况4:监听reactive所定义的引用类型响应式数据的某些属性
    watch([()=>info.hobbies,()=>info.description], ([newHobby,newDescription],[oldHobby,oldDescription]) => {
      console.log(newHobby,newDescription,oldHobby,oldDescription, '爱好描述' )
    })
    // 情况5:针对监听reactive所定义的引用类型响应式数据的全部属性之一
    watch(info, (newValue,oldValue) => {
      console.log(newValue, oldValue, 'info引用类型' )
    }),
    // 情况6:针对监听reactive所定义的引用类型响应式数据的全部属性之二
    watch(() =>info, (newValue,oldValue) => {
      console.log(newValue, oldValue, 'info引用类型之二' )
    },
    {deep: true}
    ),
    // 情况7:只监听reactive所定义的引用类型响应式数据的子属性
    watch(() => ({...info}), (newValue,oldValue) => {
      console.log(newValue, oldValue, 'info子属性' )
    })
    // 情况8:监听reactive所定义的引用类型响应式数据里的某个对象的属性
    watch(() => info.address, (newValue,oldValue) => {
      console.log(newValue, oldValue, 'info中的地址' )
    },
    {deep: true}
    )
</script>

『效果展示』

『代码解析』

上述代码中列举了几种使用reactive监听不同数据的方式和情况。

  • 情况1:监听reactive所定义的一个响应式数据,可以获取到新旧值。第一个参数写成函数的形式才会生效。

  • 情况2:监听reactive所定义的多个响应式数据,可以获取到新旧值,第一个参数写成数组的形式,返回也是数组形式。

  • 情况3:监听reactive所定义的引用类型响应式数据的某个属性,可以获取到新旧值。

  • 情况4:监听reactive所定义的引用类型响应式数据的某些属性,可以获取到新旧值。

  • 情况5:针对监听reactive所定义的引用类型响应式数据的全部属性之一,只可以获取到新值,对象里面任何一个属性改变都会触发。

  • 情况6:针对监听reactive所定义的引用类型响应式数据的全部属性之二,添加{deep:true},对象里的属性都会监听到。

  • 情况7:只监听reactive所定义的引用类型响应式数据的子属性,可以获取到新旧值。

  • 情况8:监听reactive所定义的引用类型响应式数据里的某个对象的属性,只能获取到新值,需要添加{deep:true},监听才有效。

『watch源码』

『watch源码』

watch源码如下⬇️:

r 复制代码
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  if (__DEV__ && !isFunction(cb)) {
    warn(
      `\`watch(fn, options?)\` signature has been moved to a separate API. ` +
        `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
        `supports \`watch(source, cb, options?) signature.`
    )
  }
  return doWatch(source as any, cb, options)
}

『参数说明』

  • source: 监听的数据源

    source 的类型如下:

typescript 复制代码
export type WatchSource<T = any> = Ref<T> | ComputedRef<T>
| (() => T) type MultiWatchSources = 
(WatchSource<unknown>| object)[]

可以清晰的知道,数据源支持传入『单个的Ref、Computed响应式对象、一个返回相同泛型类型的函数』,以及source支持传入数组,方便能同时监听多个数据源。

  • cb: 回调函数

    cb的类型如下:

typescript 复制代码
export type WatchCallback<V = any, OV = any> 
= ( value: V, oldValue: OV, onInvalidate: 
InvalidateCbRegistrator ) => any

回调函数提供最新的value旧的value,以及onInvalidate函数用以清除副作用。

  • options: 监听配置

    options的类型如下:

typescript 复制代码
export interface WatchOptions<Immediate = boolean> 
extends WatchOptionsBase 
{ immediate?: Immediate deep?: boolean }

监听配置的参数有immediatedeep ,监听配置的类型 WatchOptions 继承了 WatchOptionsBase,可以传递 WatchOptionsBase 中的所有参数以控制副作用执行的行为。

『watch源码分析』

watch除了接受三个参数外,watch会返回一个停止监听函数。并且还会在开发环境下检测回调函数是否是函数类型,如果回调函数不是函数,就会报警。然后它会去调用doWatch这个函数,调用doWatch函数来实现相关逻辑。

doWatchwatch的核心,doWatch函数为了兼容各个api的逻辑源码,源代码比较长,若想阅读完整源码请戳这里

接下来一起看看doWatch函数。

『doWatch源码』

先看一下doWatch的签名。

javascript 复制代码
function doWatch( 
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null, 
{ immediate, deep, flush, onTrack, onTrigger }
: WatchOptions = EMPTY_OBJ): WatchStopHandle

『参数说明』

doWatch也会接收『source: 监听的数据源、cb: 回调函数、options: 监听配置』这三个参数,同样也有会返回一个停止监听函数。

『doWatch源码分析』

接着往下看。

scss 复制代码
 if (__DEV__ && !cb) {
    if (immediate !== undefined) {
      warn(
        `watch() "immediate" option is only respected when using the ` +
          `watch(source, callback, options?) signature.`
      )
    }
    if (deep !== undefined) {
      warn(
        `watch() "deep" option is only respected when using the ` +
          `watch(source, callback, options?) signature.`
      )
    }
  }

首先是在回调函数cb为null的情况下,对immediatedeep进行校验,immediatedeep不为undefined,就会警告immediatedeep只对有回调函数cbdoWatch签名有效。

其次声明了一个source参数不合法的警告函数。

typescript 复制代码
const warnInvalidSource = (s: unknown) => {
    warn(
      `Invalid watch source: `,
      s,
      `A watch source can only be a getter/effect function, a ref, ` +
        `a reactive object, or an array of these types.`
    )
  }

紧接着声明了一些变量:

ini 复制代码
  const instance =
    getCurrentScope() === currentInstance?.scope ? currentInstance : null
  // const instance = currentInstance
  let getter: () => any
  let forceTrigger = false
  let isMultiSource = false

『说明』:

  • instance:当前组件的实例,默认值currentInstancecurrentInstance是当前调用组件暴露出来的一个变量,方便该侦听器找到自己对应的组件。
  • getter:会当做副作用的函数参数传入,在初始化effect时使用。
  • forceTrigger:标识是否需要强制更新。
  • isMultiSource:标记传入的是单个数据源还是以数组形式传入的多个数据源。

然后判断source的类型,根据不同的类型重置『getter、forceTrigger、isMultiSource』的值。

那就具体看一下是如何处理不同类型source

『source的类型是ref』:

ini 复制代码
if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } 
  • getter 是个返回 source.value 的函数,访问 getter 函数会获取到 source.value 值,直接解包。
  • forceTrigger 标记会根据 source 是否是 shallowRef 来设置。

『source的类型是reactive』:

ini 复制代码
if (isReactive(source)) {
    getter = () => source
    deep = true
  }
  • getter 是个返回 source 的函数,访问 getter 函数直接返回 source,因为 reactive 的值不需要解包获取。
  • 由于 reactive 中往往有多个属性,所以会将 deep 设置为 true,这里可以看出从外部给 reactive 设置 deep 是无效的。

『source的类型是数组』:

scss 复制代码
if (isArray(source)) {
    isMultiSource = true
    forceTrigger = source.some(s => isReactive(s) || isShallow(s))
    getter = () =>
      source.map(s => {
        if (isRef(s)) {
          return s.value
        } else if (isReactive(s)) {
          return traverse(s)
        } else if (isFunction(s)) {
          return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
        } else {
          __DEV__ && warnInvalidSource(s)
        }
      })
  }
  • 将 isMultiSource 设置为 true。
  • forceTrigger 会根据数组中是否存在 reactive 类型的数据来判断。
  • getter 是一个数组形式,会遍历source,针对不同类型的source,做相应的处理, 就会得到各个元素的单个 getter 结果。

『source的类型是function』:

scss 复制代码
if (isFunction(source)) {
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
      // no cb -> simple effect
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithAsyncErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onCleanup]
        )
      }
    }
  }
  • 如果有回调函数cb

    • getter 函数中会执行 source , source 会通过 callWithErrorHandling 函数执行, callWithErrorHandling 函数中会处理 source 执行过程中出现的错误。

『callWithErrorHandling源码』

typescript 复制代码
export function callWithErrorHandling(
  fn: Function,
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
) {
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

『参数说明』:

  1. fn:要进行错误处理的函数
  2. instance:当前组件的实例
  3. type:fn执行过程中出现的错误类型
  4. args:是fn函数的参数
  • 如果没有回调函数cb,那么此时就是 watchEffect api 的场景了。

    • 此时会为 watchEffect 设置 getter 函数,getter 函数逻辑如下:

      • 如果组件实例已经卸载,则不执行,直接返回
      • 否则判断 cleanup ,cleanup 是在 watchEffect 中通过 onCleanup 注册的清理函数,存在 cleanup ,执行 cleanup 清除依赖
      • 执行 source 函数,并返回执行结果。 source 会 callWithAsyncErrorHandling 包装,callWithAsyncErrorHandling 会处理source 执行过程中出现的错误,会处理异步错误。

『callWithAsyncErrorHandling源码』

typescript 复制代码
export function callWithAsyncErrorHandling(
  fn: Function | Function[],
  instance: ComponentInternalInstance | null,
  type: ErrorTypes,
  args?: unknown[]
): any[] {
  if (isFunction(fn)) {
    const res = callWithErrorHandling(fn, instance, type, args)
    if (res && isPromise(res)) {
      res.catch(err => {
        handleError(err, instance, type)
      })
    }
    return res
  }

  const values = []
  for (let i = 0; i < fn.length; i++) {
    values.push(callWithAsyncErrorHandling(fn[i], instance, type, args))
  }
  return values
}

『参数说明』: callWithAsyncErrorHandling 与 callWithErrorHandling 不同的是,callWithAsyncErrorHandling 可以接受一个fn数组。

『source的类型不是以上情况』:

ini 复制代码
{
   getter = NOOP
    __DEV__ && warnInvalidSource(source)
}
  • 如果 source 不是以上的情况,则将 getter 设置为空函数,并且报出 source 不合法的警告⚠️。

『Vue2的兼容处理』

doWatch函数中,还做了一层Vue2的兼容处理,主要是通过对getter函数进行了一层重载,并对getter函数返回的value进行了深度递归遍历。

scss 复制代码
if (__COMPAT__ && cb && !deep) {
    const baseGetter = getter
    getter = () => {
      const val = baseGetter()
      if (
        isArray(val) &&
        checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)
      ) {
        traverse(val)
      }
      return val
    }
  }
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

当有回调,并且 deep 选项为 true 时,将使用 traverse 来包裹 getter 函数,对数据源中的每个属性递归遍历进行监听。

『traverse源码』

scss 复制代码
export function traverse(value: unknown, seen?: Set<unknown>) {
 // 如果value不是对象或value不可被转为代理(经过markRaw处理),直接return value
 if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
 return value
 }
 // sean用于暂存访问过的属性,防止出现循环引用的问题
 seen = seen || new Set()
 // 如果seen中已经存在了value,意味着value中存在循环引用的情况,这时return value
 if (seen.has(value)) {
 return value
 }
 // 添加value到seen中
 seen.add(value)
 // Ref,递归访问value.value
 if (isRef(value)) {
 traverse(value.value, seen)
 } else if (isArray(value)) {
 // 如果是数组,遍历数组并调用traverse递归访问元素内的属性
 for (let i = 0; i < value.length; i++) {
 traverse(value[i], seen)
 }
 } else if (isSet(value) || isMap(value)) {
 // 如果是Set或Map,调用traverse递归访问集合中的值如果是原始对象,调用traverse递归访问value中的属性
 value.forEach((v: any) => {
 traverse(v, seen)
 })
 } else if (isPlainObject(value)) {
 // 如果是原始对象,调用traverse递归访问value中的属性
 for (const key in value) {
 traverse((value as any)[key], seen)
 }
 }
 return value
}

traverse函数的作用是对getter的返回值做一个递归遍历,将遍历到的值添加到一个叫做seen的集合中,seen用于防止循环引用问题。seen中的数据就是当前watch要侦听的那些数据。

经过上面一系列的过程之后,『getter函数、forceTrigger、isMultiSource』就已被确定下来了。

之后声明两个变量『cleanuponCleanup』用于清除副作用,以及SSR检测。

javascript 复制代码
let cleanup: () => void
 let onCleanup: OnCleanup = (fn: () => void) => {
 cleanup = effect.onStop = () => {
 callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
 }
 }
// in SSR there is no need to setup an actual effect, and it should be noop
// unless it's eager
if (__SSR__ && isInSSRComponentSetup) {
 // we will also not call the invalidate callback (+ runner is not set up)
 onCleanup = NOOP
 if (!cb) {
 getter()
 } else if (immediate) {
 callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
 getter(),
 isMultiSource ? [] : undefined,
 onCleanup
 ])
 }
 return NOOP
}

onCleanup 会作为参数传递给 watchEffect 中的 effect 函数。当onCleanup 执行时,会将它的参数通过 callWithErrorHandling 封装赋给 cleanup 及 effect.onStop。

然后声明了一个oldValue和job变量。

typescript 复制代码
let oldValue: any = isMultiSource
    ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
    : INITIAL_WATCHER_VALUE
  // job为当前watch要做的工作,后续通过调度器来处理
  const job: SchedulerJob = () => {
    if (!effect.active) {
      return
    }
    // cb存在,说明是watch,而不是watchEffect
    if (cb) {
      // watch(source, cb)
      const newValue = effect.run()
      if (
        deep ||
        forceTrigger ||
        (isMultiSource
          ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i]))
          : hasChanged(newValue, oldValue)) ||
         // 兼容2.x
        (__COMPAT__ &&
          isArray(newValue) &&
          isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
      ) {
        // cleanup before running cb again
        if (cleanup) {
          cleanup()
        }
        // 用异步异常处理程序包裹了一层来调用cb
        callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
          newValue,
          // pass undefined as the old value when it's changed for the first time
          oldValue === INITIAL_WATCHER_VALUE
            ? undefined
            : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE
            ? []
            : oldValue,
          onCleanup
        ])
        // cb执行完成,当前的新值就变成了旧值
        oldValue = newValue
      }
    } else {
      // cb不存在,则是watchEffect
      // watchEffect
      effect.run()
    }
  }
// 设置allowRecurse,让调度器知道它可以自己触发
job.allowRecurse = !!cb

『说明』:

  • 如果是多数据源oldValue是个数组,否则是个对象。
  • job函数的作用是触发回调函数cb(watch) 或执行 effect.run(watchEffect)。

job函数中会判断 effect 的激活状态,如果当前effect不在 active 状态,说明没有触发该 effect 的响应式变化,则直接返回。

然后判断如果存在回调函数cb,调用 effect.run 得到新的值 newValue。

下一步就是触发回调函数cb,下面是触发cb需要的条件:

  1. 深度监听deep===true
  2. 强制触发forceTrigger===true
  3. 取到的新值和旧值是否相同,如果有变化则进入分支。如果多数据源,newValue 中存在与 oldValue 中的值不相同的项(利用Object.is判断);如果不是多数据源,newValue 与 oldValue 不相同。
  4. 开启了 vue2 兼容模式,并且 newValue 是个数组,并且开启 WATCH_ARRAY

只要满足以上任意一个就可以触发回调函数cb,在触发回调函数cb之前会先调用 cleanup 函数。执行完回调函数cb后,需要将 newValue 赋值给 oldValue。

如果不存在回调函数cb,那么直接调用effect.run即可。

watch的分支出现了effect,但是这个分支并没有effect,原来是由之前取得的getter来创建的effect

接下来还定义了调度器scheduler,在 scheduler 中会根据 flush 的不同决定 job 的触发时机。(由于调度器scheduler被混合在effect里,就会影响newValue的获取,所以也就会影响cb的调用时机)

『说明』:

  • sync:同步执行,直接将 job 赋值给 scheduler,这样这个调度器函数就会直接执行。
  • pre:默认值是pre,表示组件更新前执行,调度器会区分组件是否已经挂载,副作用第一次调用时必须是在组件挂载之前,而挂载后则会被推入一个优先执行时机的队列中。
  • post:组件更新后执行,需要延迟执行时,将 job 传入 queuePostRenderEffect 中,这样 job 会被添加进一个延迟执行的队列中,这个队列会在组件被挂载后、更新的生命周期中执行。
typescript 复制代码
    let scheduler: EffectScheduler
    // 根据flush的值来创建不同的调度器
    if (flush === 'sync') {
     scheduler = job as any // the scheduler function gets called directly
    } else if (flush === 'post') {
     scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
    } else {
     // default: 'pre'
     scheduler = () => queuePreFlushCb(job)
    }

现在 getter 和 scheduler 完成。为 watch 创建 effect 。这里还有调试用的 onTrack 和 onTrigger,当收集依赖和触发更新时做一些操作。

ini 复制代码
    const effect = new ReactiveEffect(getter, scheduler)
    if (__DEV__) {
     effect.onTrack = onTrack
     effect.onTrigger = onTrigger
    }

effect 实例创建完,就是侦听器的初始化调用副作用。第一次开始执行副作用函数,会有不同的情况。

『回调函数cb存在』

  • 如果immediate为true。将直接调用一次job,触发回调函数cbjob是包裹了一层错误处理程序来调用回调函cb,所以immediate能让回调函数cb立即触发一次。
  • 否则执行effect.run()进行依赖的收集,并将返回值赋值给 oldValue。

『flush === 'post'』

  • 没有cb,是watchEffect,副作用的时机在组件更新之后,用queuePostRenderEffect包裹一层,将effect.run()推入一个延迟队列中,来调整时机。

『其他情况』

  • 是 watchEffect,副作用的时机在组件更新之前,直接执行一次effect.run()。
scss 复制代码
    // initial run
    // 有cb,是 watch
    if (cb) {
     if (immediate) {
     job()
     } else {
     // 获取一下当前的值作为旧值
     oldValue = effect.run()
     }
    } else if (flush === 'post') {
     queuePostRenderEffect(
     effect.run.bind(effect),
     instance && instance.suspense
     )
    } else {
     effect.run()
    }

最后的最后返回一个函数,这个函数的作用是停止watch对数据源的监听。

在函数内部执行 effect.stop 来达到停止监听的作用,如果存在组件实例,并且组件示例中存在effectScope,那么移除当前实例作用域下的当前effect。

scss 复制代码
return () => {
   effect.stop()
   if (instance && instance.scope) {
   remove(instance.scope.effects!, effect)
   }
}

以上就是watch的源码了。

『总结流程』

graph TD watch --> 1.判断若没有cb则告警 --> 2.尾调用doWatch,之后的操作都在doWatch里进行
graph TD doWatch --> 3.判断没有cb时,若设置了deep或immediate则告警 3.判断没有cb时,若设置了deep或immediate则告警 --> 4.根据source的类型得到getter 4.根据source的类型得到getter --> 5.如果cb存在且deep为真,则对getter进行递归遍历 5.如果cb存在且deep为真,则对getter进行递归遍历 --> 6.获取oldValue,声明job函数,在job内部获取newValue并使用callWithAsyncErrorHandling来调用cb 6.获取oldValue,声明job函数,在job内部获取newValue并使用callWithAsyncErrorHandling来调用cb --> 7.根据post的值定义的调度器scheduler 7.根据post的值定义的调度器scheduler --> 8.根据getter和scheduler创建effect 8.根据getter和scheduler创建effect --> 9.初始化侦听器,如果有cb且immediate为真值,则立即调用job函数,相当于调用自己写的cb;如果immediate为假值,则只调用effect.run来初始化oldValue 9.初始化侦听器,如果有cb且immediate为真值,则立即调用job函数,相当于调用自己写的cb;如果immediate为假值,则只调用effect.run来初始化oldValue --> 10.返回一个函数,内部通过effect.stop来实现停止监听
graph TD watch --> 11.接收到doWatch返回的函数,并返回给外部使用
相关推荐
工业互联网专业30 分钟前
毕业设计选题:基于ssm+vue+uniapp的校园水电费管理小程序
vue.js·小程序·uni-app·毕业设计·ssm·源码·课程设计
豆豆43 分钟前
为什么用PageAdmin CMS建设网站?
服务器·开发语言·前端·php·软件构建
计算机学姐1 小时前
基于SpringBoot+Vue的在线投票系统
java·vue.js·spring boot·后端·学习·intellij-idea·mybatis
twins35202 小时前
解决Vue应用中遇到路由刷新后出现 404 错误
前端·javascript·vue.js
qiyi.sky2 小时前
JavaWeb——Vue组件库Element(3/6):常见组件:Dialog对话框、Form表单(介绍、使用、实际效果)
前端·javascript·vue.js
煸橙干儿~~2 小时前
分析JS Crash(进程崩溃)
java·前端·javascript
安冬的码畜日常2 小时前
【D3.js in Action 3 精译_027】3.4 让 D3 数据适应屏幕(下)—— D3 分段比例尺的用法
前端·javascript·信息可视化·数据可视化·d3.js·d3比例尺·分段比例尺
杨荧3 小时前
【JAVA开源】基于Vue和SpringBoot的洗衣店订单管理系统
java·开发语言·vue.js·spring boot·spring cloud·开源
l1x1n03 小时前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。3 小时前
案例-任务清单
前端·javascript·css