游乐游手机版
首页/前端开发/文章详情

Vue Vapor 应用初始化全流程指南

时间:2026-06-22 10:34
VueVapor摒弃虚拟DOM和渲染器,但通过createApp与mount保持API兼容。初始化时创建组件实例并执行setup、render函数完成挂载。运行时架构更轻量,代码已重组至runtime-vapor目录,整体更高效。

在探讨前端框架初始化机制时,有一个有趣的对比:SolidJS 中组件本质上只是“代码组织方式”,程序启动后组件概念便消失,因为其更新机制完全不依赖组件自身。然而 Vue Vapor 则不同——它需要兼容 Vue3 的既有 API,因此组件这一概念必须保留。

20.Vue Vapor 的应用初始化

Vue3 应用初始化的核心逻辑解读

在 Vue3 中,通常先编写一个组件,然后通过以下方式启动应用:

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

createApp 背后的运作逻辑,是将编写的组件先转化为虚拟 DOM,再由渲染器将其渲染到页面上。要理解这一流程,需要先了解渲染器的工作原理。

渲染器通过 createRenderer 函数创建,它本质上是一个工厂函数,返回一个渲染器对象。其基本结构如下:

 复制代码// 创建渲染器
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,这包括元素的创建、删除、修改,以及属性的增删改操作。由于不同平台对 DOM 操作的 API 不同,createRenderer 允许根据平台传入对应的操作函数,实现跨平台兼容。

在日常使用 Vue3 时,默认依赖 runtime-dom 包,该包封装了浏览器环境的 DOM 操作 API:

 复制代码// 创建元素
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)
}

可以看到,创建渲染器时将 createElementinsertsetElementText 等原生 DOM 操作封装为函数,作为参数传入。这样返回的渲染器便专门针对浏览器平台。

平时编写的 const app = createApp(App),其中的 createApp 正是渲染器返回对象中的方法,而它又是通过 createAppAPI 工厂函数生成的。来看 createAppAPI 的具体实现:

 复制代码// 创建 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
    }
}

这段代码本质是一个闭包:createAppAPI 缓存了不同渲染器中的 render 方法,并返回一个 createApp 函数。平时调用的 createApp(App) 正是这个函数,返回的应用对象包含 mount 方法。挂载时先创建根组件的虚拟 DOM,再通过渲染器的 render 方法渲染到指定的 DOM 节点(通常是 #app)。

到了 Vue Vapor 这里,情况则完全不同——由于没有虚拟 DOM,渲染器实例不再需要。但为了保持 API 兼容,Vue Vapor 的启动方式仍需与 Vue3 保持一致。

Vue Vapor 应用初始化设计思路

为了使 Vue3 项目未来能平滑升级到 Vue Vapor,必须支持 createApp(App).mount('#app') 这种调用方式。既然不需要虚拟 DOM 和渲染器,那么直接从 createAppAPI 返回的 createApp 开始改造即可:

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

这样一来,调用方式变为:

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

当然,为了支持 app.mount('#app') 这种字符串形式参数,还需增加一个获取根元素的函数,在 render 方法中处理:

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

接着调整 render 方法:

 复制代码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 完全一致:

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

Vue Vapor 组件初始化流程解析

前文提到,SolidJS 中组件仅作为代码组织方式,初始化后即消失。但 Vue Vapor 因需兼容 Vue3 API,组件这一概念仍需保留。

首先,需要创建一个组件实例对象,用于保存组件的状态信息,例如指令、是否已挂载、生命周期钩子等:

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

有了实例,便需要一个挂载函数将其渲染到页面上:

 复制代码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 函数也需要调整:

 复制代码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 编译后的对象。以下是一个示例:

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

要渲染这种状态组件,需要先执行 setup 方法,将结果作为参数传给 render 函数,再执行 render 获取最终 DOM。这个逻辑只需在 mountComponent 中迭代实现:

 复制代码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,自然也无需各种 diff 算法进行对比,性能开销因此大幅降低。

为了后续开发更顺畅,需要对代码组织架构进行一次重构。

首先在根目录新建 runtime-vapor 目录,将 Vue Vapor 运行时代码统一存放。结构如下:

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

apiCreateApp.js 文件内容:

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

render.js 文件内容:

 复制代码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 文件内容:

 复制代码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 文件内容:

 复制代码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 作为入口文件,导出所有公共 API:

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

接着将测试组件 App 放到 src/App.js 中:

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

最后,src/main.js 可以写成与传统 Vue3 应用完全一样的形式:

 复制代码import { createApp } from '../runtime-vapor/src'
import App from './App'const app = createApp(App)
app.mount('#app')

重构完成后的目录结构如下:

 复制代码├── 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

总结:无虚拟 DOM 的 Vue Vapor 初始化优势

本文从 Vue3 的渲染器和虚拟 DOM 出发,对比梳理了 Vue Vapor 在应用初始化上的设计思路。

Vue Vapor 直接移除虚拟 DOM 和渲染器层,将组件编译为原生 DOM 操作。因此 createApp 不再依赖 createRenderer,而是直接创建应用实例,通过 render 完成挂载。通过引入组件实例对象(createComponentInstance)和挂载函数(mountComponent),Vue Vapor 在兼容 Vue3 组件 API(如 setuprender)的同时,大幅简化了运行时代码结构。

去除 diff 算法和 patch 流程后,整体架构变得更轻量,性能开销也显著降低。代码组织上,将运行时拆分为 apiCreateApprendercomponenttemplate 等独立模块,每个模块职责清晰,后续扩展和维护也更为便捷。

这种设计既保证了 Vue3 项目未来可平滑升级,也为无虚拟 DOM 的高性能渲染奠定了基础。可以说,这是 Vue 技术栈在编译时优化方向上迈出的重要一步。

来源:https://juejin.cn/post/7651920393360883750
上一篇Element Plus样式覆盖难点:deep()、CSS变量与滚动状态类名 下一篇深入解析Teleport的disabled与多目标传送进阶用法实战技巧
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
checked表单属性与CSS变量实现换肤原理
前端开发 · 2026-07-02

checked表单属性与CSS变量实现换肤原理

先聊一个有意思的现象:不需要编写任何 JavaScript,仅靠一个 :checked 伪类,就能驱动整个主题切换系统。听起来很神奇,但原理其实并不复杂——核心在于,:checked 是浏览器原生状态的实时镜像,而不是 JS 模拟出来的开关。 用户点击 ,或者用键盘空格键选中它,状态更新的那一刻,C

HTML meta标签页面定时跳转实现
前端开发 · 2026-07-02

HTML meta标签页面定时跳转实现

说到前端开发中最简洁的页面跳转方式,meta http-equiv= "refresh " 绝对算得上一个经典方案。不过别看它结构简单,格式上稍有疏忽,页面就可能原地卡死,或者直接跳到一个错误地址。下面把几个最容易踩坑的细节彻底讲清楚,帮你避开这些常见陷阱。 使用 http-equiv= "refresh

Cypress跨测试用例状态传递的不推荐但可选方案
前端开发 · 2026-07-02

Cypress跨测试用例状态传递的不推荐但可选方案

Cypress 默认的设计哲学很干脆:每个测试用例都必须是独立小王国,谁也不靠谁。这意味着 it() 执行前,浏览器上下文会被“一键还原”——页面状态、LocalStorage、Cookies 统统清空,强制维护测试隔离。这一规则让很多新手头疼:明明前一个测试已经创建了员工,后一个测试怎么就没法直接

全面深度解析HTML主体main标签唯一性原则与使用规范
前端开发 · 2026-07-02

全面深度解析HTML主体main标签唯一性原则与使用规范

在进行前端无障碍审计时,不少开发者会遇到一个奇怪的场景:浏览器不报错,但Lighthouse却直接标红“duplicate-main”。这其实是语义层与渲染层之间的根本差异。 为什么浏览器不报错但 Lighthouse 直接标红 duplicate-main 关键原因就在于:`main` 是语义锚点

HTML main标签在文档结构中的唯一性详解
前端开发 · 2026-07-02

HTML main标签在文档结构中的唯一性详解

先做一个快速检测:打开你最近开发的一个页面,按下 Ctrl+F 搜索 。如果搜索结果里出现2个以上,那这篇文章建议你认真读完。 本期要聊的主题,是HTML标签中一个看似简单、实际极易踩坑的核心知识点:main标签的唯一性。很多开发者知道这个标签的存在,但真正写到项目里,尤其是用了React、Vue这