Vue3 源码解读系列(七)——侦听器

侦听器

watch

侦听器是当侦听的对象或者函数发生了变化则自动执行某个回调函数。

侦听器的内部设计:侦听响应式数据的变化,内部创建 effect runner,首次执行 runner 做依赖收集,然后在数据发生变化后,以某种调度方式去执行回调函数。

调用侦听器的两种方式:

  1. 通过 Composition API watch

    watch(sourch, callback, options?)
    
  2. 通过 vm.$watch

    vm.$watch(sourch, callback, options?)
    

侦听器主要做了5件事:

  1. 标准化 source
    • 如果 source 是一个数组,则遍历该数组的每一项,判断该项为 refreactivefunction 进行分别的处理
    • 如果 sourceref 对象,则创建一个访问 source.valuegetter 函数
    • 如果 sourcereactive 对象,则创建一个访问 sourcegetter 函数,并设置 deeptrue
    • 如果 source 是一个函数,则会进一步判断第二个参数 cb 是否存在,对于 watch API 来说,cb 是一定存在且是一个回调函数,getter 就是一个简单的对 source 函数封装的函数
    • 如果 source 不满足上述条件,则在非生产环境下报警告,提示 source 类型不合法
  2. 构造 applyCb 回调函数
  3. 创建 scheduler 时序执行函数
    • flushsync 时,表示它是一个同步 watcher,即当数据变化时同步执行回调函数
    • flushpre 时,回调函数通过 queueJob 的方式在组件更新之前执行,如果组件还没挂载,则同步执行确保回调函数在组件挂载之前执行
    • 如果没设置 flush,回调函数通过 queuePostRenderEffect 的方式在组件更新之后执行
  4. 创建 effect 副作用函数,用于依赖收集
  5. 返回侦听器销毁函数
/**
 * watch 侦听器的实现
 */
function watch(source, cb, options) {
  // 如果传入的 cb 参数不是函数,则警告
  if ((process.env.NODE_ENV !== 'production') && !isFunction(cb)) {
    warn(/* ... */)
  }
  return doWatch(source, cb, options)
}

/**
 * 侦听函数
 * doWatch 主要做了 5 件事:
 * 1、标准化 source
 * 2、构造 applyCb 回调函数
 * 3、创建 scheduler 时序执行函数
 * 4、创建 effect 副作用函数
 * 5、返回侦听器销毁函数
 */
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
  // 1、标准化 source
  /**
   * source 的标准化根据 source 的类型分类:
   * · 如果 source 是 ref 对象,则创建一个访问 source.value 的 getter 函数
   * · 如果 source 是 reactive 对象,则创建一个访问 source 的 getter 函数,并设置 deep 为 true
   * · 如果 source 是一个函数,则会进一步判断第二个参数 cb 是否存在,对于 watch API 来说,cb 是一定存在且是一个回调函数,getter 就是一个简单的对 source 函数封装的函数
   * · 如果 source 不合法,则在非生产环境下报警告,提示 source 类型不合法
   */
  // source 不合法时报警告的函数
  const warnInvalidSource = (s) => {
    warn(/* ... */)
  }
  const instance = currentInstance // 当前组件实例
  let getter
  // source 为数组,遍历判断每一个的类型
  if (isArray(source)) {
    getter = () => source.map(s => {
      // 如果是 ref,则返回访问 s.value 的 getter
      if (isRef(s)) {
        return s.value
      }
      // 如果是 reactive,则返回访问 s 的 getter,并递归执行侦听
      else if (isReactive(s)) {
        return traverse(s)
      }
      // 如果是函数,则返回访问 s 的返回的响应式对象的 getter
      else if (isFunction(s)) {
        return callWithErrorHandling(s, instance, 2/* WATCH_GETTER */)
      }
      // 如果不合法,则在非生产环境下报警告
      else {
        (process.env.NODE_ENV !== 'production') && warnInvalidSource(s)
      }
    })
  }
  // source 为 ref,则返回访问 source.value 的 getter
  else if (isRef(source)) {
    getter = () => source.value
  }
  // source 为 reactive,则返回访问 source 的 getter,并且设置 deep:true
  else if (isReactive(source)) {
    getter = () => source
    deep = true
  }
  // source 为函数,则进一步判断是否存在回调函数 cb
  /**
   * 如果存在,则返回访问 source 的返回的响应式对象的 getter
   * 如果不存在,说明是省略 source,因为 cb 是一定存在的,则执行 watchEffect 的逻辑,source 为依赖的响应式对象
   */
  else if (isFunction(source)) {
    // getter with cb
    if (cb) {
      getter = () => callWithErrorHandling(source, instance, 2/* WATCH_GETTER */)
    }
    // getter without cb
    else {
      // watchEffect 的逻辑
    }
  }
  // source 不合法,则在非生产环境下报警告
  else {
    getter = NOOP;
    (process.env.NODE_ENV !== 'production') && warnInvalidSource(source)
  }
  // 如果存在 cb,且 deep:true,则递归侦听每一个子属性
  if (cb && deep) {
    const baseGetter = getter
    getter = () => traverse(baseGetter())
  }

  // 2、构造 applyCb 回调函数
  /**
   * cb 的三个参数:
   * 1、newValue - 新值
   * 2、oldValue - 旧值
   * 3、onCleanup - 无效回调函数
   */
  let cleanup
  // 注册无效回调函数
  const onInvalidate = (fn) => {
    cleanup = runner.options.onStop = () => {
      callWithErrorHandling(fn, instance, 4/* WATCH_CLEANUP */)
    }
  }
  let oldValue = isArray(source) ? [] : INITIAL_WATCHER_VALUE/* {} */ // 旧值初始值
  // applyCb 回调函数	
  const applyCb = cb ? () => {
    // 若组件被销毁,则直接返回
    if (instance && instance.isUnmounted) return
    // 执行 runner 求得新值
    const newValue = runner()
    if (deep || hasChanged(newValue, oldValue)) {
      // 执行清理函数
      if (cleanup) {
        cleanup()
      }
      callWithAsyncErrorHandling(cb, instance, 3/* WATCH_CALLBACK */, [
        newValue,
        oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, // 第一次更改时传递旧值为 undefined
        onInvalidate
      ])
      // 更新旧值,用于下次比对
      oldValue = newValue
    }
  } : void 0

  // 3、创建 scheduler 时序执行函数
  const invoke = (fn) => fn()
  // scheduler 的作用是根据某种调度方式去执行某种函数,在 watch API 中主要影响的是回调函数的执行方式,而 scheduler 又受到第三个参数 options 的 flush 属性的影响
  /**
   * 当 flush 为 sync 时,表示它是一个同步 watcher,即当数据变化时同步执行回调函数
   * 当 flush 为 pre 时,回调函数通过 queueJob 的方式在组件更新之前执行,如果组件还没挂载,则同步执行确保回调函数在组件挂载之前执行
   * 如果没设置 flush,回调函数通过 queuePostRenderEffect 的方式在组件更新之后执行
   */
  let scheduler
  // 同步
  if (flush === 'sync') {
    scheduler = invoke
  }
  // 进入异步队列,组件更新前执行
  else if (flush === 'pre') {
    scheduler = job => {
      // 如果组件已经挂载或销毁,则在组件更新之前执行
      if (!instance || instance.isMounted) {
        queueJob(job)
      }
      // 如果组件还没挂载,则同步执行确保回调函数在组件挂载前执行
      else {
        job()
      }
    }
  }
  // 进入异步队列,组件更新后执行
  else {
    scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
  }

  // 4、创建 effect 副作用函数
  const runner = effect(getter, {
    lazy: true, // 延时执行
    computed: true, // computed effect 可以优先于普通的 effect 先运行,比如组件渲染的 effect
    onTrack,
    onTrigger,
    scheduler: applyCb ? () => scheduler(applyCb) : scheduler
  })
  // 在组件实例中记录这个 effect
  recordInstanceBoundEffect(runner)
  // 存在 cb
  if (applyCb) {
    // 配置了 options: { immediate: true },直接执行
    if (immediate) {
      applyCb()
    }
    // 没有配置 options: { immediate: true },在指定时机执行
    else {
      oldValue = runner()
    }
  }
  // 没有 cb 的情况
  else {
    runner()
  }

  // 5、返回侦听器销毁函数
  return () => {
    // 执行 stop 函数让 runner 失活,这样可以停止对数据的侦听
    stop(runner)
    if (instance) {
      // 移除组件 effects 对这个 runner 的引用
      remove(instance.effects, runner)
    }
  }
  function stop(effect) {
    if (effect.active) {
      cleanup(effect)
      if (effect.options.onStop) {
        effect.options.onStop()
      }
      effect.active = false
    }
  }
}
/**
 * 异步任务队列的设计
 * 当前 flush 不是 sync 时,把回调函数执行的任务推到一个异步任务队列中执行
 */
function queuePostRenderEffect() {
  const queue = [] // 异步任务队列
  const postFlushCbs = [] // 异步任务队列任务执行完后执行的回调函数队列

  const p = Promise.resolve()
  let isFlushing = false // 是否正在执行任务队列
  let isFlushPending = false // 是否在等待 nextTick 执行 flushJobs

  // 添加到下一个 Tick(宏任务执行的周期) 队列
  function nextTick(fn) {
    // 通过 promise.then() 实现异步
    return fn ? p.then(fn) : p
  }

  // 添加异步任务到执行队列
  function queueFlush() {
    if (!isFlushing && !isFlushPending) {
      isFlushPending = true
      nextTick(flushJobs)
    }
  }

  // 添加任务到队列
  function queueJob(job) {
    if (!queue.includes(job)) {
      queue.push(job)
      queueFlush()
    }
  }

  // 添加回调函数到队列
  function queuePostFlushCb(cb) {
    // 如果不是数组,则直接 push
    if (!isArray(cb)) {
      postFlushCbs.push(cb)
    }
    // 如果是数组,则拍平后 push
    else {
      postFlushCbs.push(...cb)
    }
    queueFlush()
  }
}

const getId = (job) => (job.id == null ? Infinity : job.id)

/**
 * 执行异步任务
 */
function flushJobs(seen) {
  isFlushPending = false
  isFlushing = true
  let job
  if ((process.env.NODE_ENV !== 'production')) {
    seen = seen || new Map()
  }

  // 异步任务队列 queue 从小到大排序
  /* 
    原因:
    1、保证组件更新的顺序:创建组件的过程是父->子,创建组件副作用函数也是父->子,因此父组件的副作用渲染函数的 effect.id 小于子组件
    2、确保子组件在父组件的更新过程中被卸载就不更新自身了
  */
  queue.sort((a, b) => getId(a) - getId(b))

  while ((job = queue.shift()) !== undefined) {
    if (job === null) continue

    // 在非生产环境下检测是否有循环更新,即在侦听器的回调函数中更改了依赖响应式对象的值,从而导致死循环
    if ((process.env.NODE_ENV !== 'production')) {
      checkRecursiveUpdates(seen, job)
    }
    callWithErrorHandling(job, null, 14/* SCHEDULER */)
  }

  // 遍历执行异步任务队列
  flushPostFlushCbs(seen)
  isFlushing = false
  // 一些 postFlushCb 执行过程中会再次添加异步任务,递归 flushJobs 直到把它们都执行完毕
  if (queue.length || postFlushCbs.length) {
    flushJobs(seen)
  }
}

/**
 * 遍历执行所有推入到 postFlushCbs 的函数
 */
function flushPostFlushCbs(seen) {
  if (postFlushCbs.length) {
    // 在 cb 的执行过程中可能会修改 postFlushCbs,因此通过拷贝副本的方式以防止收到其影响
    const cbs = [...new Set(postFlushCbs)]
    postFlushCbs.length = 0
    if ((process.env.NODE_ENV !== 'production')) {
      seen = seen || new Map()
    }
    for (let i = 0; i < cbs.length; i++) {
      if ((process.env.NODE_ENV !== 'production')) {
        checkRecursiveUpdates(seen, cbs[i])
      }
    }
  }
}

/**
 * 检测是否存在循环更新
 */
const RECURSION_LIMIT = 100 // 循环的最大次数
function checkRecursiveUpdates(seen, fn) {
  // 第一次添加 fn
  if (!seen.has(fn)) {
    seen.set(fn, 1)
  }
  // 多次添加 fn
  else {
    const count = seen.get(fn)
    // 超出限制次数,则报错
    if (count > RECURSION_LIMIT) {
      throw new Error(/* 报错内容 */)
    }
    // 没有超出限制次数,则计数 +1
    else {
      seen.set(fn, count + 1)
    }
  }
}

优化:只使用一个变量

从功能上讲,isFlushPending 和 isFlushing 的作用主要有两点:

  1. 在一个 Tick 内可以多次添加任务到队列中,但是任务队列会在 nextTick 后执行
  2. 在任务队列的过程中,也可以添加新的任务到队列中,在当前 Tick 去执行剩余的任务队列

优化思路:

  • 去掉 isFlushPending 变量,默认为 true 可以执行异步任务,执行完毕后再设置为 false,禁止执行异步任务

优化后的代码:

function queueFlush() {
  if (!isFlushing) {
    isFlushing = true
    nextTick(flushJobs)
  }
}

function flushJobs(seen) {
  let job
  if ((process.env.NODE_ENV !== 'production')) {
    seen = seen || new Map()
  }
  queue.sort((a, b) => getld(a) - getld(b))
  while ((job = queue.shift()) !== undefined) {
    if (job === null) {
      continue
    }
    if ((process.env.NODE_ENV !== 'production')) {
      checkRecursiveUpdates(seen, job)
    }
    callWithErrorHandling(job, null, 14/* SCHEDULER */)
  }
  flushPostFlushCbs(seen)
  if ((queue.length || postFlushCbs.length)) {
    flushJobs(seen)
  }
  isFlushing = false
}

watchEffect

watchEffect 的作用是注册一个副作用函数,副作用函数内部可以访问到响应式对象,当内部响应式对象变化后再立即执行这个函数。

与 watch 的区别:

  1. 侦听源不同
    • watch 手动设置侦听源
    • watchEffect 自动侦听依赖响应式对象
  2. 是否懒执行
    • watch 是懒执行,即初次不会执行,需要设置 options: { immediate: true } 才会立即执行
    • watchEffect 会立即执行
/**
 * watchEffect 侦听器
 */
function watchEffect(effect, options) {
  return doWatch(effect, null, options)
}

/**
 * 侦听
 */
function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
  instance = currentlnstance
  let getter // source 包装函数
  if (isFunction(source)) {
    getter = () => {
      // 判断组件实例是否销毁
      if (instance && instance.isUnmounted) return

      // 执行清理函数
      if (cleanup) {
        cleanup()
      }

      // 执行 source 函数,传入 onInvalide 作为参数
      return callWithErrorHandling(source, instance, 3/* WATCH_CALLBACK */, [onInvalidate])
    }
  }

  let cleanup // 无效回调函数
  const onInvalidate = (fn) => {
    cleanup = runner.options.onStop = () => {
      callWithErrorHandling(fn, instance, 4/* WATCH_CLEANUP */)
    }
  }
  let scheduler
  // 创建 scheduler
  if (flush === 'sync') {
    scheduler = invoke
  } else if (flush === 'pre') {
    sheduler = job => {
      if (!instance || instance.isMounted) {
        queueJob(job)
      } else {
        job()
      }
    }
  } else {
    scheduler = job => queuePostRenderEffect(job, instance && instance.suspense)
  }

  // 创建 runner
  const runner = effect(getter, {
    lazy: true,
    computed: true,
    onTrack,
    onTrigger,
    scheduler
  })
  recordInstanceBoundEffect(runner);

  // 立即执行 runner
  runner();

  // 返回销毁函数
  return () => {
    stop(runner);
    if (instance) {
      remove(instance.effects, runner);
    }
  }
}

问题:在组件中创建的自定义 watcher,在组件销毁的时候会被销毁吗,是如何做的呢?

答:会,通过访问对象属性时的回调函数

问题:动态创建的 watcher 会销毁吗?

答:不会,需要手动清除。

你可能感兴趣的:(Vue,前端,javascript,vue.js)