likes
comments
collection
share

Vue3源码学习——侦听器Watch

作者站长头像
站长
· 阅读数 47

前言

上一节我们在看nextTick相关的源码时,发现 Vue3 关于任务执行顺序方面是有特定的一套逻辑的:

  • 首先,更新那些带有pre属性的job
  • 然后,执行那些普通的job
  • 最后,执行flushPostFlushCbs函数去处理后置任务队列中的job

这里要是感觉有点遗忘的话,可以再回顾一下《nextTick源码》这篇文章。

我们可以发现 Vue3 首先会执行带有pre属性的任务,那到底什么样的任务会被赋上pre属性呢?这里就要引出我们今天要去探讨的内容——侦听器watch

watch

我们先来看一下官网介绍watch的基本用法:

const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
  console.log(`x is ${newX}`)
})

// getter 函数
watch(
  () => x.value + y.value,
  (sum) => {
    console.log(`sum of x + y is: ${sum}`)
  }
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
  console.log(`x is ${newX} and y is ${newY}`)
})

watch名为侦听器,在组合式 API 中,我们可以使用 watch 函数在每次响应式状态发生变化时触发回调函数。

根据上面的例子,我们可以发现watch 函数可以侦听的类型还是挺多的,可以是:

  • 单个ref
  • getter函数
  • 多个来源组成的数组

那么,它的内部逻辑是如何处理多类型参数的呢?我们看一下watch的源码:

function watch(source, cb, options) {
    // 如果传入的cb不是函数则给出警告
    if (!isFunction(cb)) {
      warn(xxx)
    }
    return doWatch(source, cb, options);
  }

watch函数的主体逻辑都放在doWatch函数里,doWatch函数比较长,我们一点一点分析:

getter

首先,会创建一个getter的变量,同时会分情况进行赋值:

function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
      ...
      // 获取当前实例
      const instance = currentInstance
      let getter: () => any
      let forceTrigger = false
      
      // source是否为数组
      let isMultiSource = false
      
      if (isRef(source)) {
        // source是ref类型:
        getter = () => source.value
        forceTrigger = isShallow(source)
      } else if (isReactive(source)) {
        // source是reactive类型:
        getter = () => source
        deep = true
      } else if (isArray(source)) {
        // 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)) {
        // source是函数类型:
        if (cb) {
          // 如果存在回调函数,getter赋值为执行source的函数
          getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
       } else {
             // 如果watch中并未传入cb,执行的内容即为watchEffect逻辑
             getter = () => {
                if (instance && instance.isUnmounted) {
                  return
                }
                if (cleanup) {
                  cleanup()
                }
                return callWithAsyncErrorHandling(
                  source,
                  instance,
                  ErrorCodes.WATCH_CALLBACK,
                  [onCleanup]
                )
              }
        }
      } else {
        // 将getter赋值为一个返回空对象的函数 () => {},同时给出警告
        getter = NOOP
        __DEV__ && warnInvalidSource(source)
      }
      
    ...
}

这里我们可以看到,doWatch函数首先做的就是根据source不同类型,定义不同的getter

  • ref类型:getter = () => source.value
  • reactive类型:getter = () => source
  • array类型:getter = () => source.map(...)
  • function类型(只考虑有cb的情况):getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
  • 其他:getter = () => {}

尽管getter的形式各异,但是它们有一个共同的特点都是一个函数,是可以被执行的。

我们可以回想一下,在之前聊到响应式的时候,通常我们说的getter,都是用来进行依赖收集的,那这里的getter和那边的getter是一样的作用吗?也是用于依赖收集的吗?

这里先剧透一下,doWatch函数这里的getter在后面会被包装为一个副作用函数effect,并会在相应的时机去执行effect.run方法,也就触发了getter函数的执行,完成副作用函数的收集

我们接着往下看:

function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
    ...
    // 根据source不同定义不同的getter
    ...
    
    // 如果是深度监听,则在getter中会去执行traverse方法
    if (cb && deep) {
        const baseGetter = getter
        getter = () => traverse(baseGetter())
    }
  ...
}

这里涉及到了深度监听,如果deep属性为true的话,则会将getter重新赋值为一个新的函数,这个函数通过调用traverse函数,从而实现深度访问的效果。

traverse

traverse函数的作用是,通过递归调用自身去深度访问接收到的变量,从而完成深度依赖的收集

function traverse(value, seen) {
    if (!isObject(value) || value["__v_skip" /* SKIP */]) {
      return value;
    }
    seen = seen || /* @__PURE__ */ new Set();
    // 如果已经访问过,则直接返回
    if (seen.has(value)) {
      return value;
    }
    // 将value进行缓存
    seen.add(value);
    
    // 根据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) => {
        traverse(v, seen);
      });
    } else if (isPlainObject(value)) {
      for (const key in value) {
        traverse(value[key], seen);
      }
    }
    return value;
}

主要逻辑

回到doWatch函数

var INITIAL_WATCHER_VALUE = {}

function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
    ...
    
    // 定义旧值变量,初始变量都为undefined
    let oldValue = isMultiSource ? new Array(source.length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE;
    
    const job = () => {
      if (!effect.active) {
        return;
      }
      if (cb) {
        // 如果传入watch函数有回调函数cb,则通过执行effect.run方法,拿到执行后的结果
        const newValue = effect.run();
        
        // 传入的source含有响应式或有深度监听deep配置
        if (deep || forceTrigger || (isMultiSource ? newValue.some(
          (v, i) => hasChanged(v, oldValue[i])
        ) : hasChanged(newValue, oldValue)) || false) {
          ...
          
          // 执行cb函数,同时会将newValue和oldValue作为参数传递给回调函数
          callWithAsyncErrorHandling(cb, instance, 3 /* WATCH_CALLBACK */, [
            newValue,
            // 初始旧值赋值为undefined
            oldValue === INITIAL_WATCHER_VALUE ? void 0 : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue,
            onCleanup
          ]);
          oldValue = newValue;
        }
      } else {
        // 如果没有cb函数,则将watch当做watchEffect直接执行source函数
        effect.run();
      }
    };
    
    job.allowRecurse = !!cb;
    let scheduler;
    // flush是watch函数的第三个参数options中的其中一个属性,用于配置回调函数的触发时机
    if (flush === "sync") {
      scheduler = job;
    } else if (flush === "post") {
      scheduler = () => queuePostRenderEffect(job, instance && instance.suspense);
    } else {
     // 默认会为job赋上pre属性,从而使得watch中的回调函数会处于任务队列的前列,排在DOM更新函数之前
      job.pre = true;
      if (instance)
        job.id = instance.uid;
      scheduler = () => queueJob(job);
    }
    
    // 将getter包装成一个effect副作用函数,而副作用函数的执行顺序则由上面的scheduler决定
    const effect = new ReactiveEffect(getter, scheduler);
    
    if (cb) {
      // 如果是立即执行
      if (immediate) {
        job();
      } else {
        // 不是立即执行,则执行run 函数,获取旧值
        oldValue = effect.run();
      }
    } else {
        // watchEffect相关的逻辑
        ...
    }
    
    ...
}

这一块逻辑属于doWatch函数的主体逻辑了。

根据前面,我们知道doWatch函数已经处理好了getter,这一环节的主要工作就是:

  • 将watch的回调函数cb根据配置项flush的不同,将其维护进任务队列的不同位置,同时,将getter处理为副作用函数effect,并执行,从而完成副作用函数的收集
  • 因此,当我们的依赖项发生变化的时候,就会触发副作用函数的执行,从而会触发传入watch的cb函数执行,并将doWatch函数一开始维护的getter对应的oldValuenewValue也就是监听对象的新老值)作为cb的参数。

这里watch函数中的配置项flush取值可以是:

  • pre,默认,侦听器将在组件渲染之前执行。
  • post,侦听器在组件渲染后执行,也就是回调函数能够访问到更新后的DOM。
  • sync,在某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器,也就是同步执行了。

unwatch

最后,还剩下关于取消监听部分的逻辑,我们善始善终,继续往下看:

function doWatch(source, cb, { immediate, deep, flush, onTrack, onTrigger } = EMPTY_OBJ) {
...

const unwatch = () => {
    effect.stop()
    if (instance && instance.scope) {
      remove(instance.scope.effects!, effect)
    }
  }
  
  return unwatch
}

doWatch函数的最后部分工作就比较简单了,定义一个取消监听的函数,通过执行effect.stop方法移除收集到的对应的副作用函数,并将unwatch方法暴露出去,这样我们就可以通过在外界调用从而取消监听了。

举个例子:

 let state = reactive({
    a: 1
  })

  let cancel = watch(state, (newVal) => {
    console.log(newVal.a)
  })

  setTimeout(() => {
    cancel()
    console.log('cancel了')
  }, 3000)

  function increment() {
    state.a++
  }

我们在一开始,每次调用increment方法,控制台都会打印改变后的state.a的值,但是当3秒之后,调用了cancel方法,则取消了watch侦听器,所以再调用increment方法state.a仍然会进行加一的操作,但控制台不再会打印state.a的值了。

总结

到这里,doWatch函数的整体逻辑基本上我们就过了一遍,整体在做的事情就是:

  1. 将watch函数传入的第一个变量source封装为格式一致的getter函数
  2. 根据配置项flush的不同,确定调度程序scheduler的值。
  3. getter函数结合scheduler包装为一个副作用函数effect
  4. 定义job函数,主要功能为执行effect函数的run方法,这里执行run方法就会执行我们前面步骤维护的getter,从而能够完成依赖收集
  5. 当监听的内容发生改变时,将getter获取到的值赋值给newValue,然后去执行watch的第二个参数,也就是监听器的回调函数cb,并将旧值oldValue和新值newValue作为cb的参数。
  6. 最后,返回一个取消监听的方法unwatch,可供用户调用,取消监听。

参考文章:

官网

大佬的小册

转载自:https://juejin.cn/post/7244802134905749541
评论
请登录