Vue3中setup的运作方式

前言

在 Vue3 中,提供了一个新的选项setup,我们可以在 setup 中使用 composition api,那么,这个setup是如何工作的?

工作流程

首先,setup函数初始化是在组件 mount 的时候。

// runtime-core/src/renderer.ts
const mountComponent: MountComponentFn = (
  initialVNode,
  container,
  anchor,
  parentComponent,
  parentSuspense,
  isSVG,
  optimized
) => {
  const instance: ComponentInternalInstance = (initialVNode.component = createComponentInstance(
    initialVNode,
    parentComponent,
    parentSuspense
  ));

  // ...
  // 在这里开始安装组件
  setupComponent(instance);
  if (__DEV__) {
    endMeasure(instance, `init`);
  }
  // ...
};

它接收一个实例,执行组件的安装

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

  const { props, children, shapeFlag } = instance.vnode;
  // 判断是否是一个有状态的组件
  const isStateful = shapeFlag & ShapeFlags.STATEFUL_COMPONENT;
  // 初始化 props
  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;

  // ...
  // 0. create render proxy property access cache
  // 创建渲染代理的属性访问缓存
  instance.accessCache = {};
  // 1. create public instance / render proxy
  // also mark it raw so it's never observed
  // 创建渲染上下文代理
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers);
  if (__DEV__) {
    exposePropsOnRenderContext(instance);
  }
  // 2. call setup()
  // 调用setup
  const { setup } = Component;
  if (setup) {
    // 判断setup是否有参数,如果有,则创建一个setupContext
    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)) {
      if (isSSR) {
        // return the promise so server-renderer can wait on it
        return setupResult.then((resolvedResult: unknown) => {
          // 处理 setup 执行结果
          handleSetupResult(instance, resolvedResult, isSSR);
        });
      } else if (__FEATURE_SUSPENSE__) {
        // async setup returned Promise.
        // bail here and wait for re-entry.
        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 {
    // 完成组件实例设置
    finishComponentSetup(instance, isSSR);
  }
}

如上述代码所示,组件安装的过程很简单,首先初始化props、初始化slots,然后判断组件是否为有状态组件,如果是有状态组件,则安装有状态组件的实例,最后将setup的结果返回。

我们知道,在setup中我们可以定义很多变量,在return后可以在template中被访问到,vue 是如何做到这一点的?这里比较核心的就是安装有状态组件实例的过程了 setupStatefulComponent

它的工作流程主要分为 3 个步骤:

1、创建可访问代理缓存

2、创建渲染上下文代理

3、执行setup函数

export default {
  data() {
    return {
      name: "leevare"
    };
  },
  getName() {
    return this.name;
  }
};

通过instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)创建了渲染上下文代理,这有什么用?

在 vue2 中,我们给data定义数据name(如上述代码所示),其实在 vue 内部,实际数据是存储在this._data对象上,当你访问this.name的时候,其实真正访问的是this._data.name。在 vue3 中,也需要这个代理,这里使用了instance.proxy。那么此时对于数据的访问,都会被 proxy 所拦截,那么我们就看看PublicInstanceProxyHandlers是如何定义的。

export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
  get({ _: instance }: ComponentRenderContext, key: string) {
    const {
      ctx,
      setupState,
      data,
      props,
      accessCache,
      type,
      appContext
    } = instance;

    if (key === ReactiveFlags.SKIP) {
      return true;
    }

    let normalizedProps;
    // 从缓存中读取
    if (key[0] !== "$") {
      const n = accessCache![key];
      if (n !== undefined) {
        switch (n) {
          case AccessTypes.SETUP:
            return setupState[key];
          case AccessTypes.DATA:
            return data[key];
          case AccessTypes.CONTEXT:
            return ctx[key];
          case AccessTypes.PROPS:
            return props![key];
          // default: just fallthrough
        }
      } else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        accessCache![key] = AccessTypes.SETUP;
        // 从 setupState 中取数据
        return setupState[key];
      } else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache![key] = AccessTypes.DATA;
        // 从 data 中取数据
        return data[key];
      } else if (
        // 从 props 中取数据
        (normalizedProps = instance.propsOptions[0]) &&
        hasOwn(normalizedProps, key)
      ) {
        accessCache![key] = AccessTypes.PROPS;
        return props![key];
      } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache![key] = AccessTypes.CONTEXT;
        // 从 ctx 中取数据
        return ctx[key];
      } else if (!__FEATURE_OPTIONS_API__ || !isInBeforeCreate) {
        // 全部都取不到
        accessCache![key] = AccessTypes.OTHER;
      }
    }

    const publicGetter = publicPropertiesMap[key];
    let cssModule, globalProperties;
    // 公开的 $xxx 属性或方法
    if (publicGetter) {
      if (key === "$attrs") {
        track(instance, TrackOpTypes.GET, key);
        __DEV__ && markAttrsAccessed();
      }
      return publicGetter(instance);
    } else if (
      // css 模块,通过 vue-loader 编译的时候注入
      (cssModule = type.__cssModules) &&
      (cssModule = cssModule[key])
    ) {
      return cssModule;
    } else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
      // 用户自定义的属性,也用 `$` 开头
      accessCache![key] = AccessTypes.CONTEXT;
      return ctx[key];
    } else if (
      // 全局定义的属性
      ((globalProperties = appContext.config.globalProperties),
      hasOwn(globalProperties, key))
    ) {
      return globalProperties[key];
    } else if (
      __DEV__ &&
      currentRenderingInstance &&
      (!isString(key) ||
        // #1091 avoid internal isRef/isVNode checks on component instance leading
        // to infinite warning loop
        key.indexOf("__v") !== 0)
    ) {
      if (
        data !== EMPTY_OBJ &&
        (key[0] === "$" || key[0] === "_") &&
        hasOwn(data, key)
      ) {
        warn(
          `Property ${JSON.stringify(
            key
          )} must be accessed via $data because it starts with a reserved ` +
            `character ("$" or "_") and is not proxied on the render context.`
        );
      } else {
        warn(
          `Property ${JSON.stringify(key)} was accessed during render ` +
            `but is not defined on instance.`
        );
      }
    }
  },
  // ...

  set() {
    //...
  },
  has() {
    // ...
  }
};

这里定义了三个方法,分别为getsethas

当访问数据的时候,会被get所拦截,在get内部,控制了属性读取的先后顺序,依次为setupStatedatapropsctx、公开的内部$开头的属性、cssModule 属性、用户自定义属性。可以看到,setup中定义的属性优先级最高,就意味着,我们如果在data中和setup同时定义了同名的属性,优先访问的是setup中的属性。

// 输出:<div>setup msg</div>
<template>
  <div>{{ msg }}</div>
</template>

<script lang="ts">
export default {
  data() {
    return {
      msg: "data msg"
    };
  },
  setup() {
    return {
      msg: "setup msg"
    };
  }
};
</script>

这里使用了accessCache,这个值是从外部定义的,通过这个值可以优化属性访问的速度。怎么做到的?

从上面的代码可以看出,如果我访问this.name,那么 vue 会顺着setupStatedataprops等等的顺序依次寻找,通过hasOwn寻找是否存在某个键。如果多次这样在不同对象上去寻找内容,肯定没有在一个对象中直接访问来的快,所以,这里的accessCache就缓存了某个值具体存在于哪个属性中,下次访问的时候,直接到对应的属性中直接取值即可。

然后是sethas方法,和get类似,这里就不在赘述,有兴趣可以直接看源码

安装有状态组件的第一步完成了,然后是执行setup

const setupResult = callWithErrorHandling(
  setup,
  instance,
  ErrorCodes.SETUP_FUNCTION,
  [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
);

// ...
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执行setup,可以看到,其实在args就是传给setup的参数,这里传给了setup两个参数,分别为propssetupContext,这就和官网上描述的一致(https://v3.vuejs.org/api/composition-api.html#setup)。

然后处理setup执行的结果。

export function handleSetupResult(
  instance: ComponentInternalInstance,
  setupResult: unknown,
  isSSR: boolean
) {
  if (isFunction(setupResult)) {
    // setup returned an inline render function
    instance.render = setupResult as InternalRenderFunction;
  } else if (isObject(setupResult)) {
    // setup returned bindings.
    // assuming a render function compiled from template is present.
    if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
      instance.devtoolsRawSetupState = setupResult;
    }
    // 把 setup 返回结果变成响应式
    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);
}

如果结果是一个函数,直接将结果赋值给render,就意味着,我们可以在setup的返回函数中使用render函数或者 JSX。如果是一个对象,就将它转换为响应式数据。

最后,完成组件的安装 finishComponentSetup

function finishComponentSetup(
  instance: ComponentInternalInstance,
  isSSR: boolean
) {
  const Component = instance.type as ComponentOptions;

  // template / render function normalization
  // 对模板或者渲染函数的标准化
  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) {
      // 运行时编译
      Component.render = compile(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement,
        delimiters: Component.delimiters
      });
    }

    // 组件对象的 render 函数赋值给 instance
    instance.render = (Component.render || NOOP) as InternalRenderFunction;

    if (instance.render._rc) {
      // 对于使用 with 块的运行时编译的渲染函数,使用新的渲染上下文的代理
      instance.withProxy = new Proxy(
        instance.ctx,
        RuntimeCompiledPublicInstanceProxyHandlers
      );
    }
  }

  // support for 2.x options
  if (__FEATURE_OPTIONS_API__) {
    currentInstance = instance;
    applyOptions(instance, Component);
    currentInstance = null;
  }
}

在最后一步,主要处理了 SSR、运行时编译和兼容 Vue2.x Options api 的一些操作。

结语

setup给我们带来编码的灵活性是基于它的内部代码的复杂的处理逻辑,学习了解它的工作方式,对于我们后续的开发会有更大的帮助。

如果您觉得本文对您有用,欢迎捐赠或留言~
微信支付
支付宝

发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注