Vue 3.0 setup 函数

363 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第 7 天,点击查看活动详情

背景

Vue.js 3.0 允许我们在编写组件的时候添加一个 setup 启动函数,它是 Composition API 逻辑组织的入口。那么 setup 到底是怎么执行的?我们按照源码捋一遍流程。

执行 setup

组件的渲染流程是:创建 vnode渲染 vnode生成 DOM

mountComponent

其中渲染 vnode 的过程主要就是在挂载组件:

function mountComponent(initialVNode, container, parentComponent) {
    // 1. 先创建一个 component instance
    const instance = (initialVNode.component = createComponentInstance(
      initialVNode,
      parentComponent
    ));
    console.log(`创建组件实例:${instance.type.name}`);
    // 2. 给 instance 加工加工
    setupComponent(instance);
    // 3. 设置并运行带副作用的渲染函数
    setupRenderEffect(instance, initialVNode, container);
}

挂载组件的代码主要做了三件事情:

  • 创建组件实例
  • 设置组件实例
  • 设置并运行带副作用的渲染函数

setupComponent

组件实例的设置流程,对 setup 函数的处理就在 setupComponent 方法里完成:

export function setupComponent(instance) {
  // 1. 处理 props
  // 取出存在 vnode 里面的 props
  const { props, children } = instance.vnode;
  initProps(instance, props);
  // 2. 处理 slots
  initSlots(instance, children);

  // 源码里面有两种类型的 component
  // 一种是基于 options 创建的
  // 还有一种是 function 的
  // 这里处理的是 options 创建的
  // 叫做 stateful 类型
  setupStatefulComponent(instance);
}

setupStatefulComponent

接下来我们要关注到 setupStatefulComponent 函数:

function setupStatefulComponent(instance) {
  // 1. 先创建代理 proxy
  // proxy 对象其实是代理了 instance.ctx 对象
  // 我们在使用的时候需要使用 instance.proxy 对象
  // 因为 instance.ctx 在 prod 和 dev 坏境下是不同的
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
  // 用户声明的对象就是 instance.type
  // const Component = {setup(),render()} ....
  const Component = instance.type;
  
  // 2. 调用 setup
  // 调用 setup 的时候传入 props
  const { setup } = Component;
  if (setup) {
    // 设置当前 currentInstance 的值
    // 必须要在调用 setup 之前
    setCurrentInstance(instance);

    const setupContext = createSetupContext(instance);
    
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [instance.props, setupContext]
    )
    setCurrentInstance(null);

    // 3. 处理 setupResult
    handleSetupResult(instance, setupResult);
  } else {
    // 完成组件实例设置
    finishComponentSetup(instance);
  }
}

setupStatefulComponent 主要做了三件事:

  • 创建渲染上下文代理
  • 判断处理 setup 函数
  • 完成组件实例设置

创建渲染上下文代理

创建渲染上下文代理主要对 instance.ctx 做了代理。

这里为什么需要代理呢?在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性,所以要做一层 proxy,对渲染上下文 instance.ctx 属性的访问和修改。

PublicInstanceProxyHandlers

export const PublicInstanceProxyHandlers = {
  get({ _: instance }, key) {
    // 用户访问 proxy[key]
    // 这里就匹配一下看看是否有对应的 function
    // 有的话就直接调用这个 function
    const { setupState, props } = instance;

    if (key[0] !== "$") {
      // 说明不是访问 public api
      // 先检测访问的 key 是否存在于 setupState 中, 是的话直接返回
      if (hasOwn(setupState, key)) {
        return setupState[key];
      } else if (hasOwn(data, key)) { 
        return data[key]; 
      } else if (hasOwn(props, key)) {
        // 看看 key 是不是在 props 中
        // 代理是可以访问到 props 中的 key 的
        return props[key];
      }
    }

    const publicGetter = publicPropertiesMap[key];

    if (publicGetter) {
      return publicGetter(instance);
    }
  },

  set({ _: instance }, key, value) {
    const { setupState } = instance;

    if (setupState !== {} && hasOwn(setupState, key)) {
      // 有的话 那么就直接赋值
      setupState[key] = value;
    }

    return true
  },
};

可以看到,函数首先判断 key 不以 $ 开头的情况,这部分数据可能是 setupState、data、props、ctx 中的一种;setupState 就是 setup 函数返回的数据。

如果 key 不以 $ 开头,那么就依次判断 setupState、data、props、ctx 中是否包含这个 key,如果包含就返回对应值。注意这个判断顺序很重要在 key 相同时它会决定数据获取的优先级

判断处理 setup 函数

处理 setup 函数的流程,主要是三个步骤:

  1. 创建 setup 函数上下文
  2. 执行 setup 函数并获取结果
  3. 处理 setup 函数的执行结果

createSetupContext

setup 函数接收两个参数,第一个参数 props 对应父组件传入的 props 数据,第二个参数就是 setupContext。

function createSetupContext(instance) {
  console.log("初始化 setup context");
  return {
    attrs: instance.attrs,
    slots: instance.slots,
    emit: instance.emit,
    expose: () => {},
  };
}

callWithErrorHandling

function callWithErrorHandling (fn, instance, type, args) {
  let res
  try {
    res = args ? fn(...args) : fn()
  } catch (err) {
    handleError(err, instance, type)
  }
  return res
}

可以看到,它其实就是对 fn 做的一层包装,内部还是执行了 fn,并在有参数的时候传入参数,所以 setup 的第一个参数是 instance.props,第二个参数是 setupContext。函数执行过程中如果有 JavaScript 执行错误就会捕获错误,并执行 handleError 函数来处理。

handleSetupResult

function handleSetupResult(instance, setupResult) {
  // setup 返回值不一样的话,会有不同的处理
  // 1. 看看 setupResult 是个什么
  if (typeof setupResult === "function") {
    // 如果返回的是 function 的话,那么绑定到 render 上
    // 认为是 render 逻辑
    // setup(){ return ()=>(h("div")) }
    instance.render = setupResult;
  } else if (typeof setupResult === "object") {
    // 返回的是一个对象的话
    // 先存到 setupState 上
    // 先使用 @vue/reactivity 里面的 proxyRefs
    // 后面我们自己构建
    // proxyRefs 的作用就是把 setupResult 对象做一层代理
    // 方便用户直接访问 ref 类型的值
    // 比如 setupResult 里面有个 count 是个 ref 类型的对象,用户使用的时候就可以直接使用 count 了,而不需要在 count.value
    // 这里也就是官网里面说到的自动结构 Ref 类型
    instance.setupState = proxyRefs(setupResult);
  }

  finishComponentSetup(instance);
}

可以看到,当 setupResult 是一个对象的时候,我们把它变成了响应式并赋值给 instance.setupState,这样在模板渲染的时候,依据前面的代理规则,instance.ctx 就可以从 instance.setupState 上获取到对应的数据,这就在 setup 函数与模板渲染间建立了联系。

完成组件实例设置

finishComponentSetup

function finishComponentSetup(instance) {
  // 给 instance 设置 render

  // 先取到用户设置的 component options
  const Component = instance.type;

  if (!instance.render) {
    // 如果 compile 有值 并且当然组件没有 render 函数,那么就需要把 template 编译成 render 函数
    if (compile && !Component.render) {
      if (Component.template) {
        // 这里就是 runtime 模块和 compile 模块结合点
        const template = Component.template;
        Component.render = compile(template);
      }
    }

    instance.render = Component.render;
  }
  
  // 兼容 Vue.js 2.x Options API
  applyOptions()
}

finishComponentSetup 函数主要做了两件事情:

  1. 标准化模板或者渲染函数
  2. 兼容 Options API

标准化模板或者渲染函数

通常会使用两种方式开发组件:

1、使用 SFC(Single File Components)单文件的开发方式来开发组件

即通过编写组件的 template 模板去描述一个组件的 DOM 结构。我们知道 .vue 类型的文件无法在 Web 端直接加载,因此在 webpack 的编译阶段,它会通过 vue-loader 编译生成组件相关的 JavaScript 和 CSS,并把 template 部分转换成 render 函数添加到组件对象的属性中。

2、不借助 webpack 编译,直接引入 Vue.js

开箱即用,我们直接在组件对象 template 属性中编写组件的模板,然后在运行阶段编译生成 render 函数。

Options API:兼容 Vue.js 2.x

function applyOptions(instance, options, deferredData = [], deferredWatch = [], asMixin = false) {

  const {
    // 组合
    mixins, extends: extendsOptions,
    // 数组状态
    props: propsOptions, data: dataOptions, computed: computedOptions, methods, watch: watchOptions, provide: provideOptions, inject: injectOptions,
    // 组件和指令
    components, directives,
    // 生命周期
    beforeMount, mounted, beforeUpdate, updated, activated, deactivated, beforeUnmount, unmounted, renderTracked, renderTriggered, errorCaptured 
    } = options;

  // instance.proxy 作为 this
  const publicThis = instance.proxy;
  const ctx = instance.ctx;

  // 处理全局 mixin
  // 处理 extend
  // 处理本地 mixins
  // props 已经在外面处理过了
  // 处理 inject
  // 处理 方法
  // 处理 data
  // 处理计算属性
  // 处理 watch
  // 处理 provide
  // 处理组件
  // 处理指令
  // 处理生命周期 option
}

参考源码

github.com/vuejs/core

packages/runtime-core/src/renderer.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentProxy.ts
packages/runtime-core/src/errorHandling.ts