vue3中的watch原理你了解多少

watch api用法

1.侦听getter函数

scss 复制代码
const text = reactive({
    a: 1
})
watch(() => text.a,(newValue,oldValue)=>{
    //当text.a发生变化时,会触发此回调函数
    console.log('oldValue: ', oldValue);
    console.log('newValue: ', newValue);
});

2.侦听响应式对象

arduino 复制代码
const text = ref(0)    
watch(text,(newValue,oldValue)=> {
    //当text.value发生变化时,会触发此回调函数
    console.log('oldValue: ', oldValue); 
    console.log('newValue: ', newValue);
})

3.什么是侦听器

当侦听的对象或函数发生了变化则自动执行某个回调函数,与副作用effect函数很像,那它的内部是否依赖effect?

watch Api的具体实现:

r 复制代码
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)
}

从源码中看到,watch Api内部调用了doWatch函数,判断cb是否是一个函数,如果不是,则使用effect APi

1.标准化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.`
    )
  }
scss 复制代码
function traverse(value: unknown, seen?: Set<unknown>) {
  if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
    return value
  }
  seen = seen || new Set()
  if (seen.has(value)) {
    return value
  }
  seen.add(value)
  if (isRef(value)) {
    traverse(value.value, seen)
  } else if (isArray(value)) {
    for (let i = 0; i < value.length; i++) {
      traverse(value[i], seen)
    }
  } else if (isSet(value) || isMap(value)) {
    value.forEach((v: any) => {
      traverse(v, seen)
    })
  } else if (isPlainObject(value)) {
    for (const key in value) {
      traverse((value as any)[key], seen)
    }
  }
  return value
}

traverse函数使用递归的方式访问value的子属性,因为deep属于watcher的一个配置选项。

2.cb参数

在回调函数中,会提供最新的 value、旧 value,以及 onCleanup 函数用以清除副作用。

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

3.options的创建

typescript 复制代码
export interface WatchOptionsBase extends DebuggerOptions {
  flush?: 'pre' | 'post' | 'sync'
}

export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
  immediate?: Immediate
  deep?: boolean
}

options 的类型 WatchOptions 继承了 WatchOptionsBase,这也就是 watch 除了 immediate 和 deep 这两个特有的参数外,还可以传递 WatchOptionsBase 中的所有参数以控制副作用执行的行为。

Options 中的 flush 决定了 watcher 的执行时机:

ini 复制代码
 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'
    job.pre = true
    if (instance) job.id = instance.uid
    scheduler = () => queueJob(job)
  }

当flush为 sync 的时,表示它是一个同步 watcher,即当数据变化时同步执行回调函数。

当flush为 pre的时,回调函数通过 queueJob 的方式在组件更新之前执行。如果组件还没挂载,则同步执行确保回调函数在组件挂载之前执行。

如果没设置flush,回调函数通过 queuePostRenderEffect 的方式在组件更新之后执行

doWatch函数

scss 复制代码
if (isRef(source)) {
    getter = () => source.value
    forceTrigger = isShallow(source)
  } else if (isReactive(source)) {
    getter = () => source
    deep = true
  } else 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)
        }
      })
  } else 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]
        )
      }
    }
  } else {
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }

source标准化根据source的类型划分:

如果source是ref对象,则创建一个访问source.value的getter函数。

如果source是reactive对象,则创建一个访问source的getter函数,并设置deep为true。

如果source是函数,则进一步判断cb是否存在,对于watch Api来说,cb一定存在,且是一个回调函数,getter就是一个简单的对source函数封装的函数。

如果source不满足上述条件,则在非生产环境下报警告,提示source类型不合法。

当deep为true时,会用traverse函数把getter再包装一层。

ini 复制代码
if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
}

定义清除副作用函数

javascript 复制代码
// 清除副作用函数
let cleanup: () => void
let onCleanup: OnCleanup = (fn: () => void) => {
  cleanup = effect.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}

总结

  • watch 作用是对传入的某个或多个值的变化进行监听;触发时会返回新值和老值;也就是说第一次不会执行,只有变化时才会重新执行
  • watchEffect 是传入一个立即执行函数,所以默认第一次也会执行一次;不需要传入监听内容,会自动收集函数内的数据源作为依赖,在依赖变化的时候又会重新执行该函数,如果没有依赖就不会执行;而且不会返回变化前后的新值和老值

watchImmediate也可以立即执行

相关推荐
神夜大侠几秒前
VUE 实现公告无缝循环滚动
前端·javascript·vue.js
明辉光焱3 分钟前
【Electron】Electron Forge如何支持Element plus?
前端·javascript·vue.js·electron·node.js
柯南二号36 分钟前
HarmonyOS ArkTS 下拉列表组件
前端·javascript·数据库·harmonyos·arkts
wyy729337 分钟前
v-html 富文本中图片使用element-ui image-viewer组件实现预览,并且阻止滚动条
前端·ui·html
前端郭德纲1 小时前
ES6的Iterator 和 for...of 循环
前端·ecmascript·es6
究极无敌暴龙战神X1 小时前
前端学习之ES6+
开发语言·javascript·ecmascript
王解1 小时前
【模块化大作战】Webpack如何搞定CommonJS与ES6混战(3)
前端·webpack·es6
欲游山河十万里1 小时前
(02)ES6教程——Map、Set、Reflect、Proxy、字符串、数值、对象、数组、函数
前端·ecmascript·es6
明辉光焱1 小时前
【ES6】ES6中,如何实现桥接模式?
前端·javascript·es6·桥接模式
PyAIGCMaster1 小时前
python环境中,敏感数据的存储与读取问题解决方案
服务器·前端·python