Vue3 组件的实现原理

#vue #vue3 #2023/06/10 #vue原理

目录

1. 总结

1.1. 摘要

本文详细介绍了Vue3组件的实现原理,包括渲染器、组件和虚拟DOM之间的关系,组件状态的保存和更新机制,组件实例和生命周期的实现,props的处理,setup函数的作用,组件事件和emit的实现,插槽的工作原理,以及生命周期的注册方法。文章深入探讨了Vue3组件系统的核心概念和内部机制,为理解Vue3的工作原理提供了全面的视角。

1.2. 关键点

  • 渲染器根据vdom类型判断是否为组件,并使用mountComponent或patchComponent进行挂载和更新。
  • 组件状态:通过data()保存,并使用reactive使其成为响应式数据。
  • 组件实例:是一个对象,存储组件运行过程的所有信息,包括状态、props、生命周期等。
  • props机制:需要编写大量边界代码,并通过渲染上下文对象暴露数据。
  • setup函数:为组合式API设计,可返回渲染函数数据对象
  • emit函数:用于发射组件自定义事件,v-on指令绑定的事件会存储在props对象中。
  • 插槽内容:被编译为插槽函数,<slot>标签被编译为插槽函数的调用。
  • 生命周期函数通过onMounted等方法注册,存储在组件实例的相应数组中。

2. 渲染器、组件与 vdom 之间的关系

前文 11. 前端/2. 前端架构篇/3. Vue 篇/2. Vue 原理篇/1. Vue3 的渲染器原理|1. Vue3 的渲染器原理 实现了对单个 vdom 的渲染,比如对下面 vdom 的渲染:

const Fragment = Symbol()
const newVnode = {
  type: 'div',
  children: [
    {
      type: Fragment,
      children: [
        { type: 'p', children: 'text 1' },
        { type: 'p', children: 'text 2' },
        { type: 'p', children: 'text 3' }
      ]
    },
    { type: 'section', children: '分割线' }
  ]
}

但实际页面,会包括很多 vdom 甚至是各种嵌套的 vdom

那么,如何使用 vdom 来描述页面,描述 UI 呢?

这里直接给出答案:如下结构

const C1 = {
    name: 'C1',
    render() {
        return {
            type: 'div',
            children: [
                { type: 'p', children: 'text 1' },
                { type: 'p', children: 'text 2' },
                { type: 'p', children: 'text 3' }
            ]
        }
    }
}

两个要点:

  • ① 包含 render 函数
  • return 一个 vnode

下图展示了 渲染器、组件 、 vnode 的关系 , 很重要,打开看看

  • figjam

  • 总结下就是,渲染器会根据 vdom 的类型

    • 是字符串
    • 是文本类型(Symbol)
    • 是 Fragment
    • 是对象或者函数
      • 来判断它是否是否是组件
        • 如果是,则会继续递归使用 mountComponent 或者 patchComponent来完成组件的挂载和更新
        • 部分代码如下:
if (typeof type === 'string') {
    if (!n1) {
        mountElement(n2, container, anchor)
    } else {
        patchElement(n1, n2)
    }
} else if (type === Text) {
    if (!n1) {
        const el = n2.el = createText(n2.children)
        insert(el, container)
    } else {
        const el = n2.el = n1.el
        if (n2.children !== n1.children) {
            setText(el, n2.children)
        }
    }
} else if (type === Fragment) {
    if (!n1) {
        n2.children.forEach(c => patch(null, c, container))
    } else {
        patchChildren(n1, n2, container)
    }
} else if (typeof type === 'object' || typeof type === 'function') {
    // component
    if (!n1) {
        mountComponent(n2, container, anchor)
    } else {
        patchComponent(n1, n2, anchor)
    }
}

3. 如何保存组件状态,以及状态改变了同步更新组件

如果保存组件状态呢?答案是 data() ,并且需要让 data 数据是响应式的,所以代码应该如下

const state = data ? reactive(data()) : null

所以,每次更新 data ,会重新渲染,但是浏览器渲染是同步任务,所以需要有个调度器来调度,它通过promise 实现了一个微任务队列,避免重复渲染,去重任务队列等。

关于调度器微任务队列的实现,更多参考 11. 前端/2. 前端架构篇/3. Vue 篇/2. Vue 原理篇/3. Vue3 的响应式的系统设计原理|3. Vue3 的响应式的系统设计原理

部分代码如下:注意下面的 scheduler

 const state = data ? reactive(data()) : null
 effect(() => {
    const subTree = render.call(renderContext, renderContext)
}, {
    scheduler: queueJob
})

4. 组件实例与组件的生命周期

  • 组件实例,本质就是一个对象,存储着组件运行过程的所有信息,比如
    • 组件状态
    • 是否卸载
    • 组件的子树
    • 插槽
    • 生命周期等
  • 如下代码:
const instance = {
        state,
        props: shallowReactive(props),
        isMounted: false,
        subTree: null,
        slots,
        mounted: []
}

各类生命周期,就在具体位置上添加即可,如下:

一个组件可能存在多个同类型的组件,比如 mixin 进来的组件,这时候就需要数组了。但核心原理不变

5. props 与组件的被动更新

  • 副作用自更新所引起的子组件更新叫作子组件的被动更新
  • 我们需要检查是否需要真的更新
    • 比如 props 如果根本就没变?
    • 如果需要更新,需要同步更新子组件的 propsslots 等内容
    • 其实要 完善 vue 中的 props 机制,需要编写大量的边界代码

由于 props 数据与组件自身的状态数据都需要暴露到渲染函数中,并使得渲染函数能够通过 this 访问它们,因此我们需要封装一个渲染上下文对象(renderContext), 它实际上是组件实例的代理对象。在渲染函数内访问组件实例所暴露的数据都是通过该代理对象实现的。

vue 中,没有在 props 选项中的 props 数据都将存储到 attrs 对象中

6. setup 函数的作用 与 实现

setup 函数,它只会在挂载时执行一次,该函数是为了组合式 API 而生的,==所以我们要避免将其与 Vue.js 2 中的“传统”组件选项(如 methods watch data) 混合使用。==

setup 函数的返回值可以是两种类型

  • 如果返回函数,则将该函数作为组件的渲染函数
  • 如果返回数据对象,则将该对象暴露到渲染上下文中,暴露给模板使用。
const InnerComp = {
    name: 'InnerComp',
    /**
     * expose: 暴露给父组件的属性
     * */
    setup(props, {emit, slots,attrs,expose}) {
        const count = ref(0);
        // 返回一个【数据对象】
        return {
            count,
        }
        // 返回一个【渲染函数】
        return () => ({
            type: 'span',
            children: 'inner'
        })
    }
}

7. 组件事件与 emit

  • emit 函数包含在 setupContext 对象中,可以通过 emit 函数 发射组件的自定义事件。
  • 通过 v-on 指令为组件绑定的事件在经过编译后,会以 onXxx 的形式存储到 props 对象中。
  • 当 emit 函数执行时,会在 props 对象中寻找对应的事件处理函数并执行它。

8. 插槽 slot

它借鉴了 Web Component 中 <slot> 标签的概念。

  • 插槽内容会被编译为插槽函数,插槽函数的返回值就是向槽位填充的内容
  • <slot> 标签则会被编译为插槽函数的调用,通过执行对应的插槽函数,得到外部向槽位填充的内容(即虚拟 DOM),最后将该内容渲染到槽位中。

我们以 MyComponent 组件为例,它的模板如下:

|376

上面的代码会编译成如下:

|328

具体使用 MyComponent 组件

|288

上面的模板,会编译成如下渲染函数

|512

插槽的实现是不是和 React render props 的概念很像?,如下图:

|464

9. 注册生命周期

先看代码,下面注册两个 onMounted ,它会被放到一个数组中。

|424

  • 通过 onMounted 注册的生命周期函数会被注册到当前组件实例instance.mounted 数组中。
  • 为了维护当前正在初始化的组件实例,我们定义了全局变量currentinstance,以及用来设置该变量的 setCurrentInstance 函数。
  • 其他生命周期同理。

10. 最后

一些具体代码实现细节,没有太抠,知道大致的原理即可,真有应用场景再仔细研究一下具体代码实现细节。

11. 参考

  • 《Vue 设计与实现》