从零手写简易Vue3(二)—— setup()

3,392 阅读4分钟

本文使用的vue版本为3.0.2

setup()是什么?

从 vue3 的官方文档提炼一下信息:

定义:一个新的组件选项。

产生的背景: 当我们的组件越来越复杂时,不同的逻辑关注点也会越来越多,这会导致组件难以阅读和理解,尤其是对于没有从一开始就参与进来的开发人员而言。

作用: 将同一个逻辑关注点相关的代码配置在一起,避免处理单个逻辑关注点时,必须不断地“跳转”相关代码的选项块。

用法:

  • 调用时机:组件被创建之前,所以没有this,无法访问data,methods,computed
  • 参数(props,context)creataApp的参数props和 2.x 版本中实例this上的 3 个属性attrs,slots,emit
  • 返回值是一个对象:该对象可以被组件的其余部分(computed、methods、声明周期钩子、组件模板等)使用。
  • 返回值是一个render函数:只能使用在同一作用域中声明的响应式状态(无法使用例如 data、computed 中的响应式状态)
<script>
import { h,ref } from "vue";
export default {
  data() {
    return {
      dataInData: "dataInData",
    };
  },
  computed: {
    dataInComputed() {
      return "dataInComputed";
    },
  },
  setup() {
    const dataInSetup = ref('dataInSetup')
    return () =>
      h("div", [
        dataInData, // error, dataInData is not defined
        dataInComputed, // error, dataInComputed is not defined
        dataInSetup.value, // work
      ]);
  },
};
</script>

源码分析 setup()都做了些什么

与本章主要内容关联不大的源码均不再展开,只在注释中做一下简单说明

setup()的调用

setup()部分代码的源头位于上一篇介绍创建应用或实例的baseCreateRenderer()中。在挂载应用或实例时,会第一次触发patch()方法,setup()在这时会被调用。

// packages/runtime-core/src/renderer.ts

const patch: PatchFn = (
    n1,
    n2,
    container,
    anchor = null,
    parentComponent = null,
    parentSuspense = null,
    isSVG = false,
    optimized = false
  ) => {
    if (n1 && !isSameVNodeType(n1, n2)) {
        // 新老VNode类型不同时,解绑老的dom
    }

    const { type, ref, shapeFlag } = n2
    switch (type) {
      case Text:
          // 处理文本
        break
      case Comment:
	    // 处理注释
        break
      case Static:
	    // 处理静态节点
        break
      case Fragment:
	    // 处理代码段
        break
      default:
        if (shapeFlag & ShapeFlags.ELEMENT) {
	    // 处理Element
        } else if (shapeFlag & ShapeFlags.COMPONENT) {
          processComponent(
            n1,
            n2,
            container,
            anchor,
            parentComponent,
            parentSuspense,
            isSVG,
            optimized
          )
        } else if (shapeFlag & ShapeFlags.TELEPORT) {
	    // 处理Teleport
        } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
	    // 处理suspense
          )
        } else if (__DEV__) {
          warn('Invalid VNode type:', type, `(${typeof type})`)
        }
    }

    // set ref
    if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentComponent, parentSuspense, n2)
    }
  }

可以看到,会根据n2(也就是 vue2.x 版本中的newVNode)的不同类型做相应的处理。

n2是一个组件时,调用processComponent方法。

const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    optimized: boolean
  ) => {
    if (n1 == null) {
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      updateComponent(n1, n2, optimized)
    }
  }Ï

n1不为null时,会对比新老 VNode 差异,更新 dom 节点。

n1null时,会根据n2是否为keep-alive组件执行不同的挂载逻辑。

下面进入正题,来看mountComponent方法。

const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  // 创建实例
  const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense
  ));

  // 开发环境注册热更新
  if (__DEV__ && (__BROWSER__ || __TEST__) && instance.type.__hmrId) {
    registerHMR(instance);
  }

  // 开发环境添加警告语的上下文
  // 开始记录mount过程的性能指标
  if (__DEV__) {
    pushWarningContext(initialVNode);
    startMeasure(instance, `mount`);
  }

  // 保存keep-alive组件的render方法
  if (isKeepAlive(initialVNode)) {
    (instance.ctx as KeepAliveContext).renderer = internals;
  }

  // 开始记录init过程的性能指标
  if (__DEV__) {
    startMeasure(instance, `init`);
  }

  // 调用setup()
  setupComponent(instance);

  // 结束记录init过程的性能指标,与上面的start对应
  if (__DEV__) {
    endMeasure(instance, `init`);
  }

  // 为未来支持异步setup()留坑
  if (__FEATURE_SUSPENSE__ && instance.asyncDep) {
    parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect);

    // Give it a placeholder if this is not hydration
    // TODO handle self-defined fallback
    if (!initialVNode.el) {
      const placeholder = (instance.subTree = createVNode(Comment));
      processCommentNode(null, placeholder, container!, anchor);
    }
    return;
  }

  // 定义实例更新视图的方法
  setupRenderEffect(
    instance,
    initialVNode,
    container,
    anchor,
    parentSuspense,
    isSVG,
    optimized
  );

  // 结束记录mount的性能指标,与上面的start对应
  if (__DEV__) {
    popWarningContext();
    endMeasure(instance, `mount`);
  }
};
// packages/runtime-core/src/component.ts

export function setupComponent(
  instance: ComponentInternalInstance,
  isSSR = false
) {
  isInSSRComponentSetup = isSSR;

  const { props, children, shapeFlag } = instance.vnode;
  const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT;

  // 初始化组件的props和slots
  initProps(instance, props, isStateful, isSSR);
  initSlots(instance, children);

  // 安装有状态组件
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined;
  isInSSRComponentSetup = false;
  return setupResult;
}
function setupStatefulComponent(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions;

  // 开发环境校验组件、子组件、指令 名称的合法性
  if (__DEV__) {
    if (Component.name) {
      validateComponentName(Component.name, instance.appContext.config);
    }
    if (Component.components) {
      const names = Object.keys(Component.components);
      for (let i = 0; i < names.length; i++) {
        validateComponentName(names[i], instance.appContext.config);
      }
    }
    if (Component.directives) {
      const names = Object.keys(Component.directives);
      for (let i = 0; i < names.length; i++) {
        validateDirectiveName(names[i]);
      }
    }
  }

  // 创建缓存,优化访问速度
  instance.accessCache = Object.create(null);

  // 创建公用的代理,上一篇文章中app.mount()返回的proxy就是这里
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
  if (__DEV__) {
    exposePropsOnRenderContext(instance);
  }

  // 获取setup()方法
  const { setup } = Component;
  if (setup) {
    // 初始化setup()方法的上下文
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null);

    currentInstance = instance;

    // 暂停依赖收集
    pauseTracking();

    // 调用setup()方法,保存返回值
    const setupResult = callWithErrorHandling(
      setup,
      instance,
      ErrorCodes.SETUP_FUNCTION,
      [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
    );

    // 恢复依赖收集
    resetTracking();
    currentInstance = null;

    if (isPromise(setupResult)) {
      // 处理setup()是异步函数的情况
      if (isSSR) {
        // 服务端渲染逻辑
        return setupResult.then((resolvedResult: unknown) => {
          handleSetupResult(instance, resolvedResult, isSSR);
        });
      } else if (__FEATURE_SUSPENSE__) {
        // 给未来支持异步setup()留坑
        instance.asyncDep = setupResult;
      } else if (__DEV__) {
        warn(
          `setup() returned a Promise, but the version of Vue you are using ` +
            `does not support it yet.`
        );
      }
    } else {
      // 处理setup()返回值
      handleSetupResult(instance, setupResult, isSSR);
    }
  } else {
    // 没有setup()直接结束安装
    finishComponentSetup(instance, isSSR);
  }
}
  • 首先校验组件本身、子组件和指令的名称合法性
  • 创建访问缓存,创建公用的代理属性供外部访问
  • 在执行 setup()期间暂停了依赖收集,执行结束后恢复
  • 处理 setup()返回值

这里为什么要在执行时暂停收集依赖?

回到执行setup时包裹的callWithErrorHandling方法:

// packages/runtime-core/src/errorHandling.ts

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;
}

callWithErrorHandling的最后一个参数[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]其实就是setup的参数,与文档中(props,context)相对应。而在开发环境中,为了对一些错误操作做提示,对props包了一层 proxy 代理;由于props本身就是响应式的,这里再包的一层是无需重复收集依赖的。

处理 setup()返回值

export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
    // 返回值是函数,保存为render属性
    instance.render = setupResult as InternalRenderFunction;
  } else if (isObject(setupResult)) {
    // 返回值是对象
    if (__DEV__ && isVNode(setupResult)) {
      // 不能直接返回VNode
      warn(
        `setup() should not return VNodes directly - ` +
          `return a render function instead.`
      );
    }
    // setup returned bindings.
    // assuming a render function compiled from template is present.
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      instance.devtoolsRawSetupState = setupResult;
    }

    // 转化为响应式对象,暴露给其他地方使用
    instance.setupState = proxyRefs(setupResult);
    if (__DEV__) {
      exposeSetupStateOnRenderContext(instance);
    }
  } else if (__DEV__ && setupResult !== undefined) {
    warn(
      `setup() should return an object. Received: ${
        setupResult === null ? "null" : typeof setupResult
      }`
    );
  }
  finishComponentSetup(instance, isSSR);
}

这里的逻辑比较简单:

  • 判断setup不能返回 VNode、undefined、null 等。
  • proxyRefs方法判断返回的对象是否是响应式的,若不是,则转化为响应式对象。
  • 结束setup

结束安装

function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  // 获取实例的options
  const Component = instance.type as ComponentOptions;

  // 格式化template / render函数
  if (__NODE_JS__ && isSSR) {
    if (Component.render) {
      instance.render = Component.render as InternalRenderFunction;
    }
  } else if (!instance.render) {
    // could be set from setup()
    if (compile && Component.template && !Component.render) {
      if (__DEV__) {
        startMeasure(instance, `compile`);
      }

      // template转化为render
      Component.render = compile(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement,
        delimiters: Component.delimiters,
      });
      if (__DEV__) {
        endMeasure(instance, `compile`);
      }
    }

    instance.render = (Component.render || NOOP) as InternalRenderFunction;

    // 支持 with 代码块
    if (instance.render._rc) {
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      );
    }
  }

  // 兼容2.x版本
  if (__FEATURE_OPTIONS_API__) {
    currentInstance = instance;
    applyOptions(instance, Component);
    currentInstance = null;
  }

  // 开发环境对缺少 template 或 render 的情况报错
  if (__DEV__ && !Component.render && instance.render === NOOP) {
    /* istanbul ignore if */
    if (!compile && Component.template) {
      warn(
        `Component provided template option but ` +
          `runtime compilation is not supported in this build of Vue.` +
          (__ESM_BUNDLER__
            ? ` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
            : __ESM_BROWSER__
            ? ` Use "vue.esm-browser.js" instead.`
            : __GLOBAL__
            ? ` Use "vue.global.js" instead.`
            : ``) /* should not happen */
      );
    } else {
      warn(`Component is missing template or render function.`);
    }
  }
}

结束安装的过程主要做了 2 件事:

  1. 统一使用 render 函数
  2. 调用applyOptions转化setup中的逻辑,支持 2.x 版本

applyOptions方法很长,对其进行简化:

// packages/runtime-core/src/componentOptions.ts

export function applyOptions(
  instance: ComponentInternalInstance,
  options: ComponentOptions,
  deferredData: DataFn[] = [],
  deferredWatch: ComponentWatchOptions[] = [],
  deferredProvide: (Data | Function)[] = [],
  asMixin: boolean = false
) {
  const {
    // composition
    mixins,
    extends: extendsOptions,
    // state
    data: dataOptions,
    computed: computedOptions,
    methods,
    watch: watchOptions,
    provide: provideOptions,
    inject: injectOptions,
    // assets
    components,
    directives,
    // lifecycle
    beforeMount,
    mounted,
    beforeUpdate,
    updated,
    activated,
    deactivated,
    beforeDestroy,
    beforeUnmount,
    destroyed,
    unmounted,
    render,
    renderTracked,
    renderTriggered,
    errorCaptured
  } = options

// 注册 beforeCreate
callSyncHook(
  'beforeCreate',
  LifecycleHooks.BEFORE_CREATE,
  options,
  instance,
  globalMixins
)

// 处理全局mixin
applyMixins(
  instance,
  globalMixins,
  deferredData,
  deferredWatch,
  deferredProvide
)

// setup写法转化为options写法(2.x语法)
if (extendsOptions) {
...
}

// 处理自身mixin
if (mixins) {
    ...
}


// 配置初始化清单(与2.x保持一致)
// - props (在这个方法执行之前已经完成)
// - inject
// - methods
// - data (延迟执行,因为需要访问'this')
// - computed
// - watch (延迟执行,因为需要访问'this')

if (injectOptions) {
  // 处理inject
}

if (methods) {
  // 注册methods
}

if (dataOptions) {
  // 绑定data
}

if (computedOptions) {
  // 注册computed
}

if (watchOptions) {
  // 注册watch
}

if (provideOptions) {
  // 处理 provide
}

if (components) {
   // 注册子组件
}
if (directives) {
    // 注册指令
}

// 绑定各个声明周期钩子函数

if (beforeMount) {
  onBeforeMount(beforeMount.bind(publicThis))
}
if (mounted) {
  onMounted(mounted.bind(publicThis))
}
if (beforeUpdate) {
  onBeforeUpdate(beforeUpdate.bind(publicThis))
}
if (updated) {
  onUpdated(updated.bind(publicThis))
}
if (activated) {
  onActivated(activated.bind(publicThis))
}
if (deactivated) {
  onDeactivated(deactivated.bind(publicThis))
}
if (beforeUnmount) {
  onBeforeUnmount(beforeUnmount.bind(publicThis))
}
if (unmounted) {
  onUnmounted(unmounted.bind(publicThis))
}

可以看到,setup其实还是被转化为 2.x 的方式去执行的。

一张图简要概括:

至此,setup相关逻辑已经全部执行完毕。

总结

其实,setup过程所做的工作很简单:

  1. 执行函数内的逻辑
  2. 处理返回值:
    • 返回值是函数,保存为实例的render函数
    • 返回值是对象,转化为响应式对象,暴露出去
  3. setup语法转化为2.x版本的options语法

实现

基于上一篇的已有成果

在组件被创建之前调用setup()

const createApp = function (...args) {
  const render = createRender();
  const setupFun = args[0].setup
  if (setupFun) {
    const setupResult = setupFun()
    handleSetupResult(setupResult)
    /**
     * 转化options语法、注册声明周期钩子等内容在后续章节补充
     */
  }
  const app = {
    version: "0.0.1",
    mount(selector) {
      const container = document.querySelector(selector);
      const vnode = createVNode(container.innerHTML);
      container.innerHTML = "";
      render(vnode, container);
      return this;
    },
  };
  return app;
};

处理setup()的返回值

function handleSetupResult(res) {
  const type = Object.prototype.toString.call(res)
  if (type === '[object Object]') {
    // 细节在后续章节补充
  } else if (type === '[object Function]') {
    // 细节在后续章节补充
  } else {
    console.warn('invaild return value')
  }
}

在线demo

如有错误,欢迎指正!