阅读 316

Vue3 watchEffect 源码学习 | 8月更文挑战

前言

在看 reactive 源码的时候发现单独调用 reactive 函数并没有收集依赖,那么什么时候才会收集依赖呢,我把之前的 demo 改了改,加入了 watchEffect 之后发现在 watchEffect 中用到的属性会被收集依赖,那为什么调用了 watchEffect 之后就会收集依赖呢,让我们进入到源码中一探究竟吧。

更改后的 demo:

 <script>
     ...
     debugger
     watch((event) => {
         return data.a
     }, (newVal, oldVale) => {
         debugger
     })
     // 收集依赖 track()
     watchEffect(() => {
         /**
          * data.a // getter
          * data.count = // setter trigger()
          **/
         data.count = data.a + 1
         console.log('count: ', data.count)
     })
 </script>
复制代码

我们还是以 debug 的方式进入。

开始吧

在 demo 中我们加入了 watch 函数是因为 watchwatchEffect 都调用了同一个函数 doWatch,只不过是参数不一样。

watch & watchEffect

在这两个函数中可以看到,都是调用 doWatch 函数,将其返回值 return。接下来直接看 doWatch 函数。

doWatch

doWatch 接收四个参数

  • source 监听的源
  • callback 监听的回调函数
  • options 监听的其他选项
  • currentInstance 当前组件实例

这个时候看一下 watchEffectwatch 调用它的时候有什么区别

  • watchEffect 在调用的时候 callback 传入的是 null
  • watch 在调用的时候传入的是 callback,这个 callback 也是 watch 函数的一个参数

回到 doWatch 函数,先做了一个开发环境下的警告处理,又定义了一个 warnInvalidSource 警告处理的方法。

下面判断了 source 分别是 ref、reactive、array、function 类型的时候 getter 函数的不同定义,但是 getter 的宗旨是不变的,就是用来求值:

  • ref 类型通过 .value 获取值

     const refVal = ref(0)
     watch(ref, (newVal, oldVal) => {})
    复制代码
  • reactive 类型直接将 source retrun

     const data = reactive({
         a: 1
     })
     watch(data, (newVal, oldVal) => {})
    复制代码
  • array 类型要针对每一项的类型来求值

     const refVal = ref(0)
     const data = reactive({})
     const fun = () => 0
     const arr = [
         refVal, // .value 求值
         data, // 递归调用 traverse 函数
         fun // 调用 callWithErrorHandling 函数执行 source
     ]
    复制代码

    traverse 函数是将数组的每一项传进去,判断这一项是什么类型,递归调用,然后将其结果 return。

    callWithErrorHandling 函数就是调用第一个参数,如果有第四个参数,则在调用的时候将其传入。

  • function 类型就要判断有没有 callback,有的话调用 callWithErrorHandling 函数来执行 source。没有的话说明是 watchEffect 调用的,这时候 getter 函数中如果当前实例被卸载了则 return,然后调用 cleanup 函数,调用 callWithAsyncErrorHandling 函数执行 sourcecleanup 函数在下边会有定义,callWithAsyncErrorHandling 函数和 callWithErrorHandling 函数的作用是一样的,区别在于 callWithAsyncErrorHandling 多了一个对 promise 错误捕获。在调用 callWithAsyncErrorHandling 的时候传入了第四个参数,实参为 onInvalidate 函数,这样就可以解释我们在调用 watchEffect 的时候,回调函数里边会有一个参数,参数值就是 onInvalidate 函数。onInvalidate 函数的作用见文档

做完上边的判断下边是做了对 2.x 版本监听数组的一个兼容。

然后又判断了如果是由 watch 调用的并且设置了 deep 属性为 true 则进行深度监听。

后边定义了上边提到的 cleanuponInvalidate

 let cleanup: () => void
 let onInvalidate: InvalidateCbRegistrator = (fn: () => void) => {
     cleanup = runner.options.onStop = () => {
         // 这里会调用 fn
         callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP)
     }
 }
复制代码

onInvalidate 函数接收一个 function 类型的参数 fn,函数内部是将 fn 注册到了当前 effectonStop 函数上,然后将 onStop 又赋值给了 cleanup,这样做的好处是,在执行 cleanup 的时候 fn 也就可以被执行了。

后边针对服务端渲染做了处理,当由 watchEffect 调用的时候直接调用 getter,反之由 watch 调用并且 immediatetrue 的时候立刻调用 callback 并返回,因为 watch 他是惰性的,如果设置了 immediatetrue 则需要立刻返回。

下面定义了 job 函数,job 就是来执行 watchwatchEffecct 两种不同的操作的。

 const job: SchedulerJob = () => { 
     if (!runner.active) {
         return
     }
 ​
     if (cb) {
         // 由 watch 调用
         ...
     }
     else {
         // 对于 watchEffect 直接调用传入的函数
         runner()
     }
 }
复制代码

job 内部如果是由 watch 调用的则如果 newValueoldValue 不一样的话,调用 cleanup,并且调用 callback

如果是由 watchEffect 调用的则直接调用传入的函数,这个 runner 函数下边会有说。

下边有这么一行代码

 job.allowRecurse = !!cb
复制代码

根据代码中的注释可以知道这行代码的意思是允许递归调用,也就是允许 watch 的 callback 中修改正在监听的值,使当前这个 watch 可以监听到正在监听的这个值被改变了。也就是这样

 const num = ref(0)
 watch(num, (newVal, oldVal) => {
     if (newVal < 10) {
         num.value = newVal + 1
     }
 })
复制代码

继续往下看

 let scheduler: ReactiveEffectOptions['scheduler']
 // 同步直接 job
 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'
     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.
             // instance 存在但是实例还没有挂载,这个时候需要直接执行 job
             job()
         }
     }
 }
复制代码

这里定义了一个调度器,调度器的作用是根据 flush 来判断更新时机,也就是什么时候去调用上边定义的 jobflush 等于 sync 的时候同步调用;等于 post 的时候异步调用,将 job 添加到微任务队列中,调用时机交给事件循环;等于 pre 也就是默认的时候有两种情况,第一种是组件实例不存在或者实例已经挂载的时候也需要将 job 添加到任务队列中,第二种是组件实例存在但是还没有挂载这个时候直接执行 job

再往下就定义了 runner 这个 runner 在前边也提到过,一次是在定义 onInvalidate 函数中定义了 runner 上的 onStop 函数,另外一次是在定义 job 的时候如果是 watchEffect 调用的话直接执行了 runner。这次我们来看看这个 runner 到底是什么

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

这里终于用到了上边定义的 getter 和调度器 scheduler。进入 effect 函数

 export function effect<T = any>(
 fn: () => T,
  options: ReactiveEffectOptions = EMPTY_OBJ
 ): ReactiveEffect<T> {
     // 如果 fn 是一个 effect 则将 fn 改为原始对象
     if (isEffect(fn)) {
         fn = fn.raw
     }
     const effect = createReactiveEffect(fn, options)
     if (!options.lazy) {
         effect()
     }
     return effect
 }
复制代码

它在一首一尾有两个判断,如果传入的 fn 是一个 effect 则将其改为原始对象;如果传入的是 lazyfalse 则需要调用这个方法内的 effect, 而这个 effect 是在这两个判断中间的 createReactiveEffect 函数的返回值。看到这里就先不去看 createReactiveEffect 函数了,先将其记下,等把 doWatch 这个方法看完之后再回头来看这个函数。

定义完 runner 之后又执行了 recordInstanceBoundEffect 函数,这个函数的作用是将 runner push 到当前组件实例的 effects 数组中。也就是当前的 runner 要和当前的组件实例绑定,以便在销毁组件实例的时候一同销毁。

后边判断了如果是 watch 就判断了 immediate 是否为 true 如果是的话则会立即调用 job,反之调用 runner 即可,并将 runner 的返回值给 oldValue,以便后期 newvalueoldValue 做比对。

如果不是则判断 flush 是否等于 posttrue 则调用 queuePostRenderEffect 函数将 runner 放到事件循环中,交给事件循环去触发。

如果上边的条件都不满足则直接调用 runner 函数。

最后 return 了一个函数,函数中调用了 stop 函数,如果实例存在再去调用 remove 函数删除当前实例上的这个 runner。这也就正好和 watchEffect 会返回一个函数去停止当前这个 watchEffect 这个定义对上了。

createReactiveEffect

前边定义 runner 的时候调用了 effect 函数,在 effect 函数中又调用了 createReactiveEffect 函数,现在来看一下这个函数干了什么。

 function createReactiveEffect<T = any>(
 fn: () => T,
  options: ReactiveEffectOptions
 ): ReactiveEffect<T> {
     const effect = function reactiveEffect(): unknown {
        // ...
     } as ReactiveEffect
     effect.id = uid++
     effect.allowRecurse = !!options.allowRecurse
     effect._isEffect = true
     effect.active = true
     effect.raw = fn
     effect.deps = []
     effect.options = options
     return effect
 }
复制代码

这个函数接收了两个参数,都是调用外部 effect 函数时传入的

  • fn 对应的外部的 getter
  • options 对应外部的 { lazy: true, onTrack, onTrigger, scheduler }

createReactiveEffect 函数内部定义了一个 effect,它的值是 reactiveEffect 函数,然后又给 effect 加了很多属性

 effect.id = uid++
 effect.allowRecurse = !!options.allowRecurse
 effect._isEffect = true
 effect.active = true
 effect.raw = fn
 effect.deps = []
 effect.options = options
复制代码

然后将 effect return 出去。

看一下 reactiveEffect 函数

 const effect = function reactiveEffect(): unknown {
     if (!effect.active) {
         return fn()
     }
     if (!effectStack.includes(effect)) {
         /**
        * 这里清空依赖是为了后续能够只添加这次有需要的依赖
        * https://juejin.cn/post/6909698939696447496#heading-14 文章中有讲
        */
         cleanup(effect)
         try {
             // ...
         } finally {
             // ...
         }
     }
 } as ReactiveEffect
复制代码

这个方法里如果 activefalse 的话则调用 fn 并将其结果 return

如果 effectStack 中不包含 effect 则执行下边的代码,effectStack 是当前页面定义的 array 类型的变量,用来存放这里定义的 effect

后边紧接着执行了 cleanup,前边也提到过一个 cleanup,但是这里的这个和前边的那个并不一样

 function cleanup(effect: ReactiveEffect) {
   const { deps } = effect
   if (deps.length) {
     for (let i = 0; i < deps.length; i++) {
       deps[i].delete(effect)
     }
     deps.length = 0
   }
 }
复制代码

这个 cleanup 传入了一个 effect,主要是将 effect 中的 deps 清空,后续重新添加依赖。这个结论我是看这篇文章的得到的。

这里的 deps 是哪里来的呢,可以看一下上边给 effect 添加了好多属性,其中一个属性就有 deps。还有在 reactive 源码中的 track 函数中也提到了这个 deps,代码是这样的: activeEffect.deps.push(dep)

下面开始执行 try 中的代码

 enableTracking()
 effectStack.push(effect)
 activeEffect = effect
 return fn()
复制代码

先调用了 enableTracking 函数,函数中将 shouldTrack push 到了 trackStack 中,然后将 shouldTrack 置为 truetrackStackshouldTrack 都是当前页面声明的变量,trackStack 是一个 array 类型的变量,用来存放 shouldTrack 这个变量,shouldTrackboolean 类型的变量。

后续将 effect push 到了 effectStack 中,将 effect 赋值给了 activeEffect。走到这一步,可以联想到 track 函数中一开始的那个判断

 if (!shouldTrack || activeEffect === undefined) {
     return
 }
复制代码

当调用 watchEffect 函数并且执行到这一步的时候,在后续 track 函数收集依赖的时候就不会被 return 可以收集依赖了。

回到 try 中的代码,将 effect 赋值给了 activeEffect 后调用了 fn 并将返回值 return

因为还有 finally 所以即使 return 也会执行,因为 fn 已经调用了,所以将前边赋的值重置

 effectStack.pop()
 resetTracking()
 activeEffect = effectStack[effectStack.length - 1]
复制代码

resetTracking 函数用来删掉 trackStack 的最后一个,因为 enableTracking 函数是 push 进去的。

接着再将 shouldTrack 重置。

以上就是 createReactiveEffect 中的所有代码。

reactive 源码一文中遗留的问题

问:什么时候会收集依赖

答:调用了 watchEffect 函数之后就会收集依赖,因为代码执行过程中会将 activeEffect 进行赋值,在 track 函数中不会被 return,所以会正常收集依赖。

参考链接

文章分类
前端
文章标签