Vue3源码梳理:watch监听函数的核心实现

vue3.0watch监听属性 (6)一个特殊情况,当需要监听的是对象里面嵌套的对象中的数据时,就需要加deep:true,这个时候设置deep:true是有效的。(4):情况四:监听reactive定义的一个响应式对象中的某一个属性,是需要将监听的对象以函数的方式展示出来。(5)情况五:监听reactive定义的一个响应式对象中的多个属性,也是利用数组的方式展示出来。(2).情况二:监视ref定义的多个响应式数据(vue3.0可以调用n次watch函数)(3)情况三:监听reactive定义的一个响应式对象中的全部数据。 阅读详情

概述

  • watch 可以监听响应式数据的变化,从而触发指定的函数
  • watch 函数监听三个参数
    • 监听的响应式对象
    • 回调函数 cb
    • 配置对象 options
      • imediate: 初始化完成后会被立刻触发一次
      • deep: 深度监听

Debug Watch 源码

  • 测试示例: watch.html,测试vue源码的watch函数

    <script src='../../dist/vue.global.js'></script>
    
    <body>
      <div id='app'></div>
    </body>
    
    <script>
      const { reactive, watch } = Vue
      const obj = reactive({
        name: '张三'
      })
      watch(obj, (val, oldVal) => {
        console.log('watch ...')
        console.log(val)
      })
      const timer = setTimeout(() => {
        clearTimeout(timer)
        obj.name = '李四'
      }, 2000)
    </script>
    
  • 进入 watch 函数,在源码 packages/runtime-core/src/apiWatch.ts 中

    // implementation
    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)
    }
    
  • 从这里可以看出,它本质上是一个 doWatch 函数的触发

    function doWatch(
      source: WatchSource | WatchSource[] | WatchEffect | object,
      cb: WatchCallback | null,
      // 这个参数解析了options对象
      { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
    ): WatchStopHandle {
      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.`
          )
        }
      }
    
      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.`
        )
      }
      // 这里引用一下 currentInstance
      const instance = currentInstance
      let getter: () => any
      let forceTrigger = false
      let isMultiSource = false
      // 注意这里,暂不会执行
      if (isRef(source)) {
        getter = () => source.value // ref 数据访问需要使用 .value的方式
        forceTrigger = isShallow(source)
      } else if (isReactive(source)) {
        // 注意这里会执行,因为我们定义的就是 reactive 响应式数据
        getter = () => source // 这里响应式数据,getter 直接被赋值
        deep = true // 这里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)
      }
    
      // 2.x array mutation watch compat
      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()) // 这里做一层包装,先不管
      }
    
      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
      }
      // 这里有一个三元运算,会走到 INITIAL_WATCHER_VALUE 这里
      let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE
      // 核心的job函数
      const job: SchedulerJob = () => {
        if (!effect.active) {
          return
        }
        if (cb) {
          // watch(source, cb)
          const newValue = effect.run()
          if (
            deep ||
            forceTrigger ||
            (isMultiSource
              ? (newValue as any[]).some((v, i) =>
                  hasChanged(v, (oldValue as any[])[i])
                )
              : hasChanged(newValue, oldValue)) ||
            (__COMPAT__ &&
              isArray(newValue) &&
              isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
          ) {
            // cleanup before running cb again
            if (cleanup) {
              cleanup()
            }
            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 : oldValue,
              onCleanup
            ])
            oldValue = newValue
          }
        } else {
          // watchEffect
          effect.run()
        }
      }
    
      // important: mark the job as a watcher callback so that scheduler knows
      // it is allowed to self-trigger (#1727)
      job.allowRecurse = !!cb // 这里看下 issue #1727
      // 这里声明调度器
      let scheduler: EffectScheduler
      if (flush === 'sync') {
        scheduler = job as any // the scheduler function gets called directly
      } else if (flush === 'post') {
        scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
      } else {
        // 此时调度器变成一个回调函数,包装了 job 函数
        // default: 'pre'
        scheduler = () => queuePreFlushCb(job)
      }
      // 这里同样走了老套路,传入 getter 和 调度器
      const effect = new ReactiveEffect(getter, scheduler)
    
      if (__DEV__) {
        effect.onTrack = onTrack
        effect.onTrigger = onTrigger
      }
      // 这里开始最核心的最后一块逻辑
      // initial run
      if (cb) {
        // 这里读取配置
        if (immediate) {
          job() // 这里可以看出job的执行会代表watch函数的自动触发
        } else {
          // 这里会执行, 执行run函数中的 fn 函数
          oldValue = effect.run()
        }
      } else if (flush === 'post') {
        queuePostRenderEffect(
          effect.run.bind(effect),
          instance && instance.suspense
        )
      } else {
        effect.run()
      }
      // 最终会return这个函数
      return () => {
        effect.stop() // 停止effect监听
        if (instance && instance.scope) {
          remove(instance.scope.effects!, effect)
        }
      }
    }
    
  • 这个watch是比较复杂的,里面核心的几个点:

    • scheduler 调度器和我们的effect实例两者之间相互作用
    • 一旦effect触发了 scheduler ,就会调用 queuePreFlushCb
    • 同时,我们知道,一旦job函数执行,因为回调cb的传递,watch函数必定会被触发
  • 当watch函数执行完毕,2s之后,测试代码中会触发 setter 行为

    const timer = setTimeout(() => {
       clearTimeout(timer)
       obj.name = '李四'
     }, 2000)
    
  • setter行为的触发本质上是之前收集的依赖被触发,也就是之前分析过的

    function triggerEffect(
      effect: ReactiveEffect,
      debuggerEventExtraInfo?: DebuggerEventExtraInfo
    ) {
      if (effect !== activeEffect || effect.allowRecurse) {
        if (__DEV__ && effect.onTrigger) {
          effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
        }
        if (effect.scheduler) {
          effect.scheduler()
        } else {
          effect.run()
        }
      }
    }
    
    • triggerEffects() 后续一系列的行为,因为有调度器的存在,所有会执行
      • effect.scheduler()
  • 这个调度器就是watch中的

    • scheduler = () => queuePreFlushCb(job)
    • 好,我们看下进入 queuePreFlushCb 函数
  • 这个函数 在 scheduler.ts 中

    // 注意,这里的 cb 参数,就是我们的核心job函数
    export function queuePreFlushCb(cb: SchedulerJob) {
      queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
    }
    
  • 其本质还是 queueCb 函数,进入

    function queueCb(
      cb: SchedulerJobs,
      activeQueue: SchedulerJob[] | null,
      pendingQueue: SchedulerJob[],
      index: number
    ) {
      if (!isArray(cb)) {
        if (
          !activeQueue ||
          !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index)
        ) {
          // 这里会执行,
          pendingQueue.push(cb)
        }
      } else {
        // if cb is an array, it is a component lifecycle hook which can only be
        // triggered by a job, which is already deduped in the main queue, so
        // we can skip duplicate check here to improve perf
        pendingQueue.push(...cb)
      }
      // 这里进入
      queueFlush()
    }
    
  • 进入 queueFlush

    function queueFlush() {
      // 判断状态,这里会进入
      if (!isFlushing && !isFlushPending) {
        isFlushPending = true
        currentFlushPromise = resolvedPromise.then(flushJobs)
      }
    }
    
  • 注意这里的 resolvedPromise 的定义,它实际就是Promise的resolve 函数

    const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
    
  • 也就是说通过异步微任务来处理队列任务,这里通过then函数执行 flushJobs,这个是最后的异步执行函数,会等待所有同步任务执行完成后立即被触发, 进入它

    function flushJobs(seen?: CountMap) {
      isFlushPending = false
      isFlushing = true
      if (__DEV__) {
        seen = seen || new Map()
      }
      // 这里会执行
      flushPreFlushCbs(seen)
    
      // Sort queue before flush.
      // This ensures that:
      // 1. Components are updated from parent to child. (because parent is always
      //    created before the child so its render effect will have smaller
      //    priority number)
      // 2. If a component is unmounted during a parent component's update,
      //    its update can be skipped.
      queue.sort((a, b) => getId(a) - getId(b))
    
      // conditional usage of checkRecursiveUpdate must be determined out of
      // try ... catch block since Rollup by default de-optimizes treeshaking
      // inside try-catch. This can leave all warning code unshaked. Although
      // they would get eventually shaken by a minifier like terser, some minifiers
      // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
      const check = __DEV__
        ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
        : NOOP
    
      try {
        for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
          const job = queue[flushIndex]
          if (job && job.active !== false) {
            if (__DEV__ && check(job)) {
              continue
            }
            // console.log(`running:`, job.id)
            callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
          }
        }
      } finally {
        flushIndex = 0
        queue.length = 0
    
        flushPostFlushCbs(seen)
    
        isFlushing = false
        currentFlushPromise = null
        // some postFlushCb queued jobs!
        // keep flushing until it drains.
        if (
          queue.length ||
          pendingPreFlushCbs.length ||
          pendingPostFlushCbs.length
        ) {
          flushJobs(seen)
        }
      }
    }
    
  • 进入内部的 flushPreFlushCbs 函数

    // 这个函数通过任务队列的形式,触发job函数
    export function flushPreFlushCbs(
      seen?: CountMap,
      parentJob: SchedulerJob | null = null
    ) {
      // 检测是否有数据,这里是之前的job函数
      if (pendingPreFlushCbs.length) {
        currentPreFlushParentJob = parentJob
        // 这里 activePreFlushCbs 取代了 pendingPreFlushCbs
        activePreFlushCbs = [...new Set(pendingPreFlushCbs)] // 去重
        pendingPreFlushCbs.length = 0 // 这里清空自己,保证下次不会被触发
        if (__DEV__) {
          seen = seen || new Map()
        }
        for (
          preFlushIndex = 0;
          preFlushIndex < activePreFlushCbs.length;
          preFlushIndex++
        ) {
          if (
            __DEV__ &&
            checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
          ) {
            continue
          }
          // 这里会执行,也就是job函数会被触发
          activePreFlushCbs[preFlushIndex]()
        }
        activePreFlushCbs = null
        preFlushIndex = 0
        currentPreFlushParentJob = null
        // recursively flush until it drains
        flushPreFlushCbs(seen, parentJob)
      }
    }
    
  • 既然上述函数是为了处理队列中的job函数,我们再回到job函数中看看,位置:apiWatch.ts 中,doWatch 函数中的一段逻辑

    const job: SchedulerJob = () => {
        if (!effect.active) {
          return
        }
        // 这里会执行
        if (cb) {
          // watch(source, cb)
          const newValue = effect.run() // 运行run函数,产生了 newValue, 这里是我们的响应性对象
          if (
            deep ||
            forceTrigger ||
            (isMultiSource
              ? (newValue as any[]).some((v, i) =>
                  hasChanged(v, (oldValue as any[])[i])
                )
              : hasChanged(newValue, oldValue)) ||
            (__COMPAT__ &&
              isArray(newValue) &&
              isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance))
          ) {
            // cleanup before running cb again
            if (cleanup) {
              cleanup()
            }
            // 这里是一个通用的 try catch
            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 : oldValue,
              onCleanup
            ])
            // 更新挂载 oldValue 变量
            oldValue = newValue
          }
        } else {
          // watchEffect
          effect.run()
        }
      }
    
  • 我们看一下这个 通用的 callWithAsyncErrorHandling 函数

    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 之上,目前是拿过来看看
    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
    }
    
  • 也就是说,vue内部,通过一个 callWithAsyncErrorHandling 函数来统一处理 所有可能出现的代码,统一进行try catch的操作

  • 从本质上来说 watch函数触发,是基于job函数触发来执行 effect.run 函数的调用,也就是会执行 我们的 fn 函数,所以,只要job被触发,watch就会被触发,当run() 执行了,就会拿到新的值

关于 scheduler 调度器的探究

  • 对于watch和computed都用到了我们的调度器,scheduler
  • 调度器包含两块内容
    • 懒执行
    • 调度器本身

1 )懒执行

  • 在effect.ts中的 effect 函数内部,有懒执行的相关逻辑
    if (!options || !options.lazy) {
      _effect.run()
    }
    
    • 这里可以看出懒执行为false时,run函数也会立即被执行,在watch里的懒执行是true,所以watch不会立即执行run函数

2 ) 调度器

  • 调度器主要分成两部分

    • 控制执行逻辑
    • 控制执行顺序
  • 测试调度器,新建测试用例 scheduler.html

    <script src='../../dist/vue.global.js'></script>
    
    <body>
      <div id='app'></div>
    </body>
    
    <script>
      const { reactive, effect } = Vue
      const obj = reactive({
        count: 1
      })
      effect(() => {
        console.log(obj.count)
      })
      obj.count = 2
      console.log('over')
    </script>
    
    • 这里的执行打印顺序是:1, 2, over

2.1 ) 控制代码执行顺序

  • 我们想,修改这个打印执行顺序,想要的顺序是: 1, over, 2

    • 这个意思是:effect函数的执行是懒的,是后发的,是异步的
  • 这时候,我们就需要用到的是调度器,我们修改下测试示例程序

      const { reactive, effect } = Vue
      const obj = reactive({
        count: 1
      })
      effect(() => {
        console.log(obj.count)
      }, {
        scheduler() {
          setTimeout(() => {
            console.log(obj.count)
          })
        }
      })
      obj.count = 2
      console.log('over')
    
    • 这时候的输出顺序就是: 1, over, 2
  • 在 effect.ts 中的 triggerEffect 函数中,有以下的流程控制

    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
    
  • 存在调度器,则会执行调度器,否则才会执行 run 函数,因为示例程序修改后,传入了 调度器,所以此时run函数不再会执行,因为调度器中传入的是异步任务,所以在同步任务执行完毕后,开始执行调度器里的流程

  • 所以,scheduler可以起到影响代码执行顺序的功能

2.2 ) 控制代码执行规则

  • 修改下测试用例的程序
    const { reactive, effect } = Vue
    const obj = reactive({
      count: 1
    })
    effect(() => {
      console.log(obj.count)
    })
    obj.count = 2
    obj.count = 3
    
  • 上述代码中有两次setter行为,实际上是不需要的,在vue源码中 queuePreFlushCb 可以完成这个需求
    • 在测试示例中,它会依次输出:1, 2, 3
    • 我们修改vue源码,导出这个 queuePreFlushCb 函数,来让用例使用
      export { nextTick, queuePreFlushCb } from './scheduler'
      
    • 修改完成vue源码后,执行下 $ npm run dev
    • 如果我们继续修改测试用例
        const { reactive, effect, queuePreFlushCb } = Vue
        const obj = reactive({
          count: 1
        })
        effect(() => {
          console.log(obj.count)
        }, {
          scheduler() {
            queuePreFlushCb(() => console.log(obj.count))
          }
        })
        obj.count = 2
        obj.count = 3
      
    • 这时候,只会输出 1, 3 达成我们的目标了
    • 实际上 在watch中的调度器就是 queuePreFlushCb 这个函数
      scheduler = () => queuePreFlushCb(job)
      
  • 这个调度器最终会触发 resolvedPromise.then 的函数,这个就是把我们当前队列扔进微任务中来顺序执行
  • 最终obj.count是3,最终在微任务中输出的也是3,因为微任务是在同步代码执行完毕后,才会去执行

删减后的核心实现代码

1 )实现调度器

  • 新建 packages/runtime-core/src/scheduler.ts
    let isFlushPending = false // 状态表示
    
    const pendingPreFlushCbs: Function[] = [] // 回调队列
    const resolvedPromise = Promise.resolve() as Promise<any>
    let currentFlushPromise: Promise<void> | null = null
    
    export function queuePreFlushCb(cb: Function) {
      queueCb(cb, pendingPreFlushCbs)
    }
    
    function queueCb(cb: Function, pendingQueue: Function[]) {
      pendingQueue.push(cb)
      queueFlush()
    }
    
    // 依次执行队列中的函数
    function queueFlush() {
      if (isFlushPending) return // 真,什么都不做
      // 只有这个状态为假,才继续执行
      isFlushPending = true
      currentFlushPromise = resolvedPromise.then(flushJobs)
    }
    
    // 处理队列
    function flushJobs() {
      isFlushPending = false
      flushPreFlushCbs()
    }
    
    // 用于循环进行队列处理
    export function flushPreFlushCbs() {
      if (!pendingPreFlushCbs.length) return
      // 有队列内容时,使用新的标识,进行内容拷贝
      let activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
      pendingPreFlushCbs.length = 0 // 清空这个旧的队列
      for (let i = 0; i < activePreFlushCbs.length; i++) {
        activePreFlushCbs[i]()
      }
    }
    

2 )实现watch函数

  • 首先补充下 effect.ts 中的effect()函数和ReactiveEffect类,添加一个option参数,用于支持懒执行

    import { extend } from '@vue/shared' // 这个api就是 Object.assign,目的是通用
    
    export interface ReactiveEffectOptions {
      lazy?: boolean,
      scheduler?: EffectScheduler
    }
    
    export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions) {
      const _effect = new ReactiveEffect(fn)
      // 如果存在,则与_effect合并,这就意味着 options包含调度器,_effect中也会有调度器
      if (options) {
        extend(_effect, options)
      }
      if (!options || !options.lazy) {
        _effect.run()
      }
    }
    // 这个函数主要添加一个stop的空函数
    export class ReactiveEffect<T = any> {
      computed?: ComputedRefImpl<T>
      constructor(public fn: () => T, public scheduler: EffectScheduler | null = null) {}
      run() {
        activeEffect = this
        return this.fn()
      }
      // 后期实现
      stop() {}
    }
    
  • 再扩展 reactive.ts 补充 一个 isReactive 判断是否为响应性对象的函数,需要补充如下:

    // 定义枚举
    export const enum ReactiveFlags {
      IS_REACTIVE = '__v_isReactive' // 定义一个是 reactive 对象的私有属性标识
    }
    
    // 补充这个createReactiveObject函数
    function createReactiveObject(
      target: object,
      baseHandlers: ProxyHandler<any>,
      proxyMap: WeakMap<object, any>
    ) {
      const existingProxy = proxyMap.get(target)
      if (existingProxy) {
        return existingProxy
      }
      const proxy = new Proxy(target, baseHandlers)
      // 添加下面这行
      proxy[ReactiveFlags.IS_REACTIVE] = true // 将私有属性标识标记为 true
      proxyMap.set(target, proxy)
      return proxy
    }
    
    // 判断是否是 响应式对象
    export function isReactive(value): boolean {
      return !!(value && value[ReactiveFlags.IS_REACTIVE])
    }
    
  • 新建 packages/runtime-core/src/apiWatch.ts

    import { EMPTY_OBJ, hasChanged } from '@vue/shared'
    import { isReactive, ReactiveEffect } from '@vue/reactivity'
    import { queuePreFlushCb } from './scheduler'
    
    export interface WatchOptions<immediate = boolean> {
      immediate?: immediate,
      deep?: boolean
    }
    
    export function watch(source, cb: Function, options?: WatchOptions) {
      return doWatch(source, cb, options)
    }
    
    function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = EMPTY_OBJ) {
      let getter: () => any
      // 当前是响应性对象
      if (isReactive(source)) {
        getter = () => source
        deep = true
      } else {
        // 否则
        getter  = () => {}
      }
      // 处理deep场景
      if (cb && deep) {
        // 这种写法需要注意:这里实际上是内部进行依赖收集的地方
        const baseGetter = getter // 浅拷贝引用
        getter = () => baseGetter()
      }
    
      let oldValue = {}
      const job = () => {
        if (cb) {
          const newValue = effect.run()
          if (deep || hasChanged(newValue, oldValue)) {
            cb(newValue, oldValue)
            oldValue = newValue
          }
        }
      }
      let scheduler = () => queuePreFlushCb(job)
    
      const effect = new ReactiveEffect(getter, scheduler)
      
      if (cb) {
        if (immediate) {
          job()
        } else {
          oldValue = effect.run()
        }
      } else {
        effect.run()
      }
      return () => {
        effect.stop()
      }
    }
    
  • 导出各项需要的api, 比如watch, 重新 npm run dev 后,创建watch测试程序 watch.html

    <script src="../dist/vue.js"></script>
    <body>
      <div id='app'></div>
    </body>
    
    <script>
      const { reactive, watch } = Vue
      const obj = reactive({
        name: '张三'
      })
      watch(obj, (val, oldVal) => {
        console.log('watch ...')
        console.log(val)
      }, {
        immediate: true // 添加这个配置,会立即执行一次,但是下面2s后的异步回调没有被执行,这里watch没有监听到 reactive 的变化
      })
      const timer = setTimeout(() => {
        clearTimeout(timer)
        obj.name = '李四'
      }, 2000)
    </script>
    
  • 此时,watch是无法监听到reactive变化的, Why?

    • 因为我们没有进行依赖的收集,只有在 2s 的 setTimeout 函数中进行 setter 行为
    • 所以没有依赖可以触发
  • 实际上,我们自己的 apiWatcher.ts 中 getter = () => baseGetter() 这里,和源码是不一样的,Vue源码是 getter = () => traverse(baseGetter())

  • Vue源码中的 traverse函数就是为了一直收集getter行为,也就是说在测试程序中没有手动触发过getter行为, 我们要在watch内部把整个source对象里的所有属性,主动触发一次getter行为来完成依赖收集

  • 修改上述的 apiWatch.ts 中的getter, 如下

    function doWatch(source, cb: Function, { immediate, deep }: WatchOptions = EMPTY_OBJ) {
      // ...
      // 处理deep场景
      if (cb && deep) {
        // 这种写法需要注意:这里是依赖收集的地方
        const baseGetter = getter // 浅拷贝引用
        getter = () => traverse(baseGetter()) // 内部进行依赖收集触发getter行为
      }
      // ...
    }
    
    export function traverse(value: unknown) {
      if (!isObject(value)) {
        return value
      }
      // 循环属性
      for (const key in value as object) {
        traverse((value as object)[key]) // 访问当前属性,触发 getter 行为
      }
      return value
    }
    
  • 这样,整个watch函数就可以正常运行了

总结

  • 整个watch的实现逻辑还是依赖于 reactiveEffect 的功能(依赖收集和触发的过程)
  • 和之前reactive, ref 的依赖收集,触发的区别在于 watch 中的依赖收集操作是被动收集的
  • 还有就是watch中的scheduler调度器,用于控制执行顺序和规则,本身是一个函数,内部配合各个方便实现调度功能, 比如在watch中的 queuePreFlushCb 函数
Vue2高频面试核心:响应式原理、diff算法与Vue3对比 前端框架的核心在于响应式数据绑定,Vue2通过Object.defineProperty拦截属性读写,配合Watcher与Dep实现依赖收集和精准更新。理解这一原理不仅有助于解释新增属性、数组索引等边界问题,也是应对面试追问的关键。在实际工程中,Vue2存量项目仍大量存在,从iframe缓存处理到富文本编辑器集成,再到向Vue3升级的路径,都是开发者常遇的场景。本文系统梳理了Vue2高频面试题,涵盖响应式原理、生命周期、组件通信、diff算法、路由权限以及Vue2与Vue3的对比,帮助读者建立知识体系,提升 阅读详情

相关推荐

Vue面试核心考点全解析:从响应式原理到组件通信与性能优化

前端开发中,Vue 作为主流框架,其响应式原理、组件通信、虚拟DOM等概念是工程师必须掌握的基础能力。理解 Vue2 的 Object.defineProperty 与 Vue3 的 Proxy 实现差异,能帮助开发者从底层运行机制出发,真正看懂状态驱动视图的设计哲学。computed 的缓存机制、watch 的副作用处理,以及 diff 算法的优化策略,不仅是面试高频问题,更直接决定了项目性能与代码质量。在实际工程中,合理运用组件通信方案、路由懒加载与性能优化清单,可以显著提升应用的可维护性与加载速度。本

weixin_31412781的博客 243

vue3 监听-watch

2.当监听值为proxy对象时,oldValue值将出现异常,此时与newValue相同。需要将监听值写成函数返回形式,vue3无法直接监听对象的某个属性变化。1.此时vue3将强制开启deep深度监听。监听proxy数据的某个属性。

weixin_40489411的博客 2708

Xilinx Vitis arm-xilinx-eabi-gcc.exe: error: *.c: Invalid argument

This problem may be a bugof version 2021.1 only. Unfortunately, I'm just currently using this version. After Build Project: Find where this Makefile is through "make: Leaving directory" information. For me, it is located atZynq_CPU_wrapper_hw_platfo...

Ayka的博客 6663

vue3 watch 监听多值以及深度监听用法

vue3 watch 监听多值以及深度监听用法

qq_46617584的博客 8437

闲鱼x-sign、x-mini-wua、x-umt三字段协同验证机制解析

在移动客户端风控体系中,设备标识与行为签名是保障接口安全的核心技术。x-sign并非传统时间戳+密钥的静态签名,而是融合设备特征、运行时状态与服务端动态因子的多阶段位运算结果;x-mini-wua作为小程序轻量水印,依赖主App注入的短时效sessionKey与截断时间戳;x-umt则通过设备硬件与网络环境联合编码实现强绑定。三者构成‘设备—环境—行为’三位一体的交叉验证链路,广泛应用于电商API防护、二手平台数据采集及自动化运营等场景。本文基于Android/iOS真机逆向与Frida动态分析,揭示其生成

weixin_30457551的博客 425

Simulink模块库介绍(一)——信号源模块

Simulink模块库介绍(一)——信号源模块库目录Clock模块Digital Clock模块Band-Limited White Noise模块 目录 simulink模块库中提供了丰富的信号源模块组。逐一介绍,算是对之前做个总结吧 Clock模块 通过时钟模块,输出的时间作为判断呢,如下图,前2s之前为高斯白噪声,后2s为正弦曲线,共计10s。 Digital Clock模块 数字时钟模块主要用于离散系统的计时,sample time表示采用时间,如下图:分别是1s和0.5s的采样时间 Band-

5万+

Vue3 - watch 侦听器(保姆级详细使用教程)

它之所以叫侦听器,是因为它可以侦听一个或多个响应式数据源数据,并再数据源变化时调用所给的回调函数。就是你传给watch侦听器一个响应式变量,然后当这个变量变化时,自动触发一个你定义的函数,就像一个人被监控了一样,只要这个人一动,摄像头就会报警。

weixin_44166849的博客 6万+

【PADS-开发指南】

馒头破戒大师

sinat_23896491的博客 3226

vue的watch监听函数

vue的watch监听函数 在vue中,使用watch来响应数据的变化。watch的用法大致有三种。下面代码是watch的一种简单的用法: 1. <input type="text" v-model="cityName"/> new Vue({ el: '#root', data: { cityName: 'shanghai' }, methods:{ Namechange:function(){ ..... }

wwf1225的博客 8354

GB28181协议实战:5分钟搞定海康摄像头SIP注册失败问题(附Wireshark抓包分析)

本文详细解析了海康摄像头在GB28181协议下SIP注册失败的7个关键原因及解决方案,包括网络可达性、鉴权参数、NAT穿越等核心问题。通过Wireshark抓包分析,提供快速定位方法,帮助工程师高效解决SIP注册失败问题,提升实时图像联调效率。

weixin_29212867的博客 559

4-都2026年了,嵌入式发展前景怎么样,还适合入行吗?

本文系统分析了嵌入式行业的发展前景,指出到2026年嵌入式将迎来由"万物智联"驱动的黄金周期。文章回顾了嵌入式技术的四个演进阶段,从专用计算机到AIoT时代,强调其技术积累的延续性。作者从人才流向、产业智能化转型、高校新增专业等角度论证嵌入式行业的蓬勃发展,并提出"铁三角"模型分析其驱动力和护城河。最后给出职业发展路径建议,认为嵌入式适合长期主义者,在智能汽车、边缘计算等领域有广阔前景。文章建议读者抓住产业升级机遇,选择这一稳定且有复利效应的技术赛道。

指尖动听知识库 393

我的Vue2/Vue3知识框架汇总

基础一定要多看,很多看似复杂的bug,实际都是基础问题,不要把时间浪费在修复基础问题的bug上。本文以知识点总结为主,将技术点连成线,再结成网,不管面试官问啥,都能扯一扯啦~最近总感觉知道的越多之后,不知道的更多;还有许多需要学习的,总结这篇文章之后,也该需要制定个以后的学习计划了。然后这篇关于vue2/vue3的文章也算是一个阶段性的总结吧,有了对vue框架的理解,相信再上手框架的学习也会有帮助。

铁锤妹妹的博客 1956

PDAF相位对焦原理

pdaf光路图

camerall 1万+

知识梳理——vue

1.Vue模板编译的原理 就是将template(模板)转化为render(渲染函数)的过程。会经历以下阶段: 1.解析器: 将模板解析成AST(abstract syntax tree 抽象语法树) 2.优化器: 遍历AST标记静态节点 (生成渲染函数之前这个阶段,需要做一个优化操作:遍历一遍AST,给所有静态节点做一个标记,这样在虚拟DOM中更新节点时,如果发现这个节点有这个标记,就不会重新渲染它。) 3.代码生成器: 将优化后的AST树转换为可执行的代码(渲染函数) ...

wytraining 924

2026年国赛C题微网与外部电网电力调控策略 (三):问题三、问题四滚动预测、实时电价与结果核验

专栏内定期更新ABCDE五题详细思路、代码(含运行结果图)和讲解视频-(11号之前半价):问题三看起来只是多给了一个光伏预报附件,实质上它改变了整道题的信息结构:问题四则是在问题二、问题三之上替换价格来源。问题4-2相当于“确定性计划+实时电价”,问题4-3相当于“滚动预测+实时电价”。物理模型完全不变,改变的是价格序列和费用口径。本文先给出核心结果:从结果看,问题3比问题2多约164.21万元,问题4-3比问题4-2多约168.79万元。主要增量不是“储能没有工作”,而是预测误差、计划偏差和紧急购电造成的

YOLO 345

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

Vue实例核心属性与生命周期详解:从data、computed到钩子函数

在Web前端开发中,响应式编程和组件化架构已成为构建现代用户界面的核心技术范式。其核心原理在于通过数据绑定机制,实现数据模型与视图层的自动同步,从而提升开发效率和用户体验。这一技术价值体现在能够大幅减少手动DOM操作,使开发者更专注于业务逻辑。在实际应用场景中,无论是构建单页面应用还是复杂的管理后台,理解框架实例的核心工作机制都至关重要。本文聚焦于Vue.js框架,深入剖析其**实例属性**如data、methods、computed、watch的设计哲学与协同机制,并系统讲解**生命周期钩子**的执行时机

weixin_34392435的博客 414

C# WinForm 基于DirectShow.dll编写的程序,可边预览边保存视频,音频和图片

基于微软的DirectShow.dll的二次开发1.自动识别连接的摄像头跟麦克风且可以对其进行选择(默认选中第一个)2.可录制wmv和avi两种格式的视频并且都带有音频,avi可以录制的分辨率和帧数进行选择,wmv默认进行了淸晰度优化如不喜欢可去除代码中有相关提示3.可在预览点开之后进行拍照,也可边录制视频边拍照4.添加了视频和音频的压缩功能(酌情使用)这次二次开发发现C#对DirectShow的相关信息太少,这次做完也不敢藏私共享出来,还望大家指正

vue笔记

监听字符串、数字、布尔值等简单数据类型时,直接在watch中定义与数据同名的属性,属性值为处理函数(接收两个参数:新值newVal和旧值oldValdata() {return {username: '', // 简单数据age: 18},watch: {// 监听 username 的变化console.log('用户名从', oldVal, '变成了', newVal);},// 监听 age 的变化。

2301_79775040的博客 4301

VC中sqlite的使用

在VC中对sqlite的使用,实现数据库大批数据的快速插入,读取数据库中的数据在ListCtrl中显示

计算机控制技术及应用

《计算机控制技术及应用》通过总结作者的科研成果和吸收国内外的先进理论、方法和技术,以计算机控制系统的网络化、开放化、智能化和集成化发展趋势为主线索,主要介绍计算机控制系统的基本概念和计算机控制系统的典型形式,计算机控制系统的数学描述与基本分析方法,以及数字PID控制器、最少拍控制系统、达林算法、状态变量设计法等设计方法及其各种改进算法。重点突出计算机控制系统输入输出通道接口的设计方法和典型控制系统的设计案例与实现方法等。

梳理vue双向绑定的实现原理

Vue 采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,通过Object.defineProperty来劫持数据的setter,getter,在数据变动时发布消息给订阅者,订阅者收到消息后进行相应的处理。要实现mvvm的双向绑定,就必须要实现以下几点:Compile—指令解析系统,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数Ob...

weixin_34148456的博客 95

Vue+TypeScripts 尚硅谷vue3教程学习笔记

Vue 3 是 Vue.js 框架的一次重大升级,于 2020 年 9 月 18 日正式发布,代号为“One Piece(海贼王)”。作为新一代的前端构建框架,它在性能、源码架构、开发体验以及生 态支持上进行了全面的革新,旨在为开发者提供更高效、更灵活的开发体验。核心特性与优势Vue 3 在底层进行了大量优化,使得应用运行更加流畅。其打包体积减少了 41%,初次渲染速度提升了 55%,更新渲染速度提升了 133%,同时内存占用减少了 54%。

weixin_63108234的博客 252

VUE3 面试题总结

单文件组件< script setup > < script setup > </script> 可以直接引入,定义,逻辑,无需return返出数据 直接import引入子组件后在页面直接使用,无需注册 替代props功能的是: defineProps({}) 替代emit功能的是:defineEmits([]) 点击的dom上@click="skip" 例:cons...

hjdjhh的博客 2273

Vue3 基础实战|从 0 搭建待办清单项目(组合式 API+Pinia 状态管理,附项目源码)

本文详细介绍了如何使用Vue3和Pinia构建一个功能完整的待办清单应用。主要内容包括:1) 项目功能演示,包含核心功能(增删改查、筛选)和拓展功能(编辑、统计、持久化);2) 开发环境准备,使用Vite快速搭建Vue3项目;3) 核心知识点梳理,涵盖组合式API、Pinia状态管理等Vue3特性;4) 分步实现过程,从项目搭建到功能开发,包含详细代码示例;5) 常见问题解决方案和样式优化建议。该项目适配PC/移动端,实现了本地存储持久化和过渡动画效果,是Vue3入门实践的完整案例。

qq_32528275的博客 626

Vue2与Vue3核心区别学习笔记

Vue3采用Monorepo架构进行源码重构,将核心功能拆分为响应式、编译器、运行时等独立模块,模块间解耦更彻底,支持按需引入和Tree-Shaking,打包体积更小;移除$on/$off/$once(事件总线)、filter过滤器、mixin(不推荐使用)、$children等API,推荐使用更规范的替代方案(如mitt实现事件总线、方法/计算属性替代过滤器)。• 优势:逻辑聚合,代码可读性高;支持Suspense组件,实现异步组件的加载状态管理,可自定义加载中、加载失败界面,简化异步组件开发。

2501_91864401的博客 199

Vue面试核心考点与实战解析

响应式系统是Vue框架的核心机制,通过数据劫持实现视图自动更新。Vue2采用Object.defineProperty,而Vue3升级为Proxy API,显著提升了性能并简化了数组处理。在工程实践中,组件化开发和状态管理是关键,Vue的单文件组件(SFC)和Pinia状态库提供了完整解决方案。性能优化方面,虚拟DOM的diff算法和组件懒加载能有效提升渲染效率。这些技术广泛应用于中后台管理系统和移动端项目,掌握Vue3的Composition API和静态提升等新特性,能更好应对现代前端开发需求。本文深入

weixin_34026276的博客 406

Vue2 的所有核心知识点

从基础入门到进阶应用。主要内容包括...

MR_bailu的博客 428

Vue.js面试核心知识点与实战技巧解析

Vue.js作为主流前端框架,其响应式原理通过数据劫持实现视图自动更新,核心机制包含Object.defineProperty(Vue2)和Proxy(Vue3)两种实现方式。组件化开发中,合理的生命周期管理(如created发起请求、beforeDestroy清除资源)和多种通信方案(props/emit、provide/inject等)是工程实践的关键。性能优化方面,需重点关注虚拟DOM的diff算法和nextTick异步更新机制,结合v-if/v-show选择、路由懒加载等构建配置提升应用性能。在Vu

weixin_33795806的博客 385

Vue面试全攻略:核心知识点与高频问题解析

MVVM模式作为现代前端框架的核心架构,通过数据绑定实现视图与数据的自动同步。Vue2采用Object.defineProperty实现响应式,而Vue3升级为Proxy方案,显著提升了性能并解决了数组监听等痛点问题。在工程实践中,合理运用生命周期钩子、组件通信方案和性能优化策略,能大幅提升应用质量。针对Vue技术栈的面试准备,需要重点掌握响应式原理、Computed与Watch的差异、以及Vue3的Composition API等核心概念。本指南系统整理了Vue2/Vue3高频面试题,涵盖从MVVM原理到

weixin_34315485的博客 353
上一篇: 数据结构与算法之链表: LeetCode 141. 环形链表 (Ts, Py, Go版)
下一篇: 数据结构与算法之链表: LeetCode 148. 排序链表 (Ts, Py, Go版)
Wang's Blog
博客等级 码龄12年 7434粉丝 2421原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Wang's Blog

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值