不止响应式:Vue3探秘系列— 组件的初始化过程(四)

483 阅读19分钟

前言

Vue3探秘系列文章链接:

不止响应式:Vue3探秘系列— 虚拟结点vnode的页面挂载之旅(一)

不止响应式:Vue3探秘系列— 组件更新会发生什么(二)

不止响应式:Vue3探秘系列— diff算法的完整过程(三)

不止响应式:Vue3探秘系列— 组件的初始化过程(四)

终于轮到你了:Vue3探秘系列— 响应式设计(五)

计算属性:Vue3探秘系列— computed的实现原理(六)

侦听属性:Vue3探秘系列— watch的实现原理(七)

生命周期:Vue3探秘系列— 钩子函数的执行过程(八)

依赖注入:Vue3探秘系列— provide 与 inject 的实现原理(九)

Vue3探秘系列— Props:初始化与更新流程(十)

Vue3探秘系列— directive:指令的实现原理(十一)

Hello~大家好。我是秋天的一阵风

在前两篇的文章中我们探究了当数据改变引发组件进行更新时会发生哪些事情,由此还引申出了vue3的diff算法。本篇我们将继续探究vue3源码中组件的初始化过程,我们先来看一段熟悉的代码:

1. 引出问题

<template>
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>
<script>
import { reactive, computed } from 'vue'
export default {
  setup() {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    })
    function increment() {
      state.count++
    }
    return {
      state,
      increment
    }
  }
}
</script>

对于熟悉vue3的同学们来说,这段代码非常简单:

我们用 Composition API 的写法来实现一个按钮点击数量增加的例子, 除了setup函数之外。组件中没有定义 props、data、computed 这些 options

setup 函数内部,定义了一个响应式对象 state,它是通过 reactive API 创建的。

state 对象有 countdouble 两个属性,其中 count 对应一个数字属性的值;而double 通过 computed API 创建,对应一个计算属性的值。

这里需要注意的是,模板中引用到的变量 stateincrement 包含在 setup 函数的返回对象中,那么它们是如何建立联系的呢?

我们先来回想一下 Vue.js 2.x 编写组件的时候,会在 props、data、methods、computedoptions 中定义一些变量。在组件初始化阶段,Vue.js 内部会处理这些 options,即把定义的变量添加到了组件实例上。等模板编译成 render 函数的时候,内部通过 with(this){} 的语法去访问在组件实例中的变量。

那么到了 Vue.js 3.0,既支持组件定义 setup 函数,而且在模板 render 的时候,又可以访问到 setup 函数返回的值,这是如何实现的?我们来一探究竟。

2. 回顾组件渲染流程

在第一篇文章: 不止响应式:Vue3探秘系列— 虚拟结点vnode的页面挂载之旅(一) 中,我们提到过组件渲染要经历三个步骤:

image.png

其中 渲染 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)

先看 创建组件实例 的流程,我们要关注 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
}

如果你对vue2源码有过了解,应该记得 在 Vue.js 2.x 中,作者是使用 new Vue 来初始化一个组件的实例,到了 Vue.js 3.0,我们直接通过创建对象去创建组件的实例。这两种方式并无本质的区别,都是引用一个对象,在整个组件的生命周期中去维护组件的状态数据上下文环境

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

二、 设置组件实例 (setupComponent)

接着是组件实例的设置流程,对 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
}

1. initProps (初始化props)

2. initSlots (初始化插槽)

可以看到,我们从组件 vnode 中获取了 props、children、shapeFlag 等属性,然后分别对 props插槽进行初始化。根据 shapeFlag 的值,我们可以判断这是不是一个有状态组件,如果是则要进一步去设置有状态组件的实例。

3. setupStatefulComponent (设置有状态组件实例)

接下来我们要关注到 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)
  }
}

(1) 创建渲染上下文代理

其实在 Vue.js 2.x 中,也有类似的数据代理逻辑,比如 props 求值后的数据,实际上存储在 this._props 上,而 data 中定义的数据存储在 this._data 上。

在构建渲染上下文代理的过程中,核心操作是对instance.ctx进行代理。在这之前,我们有必要深入探讨一下,为何这一代理步骤如此关键?

回想一下Vue.js 2.x版本中的数据管理机制,你会发现一种相似的设计理念在其中体现。例如,当组件接收到属性(props)时,这些属性的真实存储位置并非直接在this对象上,而是被巧妙地隐藏在this._props中。同样地,由data选项定义的状态也并非直接挂载在this上,而是被存放在this._data之下。这种设计背后的智慧在于,它不仅有助于保持数据的清晰组织,还为后续的数据追踪和响应式系统提供了坚实的基础。

那么,为何需要这样的代理机制呢?答案在于通过代理,开发者可以直接通过this访问到_props_data中的数据,无需显式地引用内部字段,从而简化了代码,提高了可读性和易用性。

举个例子:

<template>
  <p>{{ msg }}</p>
</template>
<script>
export default {
  data() {
    msg: 1
  }
}
</script>


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

到了 Vue.js 3.0,为了方便维护,我们把组件中不同状态的数据存储到不同的属性中,

比如存储到 setupState、ctx、data、props 中。我们在执行组件渲染函数的时候,为了方便用户使用,会直接访问渲染上下文 instance.ctx 中的属性,所以我们也要做一层 proxy

对渲染上下文 instance.ctx 属性的访问和修改,代理到对 setupState、ctx、data、props 中的数据的访问和修改。

明确了代理的需求后,我们接下来就要分析 proxy 的几个方法: get、set 和 has

get 函数

当我们访问 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 不以 $ 开头的情况,这部分数据可能是 setupState、data、props、ctx 中的一种,其中data、props 我们已经很熟悉了;setupState 就是 setup 函数返回的数据,稍后我们会详细说;ctx 包括了计算属性、组件方法和用户自定义的一些数据。

如果 key 不以 $ 开头,那么就依次判断 setupState、data、props、ctx 中是否包含这个 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方法的密集调用上,尤其是当它被用来检查特定键是否存在于复杂数据结构中时。相较于此,直接在简单对象上执行属性访问则显得更为高效快捷。

鉴于此,accessCache[key]的引入便显得尤为重要。它的作用是在首次成功获取对应键的数据后,立即将这一结果缓存起来。这样一来,当下一次需要根据相同的键查询数据时,系统可以直接从accessCache[key]中快速检索出所需值,而无需重复执行耗时的hasOwn检查。这种策略不仅极大地提升了数据访问的速度,还减少了不必要的计算开销,实现了对性能的有效优化。

如果 key 以 $ 开头,那么接下来又会有一系列的判断.

  • 首先判断是不是 Vue.js 内部公开的 xxx属性或方法(比如xxx 属性或方法(比如 parent);

  • 然后判断是不是 vue-loader 编译注入的 css 模块内部的 key;

  • 接着判断是不是用户自定义以 $ 开头的 key;

  • 最后判断是不是全局属性。

如果都不满足,就剩两种情况了,即在非生产环境下就会报两种类型的警告,

  • 第一种是在 data 中定义的数据以 开头的警告,因为开头的警告,因为 是保留字符,不会做代理;

  • 第二种是在模板中使用的变量没有定义的警告。

set 函数

接下来是 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.js 内部以 $ 开头的保留属性赋值,同样也会收到一条警告。

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

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

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

has 函数

最后是 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 是否存在于 accessCache、data、setupState、props 、用户数据、公开属性以及全局属性中,然后返回结果。

(2) 处理 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)
}

举个例子,我们有个 HelloWorld 子组件,如下:

<template>
  <p>{{ msg }}</p>
  <button @click="onClick">Toggle</button>
</template>
<script>
  export default {
    props: {
      msg: String
    },
    setup (props, { emit }) {
      function onClick () {
        emit('toggle')
      }
      return {
        onClick
      }
    }
  }
</script>


我们在父组件引用这个组件:

<template>
  <HelloWorld @toggle="toggle" :msg="msg"></HelloWorld>
</template>
<script>
  import { ref } from 'vue'
  import HelloWorld from "./components/HelloWorld";
  export default {
    components: { HelloWorld },
    setup () {
      const msg = ref('Hello World')
      function toggle () {
        msg.value = msg.value === 'Hello World' ? 'Hello Vue' : 'Hello World'
      }
      return {
        toggle,
        msg
      }
    }
  }
</script>

可以看到,HelloWorld 子组件的 setup 函数接收两个参数,第一个参数 props 对应父组件传入的 props 数据,第二个参数 emit 是一个对象,实际上就是 setupContext

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

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

可以预见的是,这个 setupContext 对应的就是 setup 函数第二个参数,我们接下来看一下 setup 函数具体是如何执行的。

执行 setup 函数

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

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

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

handleSetupResult (处理setup的返回结果)
handleSetupResult(instance, setupResult)

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 不仅仅支持返回一个对象,也可以返回一个函数作为组件的渲染函数。我们可以改写前面的示例,来看一下这时的情况:

<script>
  import { h } from 'vue'
  export default {
    props: {
      msg: String
    },
    setup (props, { emit }) {
      function onClick () {
        emit('toggle')
      }
      return (ctx) => {
        return [
          h('p', null, ctx.msg),
          h('button', { onClick: onClick }, 'Toggle')
        ]
      }
    }
  }
</script>


这里,我们删除了 HelloWorld 子组件的template部分,并把 setup 函数的返回结果改成了函数,也就是说它会作为组件的渲染函数,一切运行正常。

handleSetupResult 的最后,会执行 finishComponentSetup 函数完成组件实例的设置。

另外当组件没有定义的 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)
    }
  }
  // 兼容 Vue.js 2.x Options API
  {
    currentInstance = instance
    applyOptions(instance, Component)
    currentInstance = null
  }
}

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

标准化模板或者渲染函数

在深入探讨组件渲染机制之前,我们有必要先梳理一下Vue组件开发的常见模式,以及它们如何影响最终的渲染流程。Vue提供了灵活多样的组件构建途径,其中最为人熟知的莫过于SFC(Single File Components)和传统的JavaScript组件定义方式。

SFC,即单文件组件,是一种集大成者般的开发模式,允许开发者在一个.vue文件中同时定义组件的模板、脚本和样式,形成高度封装的组件单元。然而,由于浏览器无法直接解析和执行.vue文件,因此在项目构建阶段,借助Webpack及其插件如vue-loader,会将SFC中的各个部分分别编译为JavaScriptCSS,同时将模板部分转换为可执行的render函数,嵌入到组件的配置中,为后续的组件渲染做好准备。

另一种开发组件的方式,则是直接在浏览器环境中利用Vue.js,省去了构建工具的介入。这种方式下,开发者直接在组件配置对象的template属性中编写HTML模板,而在运行时,Vue会负责将这些模板编译为render函数,适用于那些已有一定历史遗留的项目,或是希望快速搭建原型的场景。

基于上述不同的开发场景,Vue提供了两种构建版本:runtime-onlyruntime-compiled

前者剔除了模板编译器,专注于组件的运行时环境,因此体积更为精简,且无需在运行时进行模板编译,带来了更快的启动速度和更佳的性能表现,是现代项目开发的首选。

runtime-compiled版本则保留了编译器,适用于那些需要在运行时动态编译模板的特殊情况,如上述提及的旧项目改造或某些动态内容生成的场景。

总之,Vue的双版本策略,既满足了现代Web应用对高性能和轻量化的需求,又兼顾了对传统项目的兼容性,为开发者提供了广泛的选择空间。

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

在 Vue.js 3.0 中,compile 方法是通过外部注册的:

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

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

compile 和组件 template 属性存在,render 方法不存在的情况。此时, runtime-compiled 版本会在 JavaScript 运行时进行模板编译,生成 render 函数。

compilerender 方法不存在,组件 template 属性存在的情况。此时由于没有 compile,这里用的是 runtime-only 的版本,因此要报一个警告来告诉用户,想要运行时编译得使用 runtime-compiled 版本的 Vue.js。

组件既没有写 render 函数,也没有写 template 模板,此时要报一个警告,告诉用户组件缺少了 render 函数或者 template 模板

处理完以上情况后,就要把组件的 render 函数赋值给 instance.render

到了组件渲染的时候,就可以运行 instance.render 函数生成组件的子树 vnode 了。

Options API:兼容 Vue.js 2.x

我们知道 Vue.js 2.x 是通过组件对象的方式去描述一个组件,之前我们也说过,Vue.js 3.0 仍然支持 Vue.js 2.x 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
}

总结

在本次课程中,我们深入探讨了Vue组件生命周期的起始阶段——初始化流程,这是一个至关重要的环节,奠定了组件后续行为和状态管理的基础。初始化流程中的两大分支就是创建组件实例和设置组件实例

特别地,我们细致剖析了渲染上下文代理的精妙机制,这是Vue框架为了优化数据访问效率和提升开发者体验而设计的关键一环。通过这一过程,我们理解了如何在组件内部无缝地访问和操作数据,而无需直接触及底层的数据结构。

接着,我们聚焦于Composition API的核心——setup函数,探究了其执行的精确时机,以及它是如何作为桥梁,连接起开发者在setup中定义的逻辑与组件模板的渲染输出。

我们还详细探讨了组件模板或自定义渲染函数的标准化流程,这一过程确保了无论开发者采用何种方式定义组件的呈现形式,Vue都能将其转化为统一的内部表示,为后续的渲染和更新奠定基础。

最后,我们回顾了Vue.js 2.x中Options API的兼容性策略,这对于维护既有项目或逐步迁移到Vue 3的开发者来说至关重要。