1. 首页

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

本文使用的vue版本为3.0.2

  • createApp() & mount()
  • setup()
  • render() h()
  • Virtual Dom
  • 生命周期hooks
  • Proxy代理
  • reactive() ref()
  • computed()
  • watch()
  • provide() inject()
  • directives()
  • components()

setup()是什么?

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

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

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

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

用法:

  • 调用时机:组件被创建之前,所以没有this,无法访问data,methods,computed
  • 参数(props,context)creataApp的参数props和 2.x 版本中实例this上的 3 个属性attrs,slots,emit
  • 返回值是一个对象:该对象可以被组件的其余部分(computed、methods、声明周期钩子、组件模板等)使用。
  • 返回值是一个render函数:只能使用在同一作用域中声明的响应式状态(无法使用例如 data、computed 中的响应式状态)
 typescript
<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方法:

 typescript
// 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方法很长,对其进行简化:

 typescript
// 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 的方式去执行的。

一张图简要概括:

Javascript中文网是以前端进阶资源教程分享为主的专业网站

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

总结

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

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

实现

基于上一篇的已有成果

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

 typescript
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()的返回值

 typescript
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

如有错误,欢迎指正!

.markdown-body pre,.markdown-body pre>code.hljs{background:#1e1e1e;color:#dcdcdc}.hljs-keyword,.hljs-link,.hljs-literal,.hljs-name,.hljs-symbol{color:#569cd6}.hljs-link{text-decoration:underline}.hljs-built_in,.hljs-type{color:#4ec9b0}.hljs-class,.hljs-number{color:#b8d7a3}.hljs-meta-string,.hljs-string{color:#d69d85}.hljs-regexp,.hljs-template-tag{color:#9a5334}.hljs-formula,.hljs-function,.hljs-params,.hljs-subst,.hljs-title{color:#dcdcdc}.hljs-comment,.hljs-quote{color:#57a64a;font-style:italic}.hljs-doctag{color:#608b4e}.hljs-meta,.hljs-meta-keyword,.hljs-tag{color:#9b9b9b}.hljs-template-variable,.hljs-variable{color:#bd63c5}.hljs-attr,.hljs-attribute,.hljs-builtin-name{color:#9cdcfe}.hljs-section{color:gold}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}.hljs-bullet,.hljs-selector-attr,.hljs-selector-class,.hljs-selector-id,.hljs-selector-pseudo,.hljs-selector-tag{color:#d7ba7d}.hljs-addition{background-color:#144212}.hljs-addition,.hljs-deletion{display:inline-block;width:100%}.hljs-deletion{background-color:#600}

作者:Elecat
链接:https://juejin.im/post/6894182826913202189

看完两件小事

如果你觉得这篇文章对你挺有启发,我想请你帮我两个小忙:

  1. 关注我们的 GitHub 博客,让我们成为长期关系
  2. 把这篇文章分享给你的朋友 / 交流群,让更多的人看到,一起进步,一起成长!
  3. 关注公众号 「画漫画的程序员」,公众号后台回复「资源」 免费领取我精心整理的前端进阶资源教程

JS中文网是中国领先的新一代开发者社区和专业的技术媒体,一个帮助开发者成长的社区,目前已经覆盖和服务了超过 300 万开发者,你每天都可以在这里找到技术世界的头条内容。欢迎热爱技术的你一起加入交流与学习,JS中文网的使命是帮助开发者用代码改变世界

本文著作权归作者所有,如若转载,请注明出处

转载请注明:文章转载自「 Js中文网 · 前端进阶资源教程 」https://www.javascriptc.com

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

链接:https://www.javascriptc.com/4634.html

« 仿van-popup实现一个从底部弹出的Popup
五分钟弄懂移动端适配之 —— rem,vw方案»
Flutter 中文教程资源

相关推荐

QR code