前言
我们了解了 SolidJS 的基础实现原理之后,再去探索实现 Vue Vapor 就比较容易了,在 Vue3 中数据响应式部分是独立成了一个库,我们可以直接使用 @vue/reactivity 这个库响应式,然后再去实现运行时部分。为了方便开发,我们这里先搭建一个 Vite 的开发环境。
手动搭建一个 vite 开发环境
首先创建一个项目目录 vue-vapor-cobyte-test,然后进到目录里面进行 npm 项目的初始化:
csharp
npm init -y
然后安装 vite 和 @vue/reactivity:
sql
pnpm add vite @vue/reactivity
然后修改 package.json 文件的 scripts 选项设置 vite 的启动命令:
diff
"scripts": {
+ "dev": "vite"
},
在开发期间 Vite 是一个服务器,所以我们需要创建 vite 的启动入口文件 index.html,内容如下:
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vue Vapor</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="./src/main.js"></script>
</body>
</html>
然后我们创建一个 src 目录,在 src 根目录创建一个 main.js 文件,内容如下:
javascript
const root = document.getElementById('app')
root.textContent = '掘金签约作者:Cobyte'
到此我们基础的 Vite 开发环境就搭建完成了,我们通过命令来启动它:
pnpm dev
页面渲染结果如下:

我们可以看到已经成功启动了我们的项目,接下来我们就在这个 Vite 的开发环境中探索 Vue Vapor 的实现原理吧。
Vue Vapor 的核心原理实现
任何复杂的框架,只有通过最简单的代码去实现它的基本运行逻辑,才算是真正掌握了它实现原理。所谓高端的技术往往采用最原始的编程方式,还有那一句名言:Atwood 定律:Any application that can be written in JavaScript, will eventually be written in JavaScript. (翻译过来即是:凡是能用 JavaScript 写出来的,最终都会用 JavaScript 写出来)。
我们通过前面对 SolidJS 的实现原理的探索,在此基础之上,我们再去实现 Vue Vapor 则比较容易了,就像我们了解了 Vue 的数据响应式原理之后,再去了解 Mobx 的实现原理也变得简单一样,因为从宏观角度来看它们背后的实现原理是相似的,甚至可以说是一样的。
首先我们可以把 SolidJS 中的模板生成函数直接拿过来。
javascript
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
}
我们之前写的 SolidJS 的组件如下:

接着我们像 SolidJS 那样写一个组件,同时响应式变量我们则通过 @vue/reactivity 来生成:
javascript
// 生成创建 button 标签的函数
const _tmpl$ = template('<button></button>')
const App = () => {
const count = ref(0)
// 真正进行创建模板内容的地方
const el = _tmpl$()
el.addEventListener('click', () => {
count.value ++
})
insert(el, count)
return el
}
接着我们把 render 和 insert 方法也拿过来,我们之前写的如下:

现在我们需要对 insert 方法进行改造一下,使用 @vue/reactivity 的 effect 函数实现依赖收集。
javascript
function render(component, parent) {
parent.appendChild(component)
}
function insert(parent, accessor) {
effect(() => {
parent.textContent = accessor.value
})
}
这样我们就可以在实现无虚拟DOM的应用了,并且可以自动更新。
javascript
const root = document.getElementById('app')
render(App(), root)
渲染结果如下:

完整代码如下:
javascript
import { ref, effect } from '@vue/reactivity'
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
}
function render(component, parent) {
parent.appendChild(component)
}
function insert(parent, accessor) {
effect(() => {
parent.textContent = accessor.value
})
}
// 生成创建 button 标签的函数
const _tmpl$ = template('<button></button>')
const App = () => {
const count = ref(0)
// 真正进行创建模板内容的地方
const el = _tmpl$()
el.addEventListener('click', () => {
count.value++
})
insert(el, count)
return el
}
const root = document.getElementById('app')
render(App(), root)
Vue Vapor 设计原理
我们知道 Vue Vapor 是在 Vue3 的设计基础上实现的一个无虚拟 DOM 的版本,那么也就是说它应该是需要兼容目前 Vue3 的语法的,不然就又是一个新框架了。我们在上述实现的小 demo 是在 SolidJS 加 Vue3 的响应式库实现的,总体风格是 SolidJS 的设计风格,很明显上述的实现是不兼容目前 Vue3 的语法的,所以我们还需要进行迭代我们的功能。
那么我们要兼容 Vue3 的语法,首先需要修改的就是我们的 render 函数的调用方式,我们目前所实现的 App 组件是一个函数组件,在我们原来 Vue3 中,render 函数第一个参数则是一个对象或者一个函数,因为 Vue3 中的组件在数据类型上进行区分,就只有两种,一种是虚拟DOM 对象(也就是我们平时所写的 .vue 文件最后会被编译成一个对象),一种则是函数(也就是组件函数)。
调用方式修改如下:
diff
- render(App(), root)
+ render(App, root)
接着我们修改 render 函数:
javascript
function render(comp, container) {
const render = typeof comp === 'function' ? comp : comp.render
const block = render()
insert(block, container)
}
如果是组件函数则组件函数本身就是 render 渲染函数,否则就是组件对象上的 render 函数。得到 render 渲染函数后,执行渲染函数得到真实 DOM,然后挂载到对应的根节点上。
insert 函数的修改则如下:
javascript
function insert(block, parent, anchor = null) {
parent.insertBefore(block, anchor)
}
在 SolidJS 中,依赖收集的副作用函数是放在 insert 函数中,我们也可以放在编译后的组件函数中,所以我们的对编译后的组件修改如下:
javascript
const App = () => {
// 生成创建 button 标签的函数
const _tmpl$ = template('<button></button>')
const count = ref(0)
// 真正进行创建模板内容的地方
const el = _tmpl$()
el.addEventListener('click', () => {
count.value ++
})
effect(() => {
el.textContent = count.value
})
return el
}
渲染结果如下:

完整代码如下:
javascript
import { ref, effect } from '@vue/reactivity'
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
}
function render(comp, container) {
const render = typeof comp === 'function' ? comp : comp.render
const block = render()
insert(block, container)
}
function insert(block, parent, anchor) {
parent.insertBefore(block, anchor)
}
const App = () => {
const _tmpl$ = template('<button></button>')
const count = ref(0)
const el = _tmpl$()
el.addEventListener('click', () => {
count.value++
})
effect(() => {
el.textContent = count.value
})
return el
}
const root = document.getElementById('app')
render(App, root)
我们可以看到 Vue Vapor 核心实现的代码 50 行都不到。
总结
本文从零开始搭建了一个基于 Vite 的开发环境,并借助 @vue/reactivity 响应式库,成功实现了一个极简版的 Vue Vapor 运行时核心。通过对比 SolidJS 的实现思路,我们看到了无虚拟 DOM 框架的基本模式:模板编译生成 DOM 元素、利用响应式系统的 effect 自动追踪依赖并直接更新 DOM 内容。整个核心代码仅 50 行左右,却完整展现了 Vue Vapor 的设计精髓------抛弃虚拟 DOM,回归原生 DOM 操作,同时保持与 Vue3 组件语法的兼容性。
关键实现步骤包括:
- 使用
template函数静态克隆 DOM 节点,提升创建效率; - 通过
ref定义响应式状态,利用effect建立数据与视图的绑定; - 组件函数直接返回真实 DOM 节点,由
render函数完成挂载; - 事件监听直接绑定在原生元素上,无需额外代理。
这一实现不仅验证了 "任何能用 JavaScript 写出的最终都会用 JavaScript 写出" 的理念,也为深入理解 Vue Vapor 后续的指令系统、组件化、调度更新等进阶特性奠定了扎实的基础。无虚拟 DOM 的路径在性能敏感场景下具有天然优势,Vue Vapor 正是 Vue 生态对这一方向的积极探索。
我是程序员Cobyte,现在已转向研究 AI Agent,欢迎添加 v: icobyte,学习交流 AI Agent 应用开发。