20.Vue Vapor 的应用初始化

前言

在 SolidJS 中所谓组件只是代码的一种组件方式,在程序初始化后就不再存在了,因为它的更新不再依赖于组件。但在 Vue Vapor 中因为需要兼容原来 Vue3 的 API,所以还是必须存在组件这个概念。

Vue3 应用初始化的核心逻辑

在 Vue3 中一般我们写了一个组件之后,通过下面的方式进行调用的:

javascript 复制代码
const app = createApp(App)
app.mount("#app")

在 createApp 函数内部主要的过程就是把我们写的组件生成一个虚拟 DOM,然后再通过渲染器把虚拟 DOM 进行渲染到页面上。接下来我们需要去了解渲染器相关的知识。

reateApp 函数是渲染器返回的一个方法,主要是创建一个 Vue3 应用实例。渲染器(renderer)是通过 createRenderer 函数创建,createRenderer 函数主要返回一个渲染器对象。createRender 函数基本结构如下:

javascript 复制代码
// 创建渲染器
function createRenderer(options) {
    // 渲染函数,主要是把一个虚拟 DOM 渲染到某一个元素节点上
    function render(vnode, container) {
        // 具体通过 patch 函数进行渲染
        patch(null, vnode, container, null, null)
    }
    // 补丁函数
    function patch(n1, n2, container) {
		// 根据虚拟DOM 的类型不同进行不同的操作
    }
    // 返回渲染器对象
    return {
        createApp: createAppAPI(render)
    }
}

渲染器的作用就是把虚拟DOM 渲染为真实DOM,所以渲染器需要把我们写的那些元素进行创建、删除、修改和元素属性的创建、删除、修改。那么不同的平台,对元素操作的 API 都不一样,所以在执行 createRenderer 函数的时候,就需要根据不同平台对元素操作特性 API 来创建渲染器。我们平时一般用到的都是 Vue3 默认提供的 runtime-dom 这个包来创建的渲染器(renderer),runtime-dom 包就是根据浏览器的对元素操作的特有的DOM API 进行创建渲染器。runtime-dom 创建渲染器的主要过程如下:

javascript 复制代码
// 创建元素
function createElement(type) {
    return document.createElement(type)
}
// 插入元素
function insert(child, parent, anchor) {
    parent.insertBefore(child, anchor || null)
}
// 创建元素文本
function setElementText (el, text) {
    el.textContent = text
}
// 创建渲染器
const renderer = createRenderer({
    createElement,
    insert,
    setElementText
})
// 创建 Vue3 应用
export function createApp(...args) {
    return renderer.createApp(...args)
}

从上面的代码我们可以看到创建渲染器的时候是把操作原生 DOM 的创建元素、插入元素、创建文本元素的 API 包装成一个个函数,然后作为参数传递给创建渲染器的函数进行创建一个针对 DOM 平台的渲染器。

我们平时一般都是这样创建一个 Vue3 应用的:const app = createApp(App),根据上面的代码我们可以知道这个 createApp 函数是创建渲染器函数 createRenderer 返回的对象中的 createApp 方法,而 createApp 方法又是通过 createAppAPI 函数创建的,接下来,我们来看看 createAppAPI 函数的具体实现。

javascript 复制代码
// 创建 Vue3 应用实例对象
function createAppAPI(render) {
    return function createApp(rootComponent) {
        // 创建 Vue3 应用实例对象
        const app = {
            // 实例挂载方法
            mount(rootContainer) {
                // 创建根组件虚拟DOM
                const vnode = createVNode(rootComponent)
                // 把根组件的虚拟DOM 渲染到 #app 节点上
                render(vnode, rootContainer)
            }
        }
        return app
    }
}

我可以看到具体创建 Vue3 应用实例对象的 createAppAPI 函数是一个闭包函数,主要通过闭包进行缓存不同渲染器内的 render 方法,接下来就是返回一个具体创建 Vue3 应用实例对象的 createApp 方法, const app = createApp(App) 中的 createApp 方法就来自于此。createApp 方法主要返回一个对象,对象里面就包含创建 Vue3 实例对象之后进行挂载的 mount 方法,在 createApp 方法的参数中接收根组件对象,然后 mount 方法挂载的时候,创建根组件的虚拟DOM,再把根组件的虚拟DOM 通过渲染器中的 render 方法进行渲染到具体元素节点上,我们一般就是 id 为 app 的元素上。

而在 Vue Vapor 中我们则不再需要渲染器这个实例了,因为 Vue Vapor 不存在虚拟 DOM 了,但为了兼容 Vue3 的 API,我们的 Vue Vapor 的启动方式也应该跟 Vue3 项目一样。

接下来我们去实现 Vue Vapor 的应用初始化吧。

Vue Vapor 的应用初始化

为了日后才方便将 Vue3 的项目升级为 Vue Vapor 的项目,所以在 Vue Vapor 中我们也需要通过这样的方式 createApp(App).mount('#app') 调用。因为 Vue Vapor 不存在虚拟DOM 了,所以也不需要渲染器了,所以我们可以直接从上面的 createAppAPI 函数中返回的 createApp 函数开始。

javascript 复制代码
function createApp(rootComponent) {
    // 创建 Vue Vapor 应用实例对象
    const app = {
        // 实例挂载方法
        mount(rootContainer) {
            // 把根组件的挂载到 #app 节点上
            render(rootComponent, rootContainer)
        }
    }
    return app
}

这样我们的调用方式则变成:

diff 复制代码
const root = document.getElementById('app')
- render(App, root)
+ const app = createApp(App)
+ app.mount(root)

同时我们为了也可以 app.mount('#app') 的方式调用,我们可以获取根元素的方法在 render 函数中进行兼容处理。首先我们创建一个获取根元素的方法:

javascript 复制代码
function normalizeContainer(container) {
  return typeof container === 'string'
    ? (document.querySelector(container))
    : container
}

接着我们修改 render 方法:

diff 复制代码
function render(comp, container) {
    const render = typeof comp === 'function' ? comp : comp.render
    const block = render()
-    insert(block, container)
+    insert(block, (container = normalizeContainer(container)))
}

这样我们的 Vue Vapor 的应用初始化调用方式就跟 Vue3 的一样了:

javascript 复制代码
const app = createApp(App)
app.mount('#app')

Vue Vapor 组件初始化流程

我们知道在 SolidJS 中所谓组件只是代码的一种组织方式,在程序初始化后就不再存在了,因为它的更新不再依赖于组件。但在 Vue Vapor 中因为需要兼容原来 Vue3 的 API,所以还是必须存在组件这个概念。

首先我们需要创建一个组件实例对象,这个组件实例对象上保存着这个组件的一些状态信息,比如:指令、安装的组件、是否已经挂载、生命周期钩子函数等。

javascript 复制代码
export const createComponentInstance = (
  component
) => {
  const instance = {
    block: null,
    container: null, // set on mount
    component
    // TODO: registory of provides, appContext, lifecycles, ...
  }
  return instance
}

在创建了组件实例之后,我们就需要去挂载这个组件实例,所以我们还需要创建一个挂载组件的函数。

javascript 复制代码
function mountComponent(
  instance,
  container
) {
    instance.container = container
    const render = typeof instance.component === 'function' ? instance.component : instance.component.render
    const block = render()
    insert(block, instance.container)
}

我们把原来属于 render 函数的功能放在了 mountComponent 中进行实现。同时我们需要对 render 函数进行修改:

diff 复制代码
function render(comp, container) {
-    const render = typeof comp === 'function' ? comp : comp.render
-    const block = render()
-    insert(block, (container = normalizeContainer(container)))
+    const instance = createComponentInstance(comp)
+    mountComponent(instance, (container = normalizeContainer(container)))
}

我们之前的测试组件只是一个函数组件,而在 Vue3 中我们一般使用的都是状态组件,包括 script setup 方式的组件,编译后也是一个状态组件,从代码组织结构上看就是一个对象,例子如下:

javascript 复制代码
const App = {
    setup() {
        const count = ref(0)
        return { count } 
    },
    render(_ctx) {
        // 生成创建 button 标签的函数
        const _tmpl$ = template('<button></button>')
        // 真正进行创建模板内容的地方
        const el = _tmpl$()
        el.addEventListener('click', () => {
            _ctx.count.value++
        })
        effect(() => {
            el.textContent = _ctx.count.value
        })
        return el
    }
}

那么我们要实现上述状态组件的渲染,先要执行组件的 setup 方法,然后拿到执行结果然后做为组件 render 函数的参数,然后执行组件的 render 函数得到渲染结果。要实现此功能我们只需要将 mountComponet 函数进行迭代即可。

mountComponent 函数功能迭代如下:

javascript 复制代码
function mountComponent(
  instance,
  container
) {
  instance.container = container

  const { component } = instance
  // 判断是状态组件还是函数组件
  const setupFn =
      typeof component === 'function' ? component : component.setup
  // 获取 setup 方法的执行结果
  const state = setupFn && setupFn()
  // 执行 render 函数获取 DOM 结果
  const block = instance.block = component.setup ? component.render(state) : state
  // 挂载组件DOM元素到到父级元素上
  insert(block, instance.container)
  // 设置已经挂载的标记
  instance.isMounted = true
  // TODO: lifecycle hooks (mounted, ...)
  // const { m } = instance
  // m && invoke(m)
}

我们测试发现状态组件也可以实现渲染了,渲染结果如下:

代码组织结构调整优化

到目前为止我们所有的代码包括测试代码还不到一百行,我们就基本把 Vue Vapor 运行时的基本原理搞清楚了,因为不存在虚拟 DOM 我们整个运行时的架构要比存在虚拟DOM 的运行时架构要轻盈很多的,这也是为什么无虚拟DOM 性能比较好的原因之一,而且可以说是非常重要的原因。没有了虚拟DOM,则不再需要各种 diff 算法对比了,从而节省了性能开销。

为了后续更好的开发,也为了更好地组织我们的代码,我们现在对我们的程序代码组织架构进行设计和重构。

首先我们在根目录新建一个 runtime-vapor 目录把属于 Vue Vapor 运行时的代码全部放到这里面来,我们暂时的目录结构如下:

arduino 复制代码
├── runtime-vapor
│   ├── src
│   │   ├── index.js        // Vapor 运行时程序入口文件
|   |   ├── apiCreateApp.js // 存放 createApp API 
|   |   ├── render.js       // 渲染相关的
|   |   ├── component.js    // 组件相关的
|   |   ├── template.js     // 生成原生模板的

apiCreateApp.js 文件内容如下:

javascript 复制代码
import { render } from "./render"
export function createApp(rootComponent) {
    // 创建 Vue3 应用实例对象
    const app = {
        // 实例挂载方法
        mount(rootContainer) {
            // 把根组件的挂载到 #app 节点上
            render(rootComponent, rootContainer)
        }
    }
    return app
}

render.js 文件内容如下:

javascript 复制代码
import { createComponentInstance } from './component'
export function render(comp, container) {
    const instance = createComponentInstance(comp)
    mountComponent(instance, (container = normalizeContainer(container)))
}

function normalizeContainer(container) {
  return typeof container === 'string'
    ? (document.querySelector(container))
    : container
}

function mountComponent(
  instance,
  container
) {
  instance.container = container

  const { component } = instance
  // 判断是状态组件还是函数组件
  const setupFn =
      typeof component === 'function' ? component : component.setup
  // 获取 setup 方法的执行结果
  const state = setupFn && setupFn()
  // 执行 render 函数获取 DOM 结果
  const block = instance.block = component.setup ? component.render(state) : state
  // 挂载组件DOM元素到到父级元素上
  insert(block, instance.container)
  // 设置已经挂载的标记
  instance.isMounted = true
  // TODO: lifecycle hooks (mounted, ...)
  // const { m } = instance
  // m && invoke(m)
}

function insert(block, parent, anchor = null) {
    parent.insertBefore(block, anchor)
}

component.js 文件内容如下:

javascript 复制代码
let uid = 0
export const createComponentInstance = (
  component
) => {
  const instance = {
    uid: uid++,
    block: null,
    container: null, // set on mount
    component,
    isMounted: false
    // TODO: registory of provides, appContext, lifecycles, ...
  }
  return instance
}

template.js 文件内容如下:

javascript 复制代码
export function template(html) {
    let node
    const create = () => {
        const t = document.createElement("template")
        t.innerHTML = html
        return t.content.firstChild
    }
    const fn = () => (node || (node = create())).cloneNode(true)
    return fn 
}

index.js 文件内容如下:

javascript 复制代码
export { ref, effect } from '@vue/reactivity'
export { render } from './render'
export { template } from './template'
export { createApp } from './apiCreateApp'

接着我们把原来的测试组件对象 App,放到根目录 src/App.js 文件中,代码如下:

js 复制代码
import { ref, template, effect } from "../runtime-vapor/src"
const App = {
    setup() {
        const count = ref(0)
        return { count } 
    },
    render(_ctx) {
        // 生成创建 button 标签的函数
        const _tmpl$ = template('<button></button>')
        // 真正进行创建模板内容的地方
        const el = _tmpl$()
        el.addEventListener('click', () => {
            _ctx.count.value++
        })
        effect(() => {
            el.textContent = _ctx.count.value
        })
        return el
    }
}

export default App

那么 src/main.js 则可以像传统的启动方方式的代码了,代码如下:

javascript 复制代码
import { createApp } from '../runtime-vapor/src'
import App from './App'

const app = createApp(App)
app.mount('#app')

至此我们的代码就重构完成了,重构后的代码组织结构变得更加清晰了,各个文件的职责甚至可以通过文件名称来进行知晓。

重构后的代码组织结构如下:

scss 复制代码
├── runtime-vapor
│   ├── src
│   │   ├── index.js        // Vapor 运行时程序入口文件
|   |   ├── apiCreateApp.js // 存放 createApp API 
|   |   ├── render.js       // 渲染相关的
|   |   ├── component.js    // 组件相关的
|   |   ├── template.js     // 生成原生模板的
├── src
│   ├── App.js    // 测试组件
│   └── main.js   // 应用入口文件
├── index.html
└── package.json

总结

本文从 Vue3 的渲染器与虚拟 DOM 机制出发,对比分析了 Vue Vapor 在应用初始化上的演进思路。

Vue Vapor 摒弃了虚拟 DOM 和渲染器层,直接将组件编译为原生 DOM 操作,因此 createApp 不再依赖 createRenderer,而是直接创建应用实例并调用 render 完成挂载。通过引入组件实例对象(createComponentInstance)和挂载函数(mountComponent),Vue Vapor 在兼容 Vue3 组件 API(如 setuprender)的同时,大幅简化了运行时代码结构。去除了 diff 算法和 patch 流程后,整体架构更加轻盈,性能开销显著降低。代码组织上,将运行时拆分为 apiCreateApprendercomponenttemplate 等独立模块,职责清晰,便于后续扩展和维护。

这种设计既保证了 Vue3 项目未来可平滑升级,也为无虚拟 DOM 的高性能渲染提供了坚实基础,是 Vue 技术栈面向编译时优化的重要探索方向。

我是程序员Cobyte,现在已转向研究 AI Agent,欢迎添加 v: icobyte,学习交流 AI Agent 应用开发。

相关推荐
乘风gg1 小时前
手把手带你实践历时一年总结的 AI Code Review 最佳工作流!
前端·ai编程·cursor
禅思院1 小时前
POST请求发两次?一次讲透CORS预检机制,面试不再翻车
前端·架构·前端框架
IT_陈寒1 小时前
SpringBoot自动配置这么智能,为啥我写的Bean注入不了?
前端·人工智能·后端
LT10157974441 小时前
2026年Web自动化测试工具选型指南:多浏览器兼容解决方案
前端·测试工具·自动化
vx-Biye_Design1 小时前
springboot安阳地区研学旅游服务小程序-计算机毕业设计源码12785
java·vue.js·windows·spring boot·tomcat·maven·mybatis
HYCS1 小时前
用pixi.js实现fabric.js(七):框选、ActiveObject和控制点
前端·javascript·canvas
云浪1 小时前
手把手教你用 fetch 读取 SSE 流,给 AI 聊天加上打字机效果
前端·javascript·vue.js
Csvn2 小时前
Tailwind 动态拼接类名失效?JIT 引擎正在"静态分析"你
前端