从源码了解 vue3 的组合式 api

246 阅读3分钟

「这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

今天来聊聊 vue3 的组合式 api,它到底是一个什么东西,有什么内容,是怎么实现的?

参考资料

老规矩,先上参考资料:

组合式 api 一览

  • ref()
  • reactive()
  • computed()
  • provide()
  • inject()
  • watch()
  • watchEffect()
  • 11个生命周期钩子:onBeforeMountonMountedonBeforeUpdateonUpdatedonBeforeUnmountonUnmountedonErrorCapturedonRenderTrackedonRenderTriggeredonActivatedonDeactivated 官网文档介绍的十分详细,想要了解如何使用直接到官网文档(vue3 官方文档 | 什么是组合式 API?)上看即可。

本文主要做两件事情:解读源码和分析此设计的优点。

解读源码

打开3.2.21版本的vue代码仓库后,想了解组合式 api是怎么定义的,可以切到 packages\runtime-core\src

apiLifecycle.ts是注册生命周期钩子的,关键函数是 injectHook

(已省略注释)

export function injectHook(
  type: LifecycleHooks,
  hook: Function & { __weh?: Function },
  target: ComponentInternalInstance | null = currentInstance,
  prepend: boolean = false
): Function | undefined {
  if (target) {
    const hooks = target[type] || (target[type] = [])
    const wrappedHook =
      hook.__weh ||
      (hook.__weh = (...args: unknown[]) => {
        if (target.isUnmounted) {
          return
        }
        pauseTracking()

        setCurrentInstance(target)
        const res = callWithAsyncErrorHandling(hook, target, type, args)
        unsetCurrentInstance()
        resetTracking()
        return res
      })
    if (prepend) {
      hooks.unshift(wrappedHook)
    } else {
      hooks.push(wrappedHook)
    }
    return wrappedHook
  } 
}

沿着injectHook -> callWithAsyncErrorHandling -> callWithErrorHandling的链路, 我们看到注入的hook函数(就是callWithErrorHandling中的fn参数)最后会被执行

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
}

而我们在setup中用到的 onMounted 等生命周期函数钩子就是 injectHook 函数体中的 wrappedHook。而这些钩子会在另一个地方解析组件的时候按生命周期关系依次被调用。

而关于 watch、watchEffect等函数可以在 apiWatch.ts看到其实现逻辑,而refreactive,computed函数的逻辑 在 packages/reactivity/src文件夹下,篇幅原因在这里不加讲解。

分析设计点

watchEffect 从名字上看和 react 的 useEffect 有点相似,让我忍不住想将二者做个对比。

  1. 由于实现原理的差异,watchEffect不像 react 的 hook 有着被禁止放在条件语句块中的限制,可以更灵活;
  2. useEffect的入参函数的返回值是一个清除函数,会在每次重新渲染前调用清除函数以清除上一次的副作用,因为副作用函数的返回值被设计成用来接收cleanup,副作用函数体的不能定义成 async 函数,也就是说以下代码是错误的
useEffect(async () => {
   const res = await fetch('xxxx')
   // ...
},[])

这是因为async函数的返回值必是一个Promise对象,这与useEffect要求入参函数返回值必须是函数或undefined矛盾 如果你的项目必须使用 async 取代 promise-then写法,就只能写成一个 IIFE 代码块

useEffect(() => {
    void (async () => {
        const res = await fetch('xxxx')
       // ...
    })()
},[])

这样感觉就很不优雅。 而 watchEffect 就不要求返回值是函数,watchEffect的副作用函数的入参是一个注册清除函数的函数,你在副作用函数体内调用它并将cleanup函数传给它。watchEffect的返回值就是一个cleanup函数的触发器,你可以手动调用它以清除副作用,你可以搭配 onBeforeUpdate 以实现类似 useEffect 的效果, 举例如下:

const cleanup = watchEffect(async un => {
    await asyncTask()
    // 订阅某事件
    // ....
    un(() => {
    //取消订阅该事件
    })
})

onBeforeMount(() => {
    cleanup()
})