一行一行详解Vue3 watch/watchEffect源码

2,308 阅读9分钟

一行一行详解Vue3 watch/watchEffect源码

前置知识:响应式基本原理

置知识: vue3响应式系统基本原理得知道吧:响应式对象(reactive, ref),effect, track, trigger这几个是大概什么作用要知道,不知道我简短介绍

const obj = reactive({a: 0})这个函数会对一个对象作代理(proxy)

对obj任何键的访问都会触发track函数,对obj任何键值更改会触发trigger函数

effect(fn)用来被trigger触发

那么用法如下:

一个effect里面有我们传入的一个函数,当这个函数去访问obj.a的时候,触发track函数,

const fn = () => {
    console.log(obj.a) //访问了obj.a,被track住
}
effect(fn)
obj.a = 1 //这里会触发trigger让上面fn重新调用

track会去查找最"新"的effect,然后保存住 obj<-->a<-->effect这三角恋关系。这个时候如果我更改了它:obj.a = 1,那么trigger会去找obj,a对应的effect是谁,找到之后重新运行这个effect保存的函数,这就是vue3响应式的原理,而里面的细节可就太多了这里就不深入了

前置知识:watch和watchEffect用法

vue3 watchEffect/watch api大家应该了解,就是监听响应式对象,在改变的时候重新执行指定回调

const state = reactive({
    star: 0
})
watch(state, (newVal, oldVal) => {
    console.log(state.star)
  console.log(newVal)
  console.log(oldVal)
})
state.star++ //这会再次打印state.star, 新的值,老的值

watchEffect(后面简称we)则是直接,传入回调即可

const state = reactive({
    star: 0
})
watch(() => {
    console.log(state.star)
})
state.star++ //这会再次打印state.star

一行一行源码解析

我们进入源码看到这两个方法实际上用的同一套api

// Simple effect.
export function watchEffect(
  effect: WatchEffect,
  options?: WatchOptionsBase
): WatchStopHandle {
  return doWatch(effect, null, options)
}
// watch
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
  source: T | WatchSource<T>,
  cb: any,
  options?: WatchOptions<Immediate>
): WatchStopHandle {
  return doWatch(source as any, cb, options)
}

可以看到watchEffect调用第二个参数是null,而watch调用第二个参数则是我们传入的回调函数

进入doWatch里面, 以下代码全部在doWatch中

  let getter: () => any
  let forceTrigger = false
  if (isRef(source)) {
    getter = () => (source as Ref).value
    forceTrigger = !!(source as Ref)._shallow
  } else if (isReactive(source)) {
    getter = () => source
    deep = true
  } else if {
    ...
  }

看到首先我们定义了一个getter,getter的作用是去获取要监听的数据,当source是ref或者reactive的时候,getter就是去取对应的数据,getter的实现根据不同的监听目标而不同,

PS:callWithErrorHandling是调用传入的函数,如果出错有相应的处理,就当做

...
else if (isArray(source)) {
  getter = () =>
    source.map(s => {
      if (isRef(s)) {
        return s.value //如果是ref,则取得其value
      } else if (isReactive(s)) {
        //这里traverse会递归去获得reactive对象的所有键值,
        //因为只要访问一个键就会把这个键依赖收集住,递归把所有键全部都触发依赖收集
        return traverse(s)
      } else if (isFunction(s)) {
        //source也可以接受一个返回ref的函数
        return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER)
      } else {
        __DEV__ && warnInvalidSource(s) //所有情况都不是,dev模式下报个错
      }
    })
} 
...

source可以是一个返回一个响应式对象的值,或者返回一个ref对象的函数如下

const count = ref(0)
watch(() => count, (newCount, oldCount) => {
    ...
})

所以当source是函数的时候

else if (isFunction(source)) {
    if (cb) {
      /**进入这里说明是watch而不是watchEffect */
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
    /**进入这里说明是watchEffect而不是watch */
      getter = () => {
        // instance是一个全局变量,可以理解为当前执行这个方法的组件
        if (instance && instance.isUnmounted) {
          return
        }
        //cleanup是注册的清理函数
        if (cleanup) {
          cleanup()
        }
        return callWithErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onInvalidate]
        )
      }
    }
  }

以上能看到如果是函数,我们的getter其实就是去取得那个值就可以,因为访问响应式对象就会追踪到依赖,其中cleanup函数注册的地方很巧妙

声明cleanup的地方其实就在下面几行

let cleanup: () => void
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
  // runner是effect
  cleanup = runner.options.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}

现在可以看到,调用onInvalidate传入一个回调函数fn,就可以把fn注册到当前effect的onStop去,并且也把这个回调传入到了cleanup,那么当我们调用cleanup就可以调用fn了

那么这个onInvalidate我们用户是怎么能用的呢,它其实作为参数传入了watch和watchEffect

// onInvalidate传入这里
watchEffect((onInvalidate) => {
    window.addEventListener("click", handler)
  onInvalidate(() => {
    window.removeEventListener("click", handler)
  })
})

这样就将这个清除函数暴露给了我们用户,你一定会想到react的处理方式

useEffect(() => {
  window.addEventListener("click", handler)
    return () => {
    window.removeEventListener("click", handler)
    }
})

react的方法看着很直观,那么vue为什么不采用这种写法呢?因为这种写法不支持async await,useEffect里面的函数只能是普通函数,因为async函数或者生成器函数,返回值会用promise包一层,而vue这种是可以直接用async的,当然react想用也可以,在里面做一个IIFE自调用async函数就可以,但是就很丑了

接着回到刚刚的doWatch里面

/**watch api*/
if (cb && deep) {
  const baseGetter = getter
  getter = () => traverse(baseGetter())
}

let cleanup: () => void
const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
  cleanup = runner.options.onStop = () => {
    callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
  }
}

如果cb存在,说明是watch而不是watchEffect,那么将这个baseGetter再包一层,因为有可能我们传入的source是一个reactive,要递归监控他所有键,而刚刚没有对这种情况作特殊处理,cleanup上面已经说过了,继续往下看

 let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
 const job: SchedulerJob = () => {
   //这里runner是在下面定义的,js是可以在函数里面链接到未声明变量的,
   //runner是一个effect传入的函数是上面的getter,也就是说我们调用effect就可以获得getter的返回值
   //当这个runner被取消之后,我们什么也不执行直接return
   if (!runner.active) {
     return
   }
   if (cb) { //有cb说明我们使用的是watch而不是watchEffect
     const newValue = runner() //调用runner获得这次的值
     // 判断新旧的值是不是一样的,如果是一样的没有改变就不用处理
     if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
       // 如果清理函数有,就调用清理函数,防止内存泄露
       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,
         onInvalidate
       ])
       oldValue = newValue
     }
   } else {
     runner() //对于watchEffect,里面就是传入的函数,直接调用这个runner就行
   }
}

聪明的你一看,欸这个watchEffect没有处理cleanup!!!赶紧去提PR,实际上watchEffect是处理了的,可以翻到上面的getter里面可以看到

// no cb -> simple effect
getter = () => {
    ...
  if (cleanup) {
    cleanup()
  }
    ...
}

白高兴一场,以为能提个小PR呢

老老实实继续往下看

job.allowRecurse = !!cb
/**allowRecurse就是允许递归调用,也就是说watch是可以在里面修改值,达到重新触发watch的目的
    比如说
watch(count, (newVal, oldVal) => {
    if (newVal % 2) {
    count.value = newVal + 1
  }
})*/
let scheduler: ReactiveEffectOptions['scheduler']
if (flush === 'sync') {
  /*scheduler是调度器的意思,当effect被trigger触发的时候,会判断有没有调度器,
  如果有就会调用这个调度器而不是直接调用effect本身*/
  scheduler = job
} else if (flush === 'post') {
  scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
  // default: 'pre'
  scheduler = () => {
    if (!instance || instance.isMounted) {
      queuePreFlushCb(job)
    } else {
      // 如果是pre第一次调用,同步调用一次,这里我猜测是防止打乱渲染,因为渲染的flush就是pre,
      // 防止后面mount的时候,触发了这个job,
      job()
    }
  }
}

根据flush来判断更新的时机,sync代表同步更新,我们将调度器直接设置成job就可以,而如果是pre或者post,那么就要放入微任务队列里面去,让事件循环调度它。注意这里第一次调用的时候,根据文档描述,如果是pre,初始化依然是立刻调用

现在调度器已经有了,接下来看effect了

const runner = effect(getter, {
  lazy: true,
  onTrack,
  onTrigger,
  scheduler
})

// 这个方法将effect和当前的组件“绑定”,就是将effect推入到组件的effect队列里面
recordInstanceBoundEffect(runner, instance)

这里的effect终于将之前提到的getter,scheduler用上了,这里有lazy: true表示这个effect不会立即执行getter,而需要手动去调用

还是回到我们的doWatch继续看

// 初始运行
if (cb) {
  /*因为watch默认是惰性的,要改变之后才会触发,如果传入immediate为true就会立即执行调用job*/
  if (immediate) {
    job()
  } else {
    /*在这里进行了依赖收集(track),将oldValue也就是传入watch里面的那个旧值记录*/
    oldValue = runner()
  }
} else if (flush === 'post') {
  /*如果不是watch,并且flush是post,应该放在下一个"tick"中执行这个watchEffect
  这个方法,就是将effect推入到post队列中,在之后微任务执行会检查post队列,如果有
  任务就执行,那个时候就会执行这个effect去追踪依赖*/
  queuePostRenderEffect(runner, instance && instance.suspense)
} else {
  /*默认的watchEffect我们直接调用这个effect就好了*/
  runner()
}

接下来最后一步了,我们知道watchEffect会返回一个函数去停止这个watchEffect,所以最好返回一个函数

 return () => {
   stop(runner) // 这一步会将effect的active属性改成false,下次调用发现是false的话就不会执行相应回调
   if (instance) {
     remove(instance.effects!, runner) //remove就是将这个effect从组件的effect队列里面移除
   }
 }

谢谢你看到这,完整代码如下

function doWatch(
  source: WatchSource | WatchSource[] | WatchEffect | object,
  cb: WatchCallback | null,
  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
  instance = currentInstance
): 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.`
    )
  }

  let getter: () => any
  let forceTrigger = false
  if (isRef(source)) {
    getter = () => (source as Ref).value
    forceTrigger = !!(source as Ref)._shallow
  } else if (isReactive(source)) {
    getter = () => source
    deep = true
  } else if (isArray(source)) {
    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)) {
    /**watch api */
    if (cb) {
      // getter with cb
      getter = () =>
        callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
    } else {
    /**watchEffect api */  
      // no cb -> simple effect
      getter = () => {
        if (instance && instance.isUnmounted) {
          return
        }
        if (cleanup) {
          cleanup()
        }
        return callWithErrorHandling(
          source,
          instance,
          ErrorCodes.WATCH_CALLBACK,
          [onInvalidate]
        )
      }
    }
  } else {
    getter = NOOP
    __DEV__ && warnInvalidSource(source)
  }

  /**watch api  */
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

  let cleanup: () => void
  const onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
    cleanup = runner.options.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 (__NODE_JS__ && isInSSRComponentSetup) {
    if (!cb) {
      getter()
    } else if (immediate) {
      callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
        getter(),
        undefined,
        onInvalidate
      ])
    }
    return NOOP
  }

  let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE
  const job: SchedulerJob = () => {
    if (!runner.active) {
      return
    }
    if (cb) {
      // watch(source, cb)
      const newValue = runner()
      if (deep || forceTrigger || hasChanged(newValue, oldValue)) {
        // 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,
          onInvalidate
        ])
        oldValue = newValue
      }
    } else {
      // watchEffect
      runner()
    }
  }

  // important: mark the job as a watcher callback so that scheduler knows
  // it is allowed to self-trigger (#1727)
  job.allowRecurse = !!cb

  let scheduler: ReactiveEffectOptions['scheduler']
  if (flush === 'sync') {
    scheduler = job
  } else if (flush === 'post') {
    scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
  } else {
    // default: 'pre'
    scheduler = () => {
      if (!instance || instance.isMounted) {
        queuePreFlushCb(job)
      } else {
        // with 'pre' option, the first call must happen before
        // the component is mounted so it is called synchronously.
        job()
      }
    }
  }

  const runner = effect(getter, {
    lazy: true,
    onTrack,
    onTrigger,
    scheduler
  })

  recordInstanceBoundEffect(runner, instance)

  // initial run
  if (cb) {
    if (immediate) {
      job()
    } else {
      oldValue = runner()
    }
  } else if (flush === 'post') {
    queuePostRenderEffect(runner, instance && instance.suspense)
  } else {
    runner()
  }

  return () => {
    stop(runner)
    if (instance) {
      remove(instance.effects!, runner)
    }
  }
}

参考:

源码对应地址