詳解Vue Vapor 的應(yīng)用初始化
前言
在 SolidJS 中所謂組件只是代碼的一種組件方式,在程序初始化后就不再存在了,因?yàn)樗母虏辉僖蕾囉诮M件。但在 Vue Vapor 中因?yàn)樾枰嫒菰瓉?Vue3 的 API,所以還是必須存在組件這個(gè)概念。
Vue3 應(yīng)用初始化的核心邏輯
在 Vue3 中一般我們寫了一個(gè)組件之后,通過下面的方式進(jìn)行調(diào)用的:
const app = createApp(App)
app.mount("#app")
在 createApp 函數(shù)內(nèi)部主要的過程就是把我們寫的組件生成一個(gè)虛擬 DOM,然后再通過渲染器把虛擬 DOM 進(jìn)行渲染到頁面上。接下來我們需要去了解渲染器相關(guān)的知識(shí)。
reateApp 函數(shù)是渲染器返回的一個(gè)方法,主要是創(chuàng)建一個(gè) Vue3 應(yīng)用實(shí)例。渲染器(renderer)是通過 createRenderer 函數(shù)創(chuàng)建,createRenderer 函數(shù)主要返回一個(gè)渲染器對(duì)象。createRender 函數(shù)基本結(jié)構(gòu)如下:
// 創(chuàng)建渲染器
function createRenderer(options) {
// 渲染函數(shù),主要是把一個(gè)虛擬 DOM 渲染到某一個(gè)元素節(jié)點(diǎn)上
function render(vnode, container) {
// 具體通過 patch 函數(shù)進(jìn)行渲染
patch(null, vnode, container, null, null)
}
// 補(bǔ)丁函數(shù)
function patch(n1, n2, container) {
// 根據(jù)虛擬DOM 的類型不同進(jìn)行不同的操作
}
// 返回渲染器對(duì)象
return {
createApp: createAppAPI(render)
}
}
渲染器的作用就是把虛擬DOM 渲染為真實(shí)DOM,所以渲染器需要把我們寫的那些元素進(jìn)行創(chuàng)建、刪除、修改和元素屬性的創(chuàng)建、刪除、修改。那么不同的平臺(tái),對(duì)元素操作的 API 都不一樣,所以在執(zhí)行 createRenderer 函數(shù)的時(shí)候,就需要根據(jù)不同平臺(tái)對(duì)元素操作特性 API 來創(chuàng)建渲染器。我們平時(shí)一般用到的都是 Vue3 默認(rèn)提供的 runtime-dom 這個(gè)包來創(chuàng)建的渲染器(renderer),runtime-dom 包就是根據(jù)瀏覽器的對(duì)元素操作的特有的DOM API 進(jìn)行創(chuàng)建渲染器。runtime-dom 創(chuàng)建渲染器的主要過程如下:
// 創(chuàng)建元素
function createElement(type) {
return document.createElement(type)
}
// 插入元素
function insert(child, parent, anchor) {
parent.insertBefore(child, anchor || null)
}
// 創(chuàng)建元素文本
function setElementText (el, text) {
el.textContent = text
}
// 創(chuàng)建渲染器
const renderer = createRenderer({
createElement,
insert,
setElementText
})
// 創(chuàng)建 Vue3 應(yīng)用
export function createApp(...args) {
return renderer.createApp(...args)
}
從上面的代碼我們可以看到創(chuàng)建渲染器的時(shí)候是把操作原生 DOM 的創(chuàng)建元素、插入元素、創(chuàng)建文本元素的 API 包裝成一個(gè)個(gè)函數(shù),然后作為參數(shù)傳遞給創(chuàng)建渲染器的函數(shù)進(jìn)行創(chuàng)建一個(gè)針對(duì) DOM 平臺(tái)的渲染器。
我們平時(shí)一般都是這樣創(chuàng)建一個(gè) Vue3 應(yīng)用的:const app = createApp(App),根據(jù)上面的代碼我們可以知道這個(gè) createApp 函數(shù)是創(chuàng)建渲染器函數(shù) createRenderer 返回的對(duì)象中的 createApp 方法,而 createApp 方法又是通過 createAppAPI 函數(shù)創(chuàng)建的,接下來,我們來看看 createAppAPI 函數(shù)的具體實(shí)現(xiàn)。
// 創(chuàng)建 Vue3 應(yīng)用實(shí)例對(duì)象
function createAppAPI(render) {
return function createApp(rootComponent) {
// 創(chuàng)建 Vue3 應(yīng)用實(shí)例對(duì)象
const app = {
// 實(shí)例掛載方法
mount(rootContainer) {
// 創(chuàng)建根組件虛擬DOM
const vnode = createVNode(rootComponent)
// 把根組件的虛擬DOM 渲染到 #app 節(jié)點(diǎn)上
render(vnode, rootContainer)
}
}
return app
}
}
我可以看到具體創(chuàng)建 Vue3 應(yīng)用實(shí)例對(duì)象的 createAppAPI 函數(shù)是一個(gè)閉包函數(shù),主要通過閉包進(jìn)行緩存不同渲染器內(nèi)的 render 方法,接下來就是返回一個(gè)具體創(chuàng)建 Vue3 應(yīng)用實(shí)例對(duì)象的 createApp 方法, const app = createApp(App) 中的 createApp 方法就來自于此。createApp 方法主要返回一個(gè)對(duì)象,對(duì)象里面就包含創(chuàng)建 Vue3 實(shí)例對(duì)象之后進(jìn)行掛載的 mount 方法,在 createApp 方法的參數(shù)中接收根組件對(duì)象,然后 mount 方法掛載的時(shí)候,創(chuàng)建根組件的虛擬DOM,再把根組件的虛擬DOM 通過渲染器中的 render 方法進(jìn)行渲染到具體元素節(jié)點(diǎn)上,我們一般就是 id 為 app 的元素上。
而在 Vue Vapor 中我們則不再需要渲染器這個(gè)實(shí)例了,因?yàn)?Vue Vapor 不存在虛擬 DOM 了,但為了兼容 Vue3 的 API,我們的 Vue Vapor 的啟動(dòng)方式也應(yīng)該跟 Vue3 項(xiàng)目一樣。
接下來我們?nèi)?shí)現(xiàn) Vue Vapor 的應(yīng)用初始化吧。
Vue Vapor 的應(yīng)用初始化
為了日后才方便將 Vue3 的項(xiàng)目升級(jí)為 Vue Vapor 的項(xiàng)目,所以在 Vue Vapor 中我們也需要通過這樣的方式 createApp(App).mount('#app') 調(diào)用。因?yàn)?Vue Vapor 不存在虛擬DOM 了,所以也不需要渲染器了,所以我們可以直接從上面的 createAppAPI 函數(shù)中返回的 createApp 函數(shù)開始。
function createApp(rootComponent) {
// 創(chuàng)建 Vue Vapor 應(yīng)用實(shí)例對(duì)象
const app = {
// 實(shí)例掛載方法
mount(rootContainer) {
// 把根組件的掛載到 #app 節(jié)點(diǎn)上
render(rootComponent, rootContainer)
}
}
return app
}
這樣我們的調(diào)用方式則變成:
const root = document.getElementById('app')
- render(App, root)
+ const app = createApp(App)
+ app.mount(root)
同時(shí)我們?yōu)榱艘部梢?app.mount('#app') 的方式調(diào)用,我們可以獲取根元素的方法在 render 函數(shù)中進(jìn)行兼容處理。首先我們創(chuàng)建一個(gè)獲取根元素的方法:
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 的應(yīng)用初始化調(diào)用方式就跟 Vue3 的一樣了:
const app = createApp(App)
app.mount('#app')
Vue Vapor 組件初始化流程
我們知道在 SolidJS 中所謂組件只是代碼的一種組織方式,在程序初始化后就不再存在了,因?yàn)樗母虏辉僖蕾囉诮M件。但在 Vue Vapor 中因?yàn)樾枰嫒菰瓉?Vue3 的 API,所以還是必須存在組件這個(gè)概念。
首先我們需要?jiǎng)?chuàng)建一個(gè)組件實(shí)例對(duì)象,這個(gè)組件實(shí)例對(duì)象上保存著這個(gè)組件的一些狀態(tài)信息,比如:指令、安裝的組件、是否已經(jīng)掛載、生命周期鉤子函數(shù)等。
export const createComponentInstance = (
component
) => {
const instance = {
block: null,
container: null, // set on mount
component
// TODO: registory of provides, appContext, lifecycles, ...
}
return instance
}
在創(chuàng)建了組件實(shí)例之后,我們就需要去掛載這個(gè)組件實(shí)例,所以我們還需要?jiǎng)?chuàng)建一個(gè)掛載組件的函數(shù)。
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 函數(shù)的功能放在了 mountComponent 中進(jìn)行實(shí)現(xiàn)。同時(shí)我們需要對(duì) render 函數(shù)進(jìn)行修改:
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)))
}
我們之前的測試組件只是一個(gè)函數(shù)組件,而在 Vue3 中我們一般使用的都是狀態(tài)組件,包括 script setup 方式的組件,編譯后也是一個(gè)狀態(tài)組件,從代碼組織結(jié)構(gòu)上看就是一個(gè)對(duì)象,例子如下:
const App = {
setup() {
const count = ref(0)
return { count }
},
render(_ctx) {
// 生成創(chuàng)建 button 標(biāo)簽的函數(shù)
const _tmpl$ = template('<button></button>')
// 真正進(jìn)行創(chuàng)建模板內(nèi)容的地方
const el = _tmpl$()
el.addEventListener('click', () => {
_ctx.count.value++
})
effect(() => {
el.textContent = _ctx.count.value
})
return el
}
}
那么我們要實(shí)現(xiàn)上述狀態(tài)組件的渲染,先要執(zhí)行組件的 setup 方法,然后拿到執(zhí)行結(jié)果然后做為組件 render 函數(shù)的參數(shù),然后執(zhí)行組件的 render 函數(shù)得到渲染結(jié)果。要實(shí)現(xiàn)此功能我們只需要將 mountComponet 函數(shù)進(jìn)行迭代即可。
mountComponent 函數(shù)功能迭代如下:
function mountComponent(
instance,
container
) {
instance.container = container
const { component } = instance
// 判斷是狀態(tài)組件還是函數(shù)組件
const setupFn =
typeof component === 'function' ? component : component.setup
// 獲取 setup 方法的執(zhí)行結(jié)果
const state = setupFn && setupFn()
// 執(zhí)行 render 函數(shù)獲取 DOM 結(jié)果
const block = instance.block = component.setup ? component.render(state) : state
// 掛載組件DOM元素到到父級(jí)元素上
insert(block, instance.container)
// 設(shè)置已經(jīng)掛載的標(biāo)記
instance.isMounted = true
// TODO: lifecycle hooks (mounted, ...)
// const { m } = instance
// m && invoke(m)
}
我們測試發(fā)現(xiàn)狀態(tài)組件也可以實(shí)現(xiàn)渲染了,渲染結(jié)果如下:
代碼組織結(jié)構(gòu)調(diào)整優(yōu)化
到目前為止我們所有的代碼包括測試代碼還不到一百行,我們就基本把 Vue Vapor 運(yùn)行時(shí)的基本原理搞清楚了,因?yàn)椴淮嬖谔摂M DOM 我們整個(gè)運(yùn)行時(shí)的架構(gòu)要比存在虛擬DOM 的運(yùn)行時(shí)架構(gòu)要輕盈很多的,這也是為什么無虛擬DOM 性能比較好的原因之一,而且可以說是非常重要的原因。沒有了虛擬DOM,則不再需要各種 diff 算法對(duì)比了,從而節(jié)省了性能開銷。
為了后續(xù)更好的開發(fā),也為了更好地組織我們的代碼,我們現(xiàn)在對(duì)我們的程序代碼組織架構(gòu)進(jìn)行設(shè)計(jì)和重構(gòu)。
首先我們?cè)诟夸浶陆ㄒ粋€(gè) runtime-vapor 目錄把屬于 Vue Vapor 運(yùn)行時(shí)的代碼全部放到這里面來,我們暫時(shí)的目錄結(jié)構(gòu)如下:
├── runtime-vapor │ ├── src │ │ ├── index.js // Vapor 運(yùn)行時(shí)程序入口文件 | | ├── apiCreateApp.js // 存放 createApp API | | ├── render.js // 渲染相關(guān)的 | | ├── component.js // 組件相關(guān)的 | | ├── template.js // 生成原生模板的
apiCreateApp.js 文件內(nèi)容如下:
import { render } from "./render"
export function createApp(rootComponent) {
// 創(chuàng)建 Vue3 應(yīng)用實(shí)例對(duì)象
const app = {
// 實(shí)例掛載方法
mount(rootContainer) {
// 把根組件的掛載到 #app 節(jié)點(diǎn)上
render(rootComponent, rootContainer)
}
}
return app
}
render.js 文件內(nèi)容如下:
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
// 判斷是狀態(tài)組件還是函數(shù)組件
const setupFn =
typeof component === 'function' ? component : component.setup
// 獲取 setup 方法的執(zhí)行結(jié)果
const state = setupFn && setupFn()
// 執(zhí)行 render 函數(shù)獲取 DOM 結(jié)果
const block = instance.block = component.setup ? component.render(state) : state
// 掛載組件DOM元素到到父級(jí)元素上
insert(block, instance.container)
// 設(shè)置已經(jīng)掛載的標(biāo)記
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 文件內(nèi)容如下:
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 文件內(nèi)容如下:
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 文件內(nèi)容如下:
export { ref, effect } from '@vue/reactivity'
export { render } from './render'
export { template } from './template'
export { createApp } from './apiCreateApp'
接著我們把原來的測試組件對(duì)象 App,放到根目錄 src/App.js 文件中,代碼如下:
import { ref, template, effect } from "../runtime-vapor/src"
const App = {
setup() {
const count = ref(0)
return { count }
},
render(_ctx) {
// 生成創(chuàng)建 button 標(biāo)簽的函數(shù)
const _tmpl$ = template('<button></button>')
// 真正進(jìn)行創(chuàng)建模板內(nèi)容的地方
const el = _tmpl$()
el.addEventListener('click', () => {
_ctx.count.value++
})
effect(() => {
el.textContent = _ctx.count.value
})
return el
}
}
export default App
那么 src/main.js 則可以像傳統(tǒng)的啟動(dòng)方方式的代碼了,代碼如下:
import { createApp } from '../runtime-vapor/src'
import App from './App'
const app = createApp(App)
app.mount('#app')
至此我們的代碼就重構(gòu)完成了,重構(gòu)后的代碼組織結(jié)構(gòu)變得更加清晰了,各個(gè)文件的職責(zé)甚至可以通過文件名稱來進(jìn)行知曉。
重構(gòu)后的代碼組織結(jié)構(gòu)如下:
├── runtime-vapor │ ├── src │ │ ├── index.js // Vapor 運(yùn)行時(shí)程序入口文件 | | ├── apiCreateApp.js // 存放 createApp API | | ├── render.js // 渲染相關(guān)的 | | ├── component.js // 組件相關(guān)的 | | ├── template.js // 生成原生模板的 ├── src │ ├── App.js // 測試組件 │ └── main.js // 應(yīng)用入口文件 ├── index.html └── package.json
總結(jié)
本文從 Vue3 的渲染器與虛擬 DOM 機(jī)制出發(fā),對(duì)比分析了 Vue Vapor 在應(yīng)用初始化上的演進(jìn)思路。
Vue Vapor 摒棄了虛擬 DOM 和渲染器層,直接將組件編譯為原生 DOM 操作,因此 createApp 不再依賴 createRenderer,而是直接創(chuàng)建應(yīng)用實(shí)例并調(diào)用 render 完成掛載。通過引入組件實(shí)例對(duì)象(createComponentInstance)和掛載函數(shù)(mountComponent),Vue Vapor 在兼容 Vue3 組件 API(如 setup、render)的同時(shí),大幅簡化了運(yùn)行時(shí)代碼結(jié)構(gòu)。去除了 diff 算法和 patch 流程后,整體架構(gòu)更加輕盈,性能開銷顯著降低。代碼組織上,將運(yùn)行時(shí)拆分為 apiCreateApp、render、component、template 等獨(dú)立模塊,職責(zé)清晰,便于后續(xù)擴(kuò)展和維護(hù)。
這種設(shè)計(jì)既保證了 Vue3 項(xiàng)目未來可平滑升級(jí),也為無虛擬 DOM 的高性能渲染提供了堅(jiān)實(shí)基礎(chǔ),是 Vue 技術(shù)棧面向編譯時(shí)優(yōu)化的重要探索方向。
到此這篇關(guān)于詳解Vue Vapor 的應(yīng)用初始化的文章就介紹到這了,更多相關(guān)Vue Vapor應(yīng)用初始化內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Vue3視頻播放組件 vue3-video-play使用方式
vue3-video-play是Vue3的視頻播放組件,基于原生video標(biāo)簽開發(fā),支持MP4和HLS流,提供全局/局部引入方式,可監(jiān)聽播放、暫停等事件,并具備字幕、播放列表等高級(jí)功能,適合靈活配置需求2025-09-09
詳解windows下vue-cli及webpack 構(gòu)建網(wǎng)站(二)導(dǎo)入bootstrap樣式
這篇文章主要介紹了詳解windows下vue-cli及webpack 構(gòu)建網(wǎng)站(二)導(dǎo)入bootstrap樣式,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-06-06
vue項(xiàng)目中swiper輪播active圖片實(shí)現(xiàn)居中并放大
這篇文章主要介紹了vue項(xiàng)目中swiper輪播active圖片實(shí)現(xiàn)居中并放大方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-05-05
Vue2響應(yīng)式系統(tǒng)之set和delete
這篇文章主要介紹了Vue2響應(yīng)式系統(tǒng)之set和delete,通過為對(duì)象收集依賴,將對(duì)象、數(shù)組的修改、刪除也變成響應(yīng)式的了,同時(shí)為用戶提供了和方法,下文詳細(xì)介紹需要的朋友可以參考一下2022-04-04
Vue3.0之引入Element-plus ui樣式的兩種方法
本文主要介紹了Vue3.0之引入Element-plus ui樣式的兩種方法,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02
postcss-pxtorem設(shè)置不轉(zhuǎn)換UI框架的CSS單位問題
這篇文章主要介紹了postcss-pxtorem設(shè)置不轉(zhuǎn)換UI框架的CSS單位問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07

