你不知道的setup

发表于 2年以前  | 总阅读数:404 次

我们在 Vue2编写组件的时候,会在 propsdatamethodscomputed 等 options 中定义一些变量。在组件初始化阶段,Vue内部会处理这些 options,即把定义的变量添加到了组件实例上。等模板编译成 render 函数的时候,内部通过 with(this){} 的语法去访问在组件实例中的变量。

那么到了 Vue3,新出现的setup启动函数,是整个组件逻辑组织的入口,我们可以在它内部写composition-api,以更加直观的形式声明变量,有利于代码的逻辑组织和复用,但是我们要明确一点,composition-api 属于 api 的增强,它并不是 Vue3 组件开发的范式,如果组件足够简单,还是可以使用 options-api,在了解了 composition-api的应用场景和使用方式后,我们需要进一步思考,setup启动函数是如何执行的,它所返回的数据又是如何与模板建立联系的?

创建并设置组件实例

首先,我们来回顾一下组件的渲染流程:创建 vnode渲染 vnode生成 DOM

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

const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
  // 创建组件实例
  const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense))
  // 设置组件实例
  setupComponent(instance)
  // 设置并运行带副作用的渲染函数
  setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized)
}

可以看到,这段挂载组件的代码主要做了三件事情:创建组件实例设置组件实例设置并运行带副作用的渲染函数。前两个流程就跟我们要探讨的问题息息相关,先看创建组件实例的流程,我们要关注 createComponentInstance 方法的实现:

function createComponentInstance (vnode, parent, suspense) {
  // 继承父组件实例上的 appContext,如果是根组件,则直接从根 vnode 中取。
  const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
  const instance = {
    // 组件唯一 id
    uid: uid++,
    // 组件 vnode
    vnode,
    // 父组件实例
    parent,
    // app 上下文
    appContext,
    // vnode 节点类型
    type: vnode.type,
    // 根组件实例
    root: null,
    // 新的组件 vnode
    next: null,
    // 子节点 vnode
    subTree: null,
    // 带副作用更新函数
    update: null,
    // 渲染函数
    render: null,
    // 渲染上下文代理
    proxy: null,
    // 带有 with 区块的渲染上下文代理
    withProxy: null,
    // 响应式相关对象
    effects: null,
    // 依赖注入相关
    provides: parent ? parent.provides : Object.create(appContext.provides),
    // 渲染代理的属性访问缓存
    accessCache: null,
    // 渲染缓存
    renderCache: [],
    // 渲染上下文
    ctx: EMPTY_OBJ,
    // data 数据
    data: EMPTY_OBJ,
    // props 数据
    props: EMPTY_OBJ,
    // 普通属性
    attrs: EMPTY_OBJ,
    // 插槽相关
    slots: EMPTY_OBJ,
    // 组件或者 DOM 的 ref 引用
    refs: EMPTY_OBJ,
    // setup 函数返回的响应式结果
    setupState: EMPTY_OBJ,
    // setup 函数上下文数据
    setupContext: null,
    // 注册的组件
    components: Object.create(appContext.components),
    // 注册的指令
    directives: Object.create(appContext.directives),
    // suspense 相关
    suspense,
    // suspense 异步依赖
    asyncDep: null,
    // suspense 异步依赖是否都已处理
    asyncResolved: false,
    // 是否挂载
    isMounted: false,
    // 是否卸载
    isUnmounted: false,
    // 是否激活
    isDeactivated: false,
    // 生命周期,before create
    bc: null,
    // 生命周期,created
    c: null,
    // 生命周期,before mount
    bm: null,
    // 生命周期,mounted
    m: null,
    // 生命周期,before update
    bu: null,
    // 生命周期,updated
    u: null,
    // 生命周期,unmounted
    um: null,
    // 生命周期,before unmount
    bum: null,
    // 生命周期, deactivated
    da: null,
    // 生命周期 activated
    a: null,
    // 生命周期 render triggered
    rtg: null,
    // 生命周期 render tracked
    rtc: null,
    // 生命周期 error captured
    ec: null,
    // 派发事件方法
    emit: null
  }
  // 初始化渲染上下文
  instance.ctx = { _: instance }
  // 初始化根组件指针
  instance.root = parent ? parent.root : instance
  // 初始化派发事件方法
  instance.emit = emit.bind(null, instance)
  return instance
}

从上述代码中可以看到,组件实例 instance 上定义了很多属性,其中很多一些属性是为了实现某个场景或者某个功能所定义的,只需要通过代码中的注释大概知道它们是做什么的即可。

Vue2使用 new Vue()来初始化一个组件的实例,而到了Vue3,移除了Vue构造函数,我们直接通过创建对象字面量的方式去创建组件的实例。这两种方式并无本质的区别,都是引用一个对象,然后在整个组件的生命周期中去维护组件的状态数据和上下文环境

创建好 instance 实例后,接下来就是设置它的一些属性。目前已完成了组件的上下文、根组件指针以及派发事件方法的设置,之后会有更多 instance 实例属性的设置逻辑。

接着是组件实例的设置流程,对 setup 函数的处理就在这里完成,我们来看一下 setupComponent 方法的实现:

function setupComponent (instance, isSSR = false) {
  const { props, children, shapeFlag } = instance.vnode
  // 判断是否是一个有状态的组件
  const isStateful = shapeFlag & 4
  // 初始化 props
  initProps(instance, props, isStateful, isSSR)
  // 初始化 插槽
  initSlots(instance, children)
  // 设置有状态的组件实例
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  return setupResult
}
const shapeFlag = isString(type)
  ? 1 /* ELEMENT */
  : isSuspense(type)
    ? 128 /* SUSPENSE */
    : isTeleport(type)
      ? 64 /* TELEPORT */
      : isObject(type)
        ? 4 /* STATEFUL_COMPONENT */
        : isFunction(type)
          ? 2 /* FUNCTIONAL_COMPONENT */
          : 0

可以看到,我们从组件 vnode 中获取了 propschildrenshapeFlag 等属性,然后分别对 props 和插槽进行初始化。而shapeFlag则是把 vnode 的类型信息做了编码,以便在后面的 patch 阶段,可以根据不同的类型执行相应的处理逻辑,在这里根据 shapeFlag 的值,我们可以判断这是不是一个有状态组件,如果是则要进一步去设置有状态组件的实例。

接下来我们要关注到 setupStatefulComponent 函数,它主要做了三件事:创建渲染上下文代理判断处理 setup 函数完成组件实例设置。它代码如下所示:

function setupStatefulComponent (instance, isSSR) {
  const Component = instance.type
  // 创建渲染代理的属性访问缓存
  instance.accessCache = {}
  // 创建渲染上下文代理
  instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
  // 判断处理 setup 函数
  const { setup } = Component
  if (setup) {
    // 如果 setup 函数带参数,则创建一个 setupContext
    const setupContext = (instance.setupContext =
      setup.length > 1 ? createSetupContext(instance) : null)
    // 执行 setup 函数,获取结果
    const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
    // 处理 setup 执行结果
    handleSetupResult(instance, setupResult)
  }
  else {
    // 完成组件实例设置
    finishComponentSetup(instance)
  }
}

首先是创建渲染上下文代理的流程,它主要对 instance.ctx 做了代理。在分析实现前,我们需要思考一个问题,这里为什么需要代理呢?

创建渲染上下文代理

其实在 Vue2 中,也有类似的数据代理逻辑,比如 props 求值后的数据,实际上存储在 this._props 上,而 data 中定义的数据存储在 this._data 上。举个例子:

<template>
  <p>{{ zz }}</p>
</template>
<script>
export default {
  data() {
    inZZ: true
  }
}
</script>

在初始化组件的时候,data 中定义的 inZZ 在组件内部是存储在 this._data 上的,而模板渲染的时候访问 this.inZZ,实际上访问的是 this._data.inZZ,这是因为 Vue2 在初始化 data 的时候,做了一层 proxy 代理。

到了 Vue3,为了方便维护,我们把组件中不同状态的数据存储到不同的属性中,比如存储到 setupStatectxdataprops 中。我们在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性,所以我们也要做一层 proxy,对渲染上下文 instance.ctx 属性的访问和修改,代理到对 setupStatectxdataprops 中的数据的访问和修改。

明确了代理的需求后,我们接下来就要分析 proxy 的几个方法: getsethas,当我们访问 instance.ctx 渲染上下文中的属性时,就会进入 get 函数,我们来看一下它的实现:

const PublicInstanceProxyHandlers = {
  get ({ _: instance }, key) {
    const { ctx, setupState, data, props, accessCache, type, appContext } = instance
    if (key[0] !== '$') {
      // setupState / data / props / ctx
      // 渲染代理的属性访问缓存中
      const n = accessCache[key]
      if (n !== undefined) {
        // 从缓存中取
        switch (n) {
          case 0: /* SETUP */
            return setupState[key]
          case 1 :/* DATA */
            return data[key]
          case 3 :/* CONTEXT */
            return ctx[key]
          case 2: /* PROPS */
            return props[key]
        }
      }
      else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
        accessCache[key] = 0
        // 从 setupState 中取数据
        return setupState[key]
      }
      else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
        accessCache[key] = 1
        // 从 data 中取数据
        return data[key]
      }
      else if (
        type.props &&
        hasOwn(normalizePropsOptions(type.props)[0], key)) {
        accessCache[key] = 2
        // 从 props 中取数据
        return props[key]
      }
      else if (ctx !== EMPTY_OBJ && hasOwn(ctx, key)) {
        accessCache[key] = 3
        // 从 ctx 中取数据
        return ctx[key]
      }
      else {
        // 都取不到
        accessCache[key] = 4
      }
    }
    const publicGetter = publicPropertiesMap[key]
    let cssModule, globalProperties
    // 公开的 $xxx 属性或方法
    if (publicGetter) {
      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] = 3
      return ctx[key]
    }
    else if (
      // 全局定义的属性
      ((globalProperties = appContext.config.globalProperties),
        hasOwn(globalProperties, key))) {
      return globalProperties[key]
    }
    else if ((process.env.NODE_ENV !== 'production') &&
      currentRenderingInstance && key.indexOf('__v') !== 0) {
      if (data !== EMPTY_OBJ && key[0] === '$' && hasOwn(data, key)) {
        // 如果在 data 中定义的数据以 $ 开头,会报警告,因为 $ 是保留字符,不会做代理
        warn(`Property ${JSON.stringify(key)} must be accessed via $data because it starts with a reserved ` +
          `character and is not proxied on the render context.`)
      }
      else {
        // 在模板中使用的变量如果没有定义,报警告
        warn(`Property ${JSON.stringify(key)} was accessed during render ` +
          `but is not defined on instance.`)
      }
    }
  }
}

可以看到,函数首先判断 key 不以 $ 开头的情况,这部分数据可能是 setupStatedatapropsctx 中的一种,其中 data、props 我们已经很熟悉了,setupState 就是 setup 函数返回的数据,ctx 包括了计算属性、组件方法和用户自定义的一些数据。

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

<template>
  <p>{{msg}}</p>
</template>
<script>
  import { ref } from 'vue'
  export default {
    data() {
      return {
        msg: 'msg from data'
      }
    },
    setup() {
      const msg = ref('msg from setup')
      return {
        msg
      }
    }
  }
</script>

我们在 data 和 setup 中都定义了 msg 变量,但最终输出到界面上的是"msg from setup",这是因为 setupState 的判断优先级要高于 data

再回到 get 函数中,我们可以看到这里定义了 accessCache 作为渲染代理的属性访问缓存,它具体是干什么的呢?组件在渲染时会经常访问数据进而触发 get 函数,这其中最昂贵的部分就是多次调用 hasOwn 去判断 key 在不在某个类型的数据中,但是在普通对象上执行简单的属性访问相对要快得多。所以在第一次获取 key 对应的数据后,我们利用 accessCache[key] 去缓存数据,下一次再次根据 key 查找数据,就可以直接通过 accessCache[key] 获取对应的值,就不需要依次调用 hasOwn 去判断了,这也是一个性能优化的小技巧。

如果key$开头,那么接下来又会有一系列的判断,首先判断是不是Vue内部公开的 $xxx属性或方法(比如$parent),然后判断是不是vue-loader编译注入的 css 模块内部的 key,接着判断是不是用户自定义以$开头的 key,最后判断是不是全局属性。如果都不满足,就剩两种情况了,即在非生产环境下就会报两种类型的警告,第一种是在 data 中定义的数据以$开头的警告,因为$是保留字符,不会做代理,第二种是在模板中使用的变量没有定义的警告。

接下来是 set 代理过程,当我们修改 instance.ctx 渲染上下文中的属性的时候,就会进入 set 函数,我们来看一下 set 函数的实现:

const PublicInstanceProxyHandlers = {
  set ({ _: instance }, key, value) {
    const { data, setupState, ctx } = instance
    if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
      // 给 setupState 赋值
      setupState[key] = value
    }
    else if (data !== EMPTY_OBJ && hasOwn(data, key)) {
      // 给 data 赋值
      data[key] = value
    }
    else if (key in instance.props) {
      // 不能直接给 props 赋值
      (process.env.NODE_ENV !== 'production') &&
      warn(`Attempting to mutate prop "${key}". Props are readonly.`, instance)
      return false
    }
    if (key[0] === '$' && key.slice(1) in instance) {
      // 不能给 Vue 内部以 $ 开头的保留属性赋值
      (process.env.NODE_ENV !== 'production') &&
      warn(`Attempting to mutate public property "${key}". ` +
        `Properties starting with $ are reserved and readonly.`, instance)
      return false
    }
    else {
      // 用户自定义数据赋值
      ctx[key] = value
    }
    return true
  }
}

结合代码来看,函数主要做的事情就是对渲染上下文 instance.ctx 中的属性赋值,它实际上是代理到对应的数据类型中去完成赋值操作的,这里仍然要注意顺序问题,和 get 一样,优先判断 setupState,然后是 data,接着是 props

我们对之前的例子做点修改,添加一个方法:

<template>
  <p>{{ msg }}</p>
  <button @click="random">Random msg</button>
</template>
<script>
  import { ref } from 'vue'
  export default {
    data() {
      return {
        msg: 'msg from data'
      }
    },
    setup() {
      const msg = ref('msg from setup')
      return {
        msg
      }
    },
    methods: {
      random() {
        this.msg = Math.random()
      }
    }
  }
</script>

我们点击按钮会执行 random 函数,这里的 this 指向的就是 instance.ctx,我们修改 this.msg 会触发 set 函数,所以最终修改的是 setupState 中的 msg 对应的值。

注意,如果我们直接对 props 中的数据赋值,在非生产环境中会收到一条警告,这是因为直接修改 props 不符合数据单向流动的设计思想,如果对 Vue 内部以 $ 开头的保留属性赋值,同样也会收到一条警告。

如果是用户自定义的数据,比如在 created 生命周期内定义的数据,它仅用于组件上下文的共享,如下所示:

export default {
  created() {
    this.userMsg = 'msg from user'
  }
}

当执行 this.userMsg 赋值的时候,会触发 set 函数,最终 userMsg 会被保留到 ctx 中。

最后是 has 代理过程,当我们判断属性是否存在于 instance.ctx 渲染上下文中时,就会进入 has 函数,这个在平时项目中用的比较少,同样来举个例子,当执行 created 钩子函数中的 'msg' in this 时,就会触发 has 函数。

export default {
  created () {
    console.log('msg' in this)
  }
}

下面我们来看一下 has 函数的实现:

const PublicInstanceProxyHandlers = {
  has
    ({ _: { data, setupState, accessCache, ctx, type, appContext } }, key) {
    // 依次判断
    return (accessCache[key] !== undefined ||
      (data !== EMPTY_OBJ && hasOwn(data, key)) ||
      (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) ||
      (type.props && hasOwn(normalizePropsOptions(type.props)[0], key)) ||
      hasOwn(ctx, key) ||
      hasOwn(publicPropertiesMap, key) ||
      hasOwn(appContext.config.globalProperties, key))
  }
}

这个函数的实现很简单,依次判断 key 是否存在于 accessCachedatasetupStateprops用户数据公开属性以及全局属性中,然后返回结果。

至此,我们就搞清楚了创建上下文代理的过程,让我们回到 setupStatefulComponent 函数中,接下来分析第二个流程——判断处理 setup 函数。

判断处理 setup 函数

我们看一下整个逻辑涉及的代码:

// 判断处理 setup 函数
const { setup } = Component
if (setup) {
  // 如果 setup 函数带参数,则创建一个 setupContext
  const setupContext = (instance.setupContext =
    setup.length > 1 ? createSetupContext(instance) : null)
  // 执行 setup 函数获取结果
  const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])
  // 处理 setup 执行结果
  handleSetupResult(instance, setupResult)
}

如果我们在组件中定义了 setup 函数,接下来就是处理 setup 函数的流程,主要是三个步骤:创建 setup 函数上下文执行 setup 函数并获取结果处理 setup 函数的执行结果,接下来我们就逐个来分析。

首先判断 setup 函数的参数长度,如果大于 1,则创建 setupContext 上下文。

const setupContext = (instance.setupContext =
    setup.length > 1 ? createSetupContext(instance) : null)

我们知道第一个参数 props 对应父组件传入的 props 数据,第二个参数 ctx 是一个对象,实际上就是setupContext

下面我们来看一下用 createSetupContext 函数来创建 setupContext

function createSetupContext (instance) {
  return {
    attrs: instance.attrs,
    slots: instance.slots,
    emit: instance.emit
  }
}

这里返回了一个对象,包括 attrsslotsemit 三个属性。setupContext 让我们在 setup 函数内部可以获取到组件的属性、插槽以及派发事件的方法 emit

通过下面这行代码来执行 setup 函数并获取结果:

const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext])

我们具体来看一下 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 函数来处理。

执行 setup 函数并拿到了返回的结果,那么接下来就要用 handleSetupResult 函数来处理结果。

handleSetupResult(instance, setupResult)

我们详细看一下 handleSetupResult 函数的实现:

function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    // setup 返回渲染函数
    instance.render = setupResult
  }
  else if (isObject(setupResult)) {
    // 把 setup 返回结果变成响应式
    instance.setupState = reactive(setupResult)
  }
  finishComponentSetup(instance)
}

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

handleSetupResult 的最后,会执行 finishComponentSetup 函数完成组件实例的设置,其实这个函数和 setup 函数的执行结果已经没什么关系了,个人认为提取到外面放在 handleSetupResult 函数后面执行更合理一些。另外当组件没有定义的 setup 的时候,也会执行 finishComponentSetup 函数去完成组件实例的设置。

完成组件实例的设置

接下来我们来看一下 finishComponentSetup 函数的实现:

function finishComponentSetup (instance) {
  const Component = instance.type
  // 对模板或者渲染函数的标准化
  if (!instance.render) {
    if (compile && Component.template && !Component.render) {
      // 运行时编译
      Component.render = compile(Component.template, {
        isCustomElement: instance.appContext.config.isCustomElement || NO
      })
      Component.render._rc = true
    }
    if ((process.env.NODE_ENV !== 'production') && !Component.render) {
      if (!compile && Component.template) {
        // 只编写了 template 但使用了 runtime-only 的版本
        warn(`Component provided template option but ` +
          `runtime compilation is not supported in this build of Vue.` +
          (` Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js".`
          ) /* should not happen */)
      }
      else {
        // 既没有写 render 函数,也没有写 template 模板
        warn(`Component is missing template or render function.`)
      }
    }
    // 组件对象的 render 函数赋值给 instance
    instance.render = (Component.render || NOOP)
    if (instance.render._rc) {
      // 对于使用 with 块的运行时编译的渲染函数,使用新的渲染上下文的代理
      instance.withProxy = new Proxy(instance.ctx, RuntimeCompiledPublicInstanceProxyHandlers)
    }
  }
  // 兼容 Vue2 options-api
  {
    currentInstance = instance
    applyOptions(instance, Component)
    currentInstance = null
  }
}

函数主要做了两件事情:标准化模板或者渲染函数兼容 options-api,接下来我们详细分析这两个流程。

标准化模板或者渲染函数

在分析这个过程之前,我们需要了解一些背景知识。组件最终通过运行 render 函数生成子树 vnode,但是我们很少直接去编写 render 函数,通常会使用两种方式开发组件。

  1. 第一种是使用 SFC(Single File Components)单文件的开发方式来开发组件,即通过编写组件的 template 模板去描述一个组件的 DOM 结构。我们知道 .vue 类型的文件无法在 Web 端直接加载,因此在 webpack 的编译阶段,它会通过 vue-loader 编译生成组件相关的 JavaScript 和 CSS,并把 template 部分转换成 render 函数添加到组件对象的属性中。
  2. 另外一种开发方式是不借助 webpack 编译,直接引入 Vue,开箱即用,我们直接在组件对象 template 属性中编写组件的模板,然后在运行阶段编译生成 render 函数,这种方式通常用于有一定历史包袱的古老项目。

因此 Vue 在 Web 端有两个版本:runtime-onlyruntime-compiled。我们更推荐用 runtime-only 版本的 Vue,因为相对而言它体积更小,而且在运行时不用编译,不仅耗时更少而且性能更优秀。遇到一些不得已的情况比如上述提到的古老项目,我们也可以选择 runtime-compiled 版本。

runtime-onlyruntime-compiled 的主要区别在于是否注册了这个 compile 方法。

在 Vue3 中,compile 方法是通过外部注册的:

let compile;
function registerRuntimeCompiler(_compile) {
    compile = _compile;
}

回到标准化模板或者渲染函数逻辑,我们先看 instance.render 是否存在,如果不存在则开始标准化流程,这里主要需要处理以下三种情况。

  1. compile 和组件 template 属性存在,render 方法不存在的情况。此时, runtime-compiled 版本会在 JavaScript 运行时进行模板编译,生成 render 函数。
  2. compile 和 render 方法不存在,组件 template 属性存在的情况。此时由于没有 compile,这里用的是 runtime-only 的版本,因此要报一个警告来告诉用户,想要运行时编译得使用 runtime-compiled 版本的 Vue。
  3. 组件既没有写 render 函数,也没有写 template 模板,此时要报一个警告,告诉用户组件缺少了 render 函数或者 template 模板。

处理完以上情况后,就要把组件的 render 函数赋值给 instance.render,到了组件渲染的时候,就可以运行 instance.render 函数生成组件的子树 vnode 了。

另外对于使用 with 块运行时编译的渲染函数,渲染上下文的代理是 RuntimeCompiledPublicInstanceProxyHandlers,它是在之前渲染上下文代理 PublicInstanceProxyHandlers 的基础上进行的扩展,主要对 has 函数的实现做了优化:

const RuntimeCompiledPublicInstanceProxyHandlers = {
  ...PublicInstanceProxyHandlers,
  get(target, key) {
    if (key === Symbol.unscopables) {
      return
    }
    return PublicInstanceProxyHandlers.get(target, key, target)
  },
  has(_, key) {
    // 如果 key 以 _ 开头或者 key 在全局变量白名单内,则 has 为 false
    const has = key[0] !== '_' && !isGloballyWhitelisted(key)
    if ((process.env.NODE_ENV !== 'production') && !has && PublicInstanceProxyHandlers.has(_, key)) {
      warn(`Property ${JSON.stringify(key)} should not start with _ which is a reserved prefix for Vue internals.`)
    }
    return has
  }
}

这里如果 key 以 _ 开头,或者 key 在全局变量的白名单内,则 has 为 false,此时则直接命中警告,不用再进行之前那一系列的判断了。

了解完标准化模板或者渲染函数流程,我们来看完成组件实例设置的最后一个流程——兼容 Vue2 的 options-api

兼容 Vue2 的 options-api

我们知道 Vue2 是通过组件对象的方式去描述一个组件,Vue3 也仍然支持 Vue2 options-api 的写法,这主要就是通过 applyOptions方法实现的。

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
}

applyOptions 的代码特别长,所以只用注释列出了它主要做的事情,如果你对 Vue2 的原理如数家珍,理解这段代码会很轻松,感兴趣的小伙伴可以去翻阅它的源码。

总结

我们主要分析了组件的初始化流程,主要包括创建组件实例和设置组件实例。通过进一步细节的深入,我们也了解了渲染上下文的代理过程,了解了 composition-api 中的 setup 启动函数执行的时机,以及如何建立 setup 返回结果和模板渲染之间的联系,了解了组件定义的模板或者渲染函数的标准化过程,了解了如何兼容 Vue2 的 options-api。 最后,我们通过一张图再直观感受一下 Vue3 组件的初始化流程:

由于篇幅有限,文中仅贴出精简过后的代码,本文所涉及源代码的仓库地址如下: packages/runtime-core/src/renderer.ts packages/runtime-core/src/component.ts packages/runtime-core/src/errorHandling.ts

有关setup函数的分享就到这里了,如果阅读完本篇文章后,你对setup函数有了更深一步地理解和认识的话,欢迎给笔者点赞鼓励!若有不对的地方也希望大家多多指正,一起进步~

本文由哈喽比特于2年以前收录,如有侵权请联系我们。
文章来源:https://mp.weixin.qq.com/s/hYYNbMUkheHVgzi26rnWPQ

 相关推荐

刘强东夫妇:“移民美国”传言被驳斥

京东创始人刘强东和其妻子章泽天最近成为了互联网舆论关注的焦点。有关他们“移民美国”和在美国购买豪宅的传言在互联网上广泛传播。然而,京东官方通过微博发言人发布的消息澄清了这些传言,称这些言论纯属虚假信息和蓄意捏造。

发布于:1年以前  |  808次阅读  |  详细内容 »

博主曝三大运营商,将集体采购百万台华为Mate60系列

日前,据博主“@超能数码君老周”爆料,国内三大运营商中国移动、中国电信和中国联通预计将集体采购百万台规模的华为Mate60系列手机。

发布于:1年以前  |  770次阅读  |  详细内容 »

ASML CEO警告:出口管制不是可行做法,不要“逼迫中国大陆创新”

据报道,荷兰半导体设备公司ASML正看到美国对华遏制政策的负面影响。阿斯麦(ASML)CEO彼得·温宁克在一档电视节目中分享了他对中国大陆问题以及该公司面临的出口管制和保护主义的看法。彼得曾在多个场合表达了他对出口管制以及中荷经济关系的担忧。

发布于:1年以前  |  756次阅读  |  详细内容 »

抖音中长视频App青桃更名抖音精选,字节再发力对抗B站

今年早些时候,抖音悄然上线了一款名为“青桃”的 App,Slogan 为“看见你的热爱”,根据应用介绍可知,“青桃”是一个属于年轻人的兴趣知识视频平台,由抖音官方出品的中长视频关联版本,整体风格有些类似B站。

发布于:1年以前  |  648次阅读  |  详细内容 »

威马CDO:中国每百户家庭仅17户有车

日前,威马汽车首席数据官梅松林转发了一份“世界各国地区拥车率排行榜”,同时,他发文表示:中国汽车普及率低于非洲国家尼日利亚,每百户家庭仅17户有车。意大利世界排名第一,每十户中九户有车。

发布于:1年以前  |  589次阅读  |  详细内容 »

研究发现维生素 C 等抗氧化剂会刺激癌症生长和转移

近日,一项新的研究发现,维生素 C 和 E 等抗氧化剂会激活一种机制,刺激癌症肿瘤中新血管的生长,帮助它们生长和扩散。

发布于:1年以前  |  449次阅读  |  详细内容 »

苹果据称正引入3D打印技术,用以生产智能手表的钢质底盘

据媒体援引消息人士报道,苹果公司正在测试使用3D打印技术来生产其智能手表的钢质底盘。消息传出后,3D系统一度大涨超10%,不过截至周三收盘,该股涨幅回落至2%以内。

发布于:1年以前  |  446次阅读  |  详细内容 »

千万级抖音网红秀才账号被封禁

9月2日,坐拥千万粉丝的网红主播“秀才”账号被封禁,在社交媒体平台上引发热议。平台相关负责人表示,“秀才”账号违反平台相关规定,已封禁。据知情人士透露,秀才近期被举报存在违法行为,这可能是他被封禁的部分原因。据悉,“秀才”年龄39岁,是安徽省亳州市蒙城县人,抖音网红,粉丝数量超1200万。他曾被称为“中老年...

发布于:1年以前  |  445次阅读  |  详细内容 »

亚马逊股东起诉公司和贝索斯,称其在购买卫星发射服务时忽视了 SpaceX

9月3日消息,亚马逊的一些股东,包括持有该公司股票的一家养老基金,日前对亚马逊、其创始人贝索斯和其董事会提起诉讼,指控他们在为 Project Kuiper 卫星星座项目购买发射服务时“违反了信义义务”。

发布于:1年以前  |  444次阅读  |  详细内容 »

苹果上线AppsbyApple网站,以推广自家应用程序

据消息,为推广自家应用,苹果现推出了一个名为“Apps by Apple”的网站,展示了苹果为旗下产品(如 iPhone、iPad、Apple Watch、Mac 和 Apple TV)开发的各种应用程序。

发布于:1年以前  |  442次阅读  |  详细内容 »

特斯拉美国降价引发投资者不满:“这是短期麻醉剂”

特斯拉本周在美国大幅下调Model S和X售价,引发了该公司一些最坚定支持者的不满。知名特斯拉多头、未来基金(Future Fund)管理合伙人加里·布莱克发帖称,降价是一种“短期麻醉剂”,会让潜在客户等待进一步降价。

发布于:1年以前  |  441次阅读  |  详细内容 »

光刻机巨头阿斯麦:拿到许可,继续对华出口

据外媒9月2日报道,荷兰半导体设备制造商阿斯麦称,尽管荷兰政府颁布的半导体设备出口管制新规9月正式生效,但该公司已获得在2023年底以前向中国运送受限制芯片制造机器的许可。

发布于:1年以前  |  437次阅读  |  详细内容 »

马斯克与库克首次隔空合作:为苹果提供卫星服务

近日,根据美国证券交易委员会的文件显示,苹果卫星服务提供商 Globalstar 近期向马斯克旗下的 SpaceX 支付 6400 万美元(约 4.65 亿元人民币)。用于在 2023-2025 年期间,发射卫星,进一步扩展苹果 iPhone 系列的 SOS 卫星服务。

发布于:1年以前  |  430次阅读  |  详细内容 »

𝕏(推特)调整隐私政策,可拿用户发布的信息训练 AI 模型

据报道,马斯克旗下社交平台𝕏(推特)日前调整了隐私政策,允许 𝕏 使用用户发布的信息来训练其人工智能(AI)模型。新的隐私政策将于 9 月 29 日生效。新政策规定,𝕏可能会使用所收集到的平台信息和公开可用的信息,来帮助训练 𝕏 的机器学习或人工智能模型。

发布于:1年以前  |  428次阅读  |  详细内容 »

荣耀CEO谈华为手机回归:替老同事们高兴,对行业也是好事

9月2日,荣耀CEO赵明在采访中谈及华为手机回归时表示,替老同事们高兴,觉得手机行业,由于华为的回归,让竞争充满了更多的可能性和更多的魅力,对行业来说也是件好事。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI操控无人机能力超越人类冠军

《自然》30日发表的一篇论文报道了一个名为Swift的人工智能(AI)系统,该系统驾驶无人机的能力可在真实世界中一对一冠军赛里战胜人类对手。

发布于:1年以前  |  423次阅读  |  详细内容 »

AI生成的蘑菇科普书存在可致命错误

近日,非营利组织纽约真菌学会(NYMS)发出警告,表示亚马逊为代表的电商平台上,充斥着各种AI生成的蘑菇觅食科普书籍,其中存在诸多错误。

发布于:1年以前  |  420次阅读  |  详细内容 »

社交媒体平台𝕏计划收集用户生物识别数据与工作教育经历

社交媒体平台𝕏(原推特)新隐私政策提到:“在您同意的情况下,我们可能出于安全、安保和身份识别目的收集和使用您的生物识别信息。”

发布于:1年以前  |  411次阅读  |  详细内容 »

国产扫地机器人热销欧洲,国产割草机器人抢占欧洲草坪

2023年德国柏林消费电子展上,各大企业都带来了最新的理念和产品,而高端化、本土化的中国产品正在不断吸引欧洲等国际市场的目光。

发布于:1年以前  |  406次阅读  |  详细内容 »

罗永浩吐槽iPhone15和14不会有区别,除了序列号变了

罗永浩日前在直播中吐槽苹果即将推出的 iPhone 新品,具体内容为:“以我对我‘子公司’的了解,我认为 iPhone 15 跟 iPhone 14 不会有什么区别的,除了序(列)号变了,这个‘不要脸’的东西,这个‘臭厨子’。

发布于:1年以前  |  398次阅读  |  详细内容 »
 相关文章
Android插件化方案 5年以前  |  237270次阅读
vscode超好用的代码书签插件Bookmarks 2年以前  |  8108次阅读
 目录