您好,登錄后才能下訂單哦!
這篇文章給大家介紹Vue.js組件實(shí)現(xiàn)渲染DOM的原理解析,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。
本文主要是講述 Vue.js 3.0 中一個組件是如何轉(zhuǎn)變?yōu)轫撁嬷姓鎸?shí) DOM 節(jié)點(diǎn)的。對于任何一個基于 Vue.js 的應(yīng)用來說,一切的故事都要從應(yīng)用初始化「根組件(通常會命名為 APP)掛載到 HTML 頁面 DOM 節(jié)點(diǎn)(根組件容器)上」說起。所以,我們可以從應(yīng)用的根組件為切入點(diǎn)。
主線思路:聚焦于一個組件是如何轉(zhuǎn)變?yōu)?DOM 的。
輔助思路:
應(yīng)用初始化
在 Vue.js 3.0 中,初始化一個應(yīng)用的方式和 Vue.js 2.x 有差別但是差別不大(本質(zhì)上都是把 App 組件掛載到 id 為 app 的 DOM 節(jié)點(diǎn)上),在 Vue.js 3.0 中用法如下:
import { createApp } from 'vue' import App from './app' const app = createApp(App) app.mount('#app')
createApp 簡化版源碼
// packages/runtime-dom/src/index.ts // 創(chuàng)建應(yīng)用 const createApp = ((...args) => { // 1. 創(chuàng)建 app 對象 const app = ensureRenderer().createApp(...args) const { mount } = app // 2. 重寫 mount 方法 app.mount = (containerOrSelector) => { // ... } return app })
createApp 方法中主要做了兩件事:
接下來會分別看一下這兩個過程都做了什么事情。
創(chuàng)建 app 對象
從 ensureRenderer() 著手。在 Vue.js 3.0 中有一個「渲染器」的概念,我們先對渲染器有一個初步的印象:**渲染器可以用于跨平臺渲染,是一個包含了平臺渲染核心邏輯的 JavaScript 對象。**接下來,我們通過簡化版源碼來驗(yàn)證這個結(jié)論:
// packages/runtime-dom/src/index.ts // 定義渲染器變量 let renderer // 創(chuàng)建一個渲染器對象 // 惰性創(chuàng)建渲染器(當(dāng)用戶只依賴響應(yīng)式包的時候可以通過 tree-shaking 的方式移除核心渲染邏輯相關(guān)的代碼) function ensureRenderer() { return renderer || (renderer = createRenderer(rendererOptions)) } // packages/runtime-core/src/renderer.ts export function createRenderer(options) { return baseCreateRenderer(options) } // 創(chuàng)建不同平臺渲染器的函數(shù),在其內(nèi)部都會調(diào)用 baseCreateRenderer function baseCreateRenderer(options, createHydrationFns) { // 一系列內(nèi)部函數(shù) const render = (vnode, container) => { // 組件渲染的核心邏輯 } // 返回渲染器對象 return { render, hydrate, createApp: createAppAPI(render, hydrate) } }
可以看出渲染器最終由 baseCreateRenderer 函數(shù)生成,是一個包含 render 和createApp 函數(shù)的 JS 對象。其中 createApp 函數(shù)是由 createAppAPI 函數(shù)返回的。那 createApp 接收的參數(shù)有哪些呢?為了尋求答案,我們需要看一下 createAppAPI 做了什么事情。
// packages/runtime-core/src/apiCreateApp.ts // 接收一個渲染器 render 作為參數(shù),接收一個可選參數(shù) hydrate,返回一個用于創(chuàng)建 app 的函數(shù) export function createAppAPI(render, hydrate) { // createApp 接收兩個參數(shù):根組件對象和根組件的prop return function createApp(rootComponent, rootProps = null) { const context = createAppContext() const app: App = (context.app = { _uid: uid++, _component: rootComponent, _props: rootProps, _container: null, _context: context, version, get config() {}, set config(v) {}, use(plugin: Plugin, ...options: any[]) {}, mixin(mixin: ComponentOptions) {}, component(name: string, component?: Component): any {}, directive(name: string, directive?: Directive) {}, mount(rootContainer: HostElement, isHydrate?: boolean): any { // 創(chuàng)建根組件的 vnode const vnode = createVNode(rootComponent, rootProps) // 利用函數(shù)參數(shù)傳入的渲染器渲染 vnode render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy }, unmount() {}, provide(key, value) {} } return app } }
渲染器對象的 createApp 方法接收兩個參數(shù):根組件對象和根組件的prop。這和應(yīng)用初始化 demo 中 createApp(App) 的使用方式是吻合的。還可以看到的是:createApp 返回的 app 對象在最初定義時包含了 _uid 、 use 、 mixin 、 component 、mount 等屬性。
此時,我們可以得出結(jié)論:在應(yīng)用層調(diào)用的 createApp 方法內(nèi)部,首先會生成一個渲染器,然后調(diào)用渲染器的 createApp 方法創(chuàng)建 app 對象。app 對象中具有一系列我們在日常開發(fā)應(yīng)用時已經(jīng)很熟悉的屬性。
在應(yīng)用層調(diào)用的 createApp 方法內(nèi)部創(chuàng)建好 app 對象后,接下來便是對 app.mount 方法重寫。
重寫 app.mount 方法
先看一下簡化版的 app.mount 源碼:
// packages/runtime-dom/src/index.ts const { mount } = app app.mount = (containerOrSelector): any => { // 1. 標(biāo)準(zhǔn)化容器(將傳入的 DOM 對象或者節(jié)點(diǎn)選擇器統(tǒng)一為 DOM 對象) const container = normalizeContainer(containerOrSelector) if (!container) return const component = app._component // 2. 標(biāo)準(zhǔn)化組件(如果根組件不是函數(shù),并且沒有 render 函數(shù)和 template 模板,則把根組件 innerHTML 作為 template) if (!isFunction(component) && !component.render && !component.template) { component.template = container.innerHTML } // 3. 掛載前清空容器的內(nèi)容 container.innerHTML = '' // 4. 執(zhí)行渲染器創(chuàng)建 app 對象時定義的 mount 方法(在后文中稱之為「標(biāo)準(zhǔn) mount 函數(shù)」)來渲染根組件 const proxy = mount(container) return proxy }
瀏覽器平臺 app.mount 方法重寫主要做了 4 件事情:
此時可能會有人思考一個問題:為什么要重寫app.mount 呢?答案是因?yàn)?Vue.js 需要支持跨平臺渲染。
支持跨平臺渲染的思路:不同的平臺具有不同的渲染器,不同的渲染器中會調(diào)用標(biāo)準(zhǔn)的 baseCreateRenderer 來保證核心(標(biāo)準(zhǔn))的渲染流程是一致的。
以瀏覽器端和服務(wù)端渲染的代碼實(shí)現(xiàn)為例:
createApp 流程圖
在分別了解了 創(chuàng)建 app 對象和重寫 app.mount 過程后,我們來以整體的視角看一下 createApp 函數(shù)的實(shí)現(xiàn):
目前為止,只是對應(yīng)用的初始化有了一個初步的印象,但是還沒有涉及到具體的組件渲染過程??梢钥吹礁M件的渲染是在標(biāo)準(zhǔn) mount 函數(shù)中進(jìn)行的。所以接下來需要去深入了解標(biāo)準(zhǔn) mount 函數(shù)。
標(biāo)準(zhǔn) mount 函數(shù)
簡化版源碼
// packages/runtime-core/src/apiCreateApp.ts // createAppAPI 函數(shù)內(nèi)部返回的 createApp 函數(shù)中定義了 app 對象,mount 函數(shù)是 app 對象的方法之一 mount(rootContainer, isHydrate) { // 1. 創(chuàng)建根組件的 vnode const vnode = createVNode(rootComponent, rootProps) // 2. 利用函數(shù)參數(shù)傳入的渲染器渲染 vnode render(vnode, rootContainer) app._container = rootContainer return vnode.component.proxy },
createVNode 方法做了兩件事:
vnode 大致可以理解為 Virtual DOM(虛擬 DOM)概念的一個具體實(shí)現(xiàn),是用普通的 JS 對象來描述 DOM 對象。因?yàn)椴皇钦鎸?shí)的 DOM 對象,所以叫做 Virtual DOM。
我們來一起看一下創(chuàng)建 vnode 和渲染 vnode 的具體過程。
創(chuàng)建 vnode:createVNode(rootComponent, rootProps)
簡化版源碼(已經(jīng)把分支邏輯拿掉)
// packages/runtime-core/src/vnode.ts function _createVNode(type, props, children, patchFlag, dynamicProps, isBlockNode = false) { // 1. 對 VNodeTypes 或 ClassComponent 類型的 type 進(jìn)行各種標(biāo)準(zhǔn)化處理:規(guī)范化 vnode、規(guī)范化 component、規(guī)范化 CSS 類和樣式 // 2. 將 vnode 類型信息編碼為位圖 const shapeFlag = isString(type) ? ShapeFlags.ELEMENT : __FEATURE_SUSPENSE__ && isSuspense(type) ? ShapeFlags.SUSPENSE : isTeleport(type) ? ShapeFlags.TELEPORT : isObject(type) ? ShapeFlags.STATEFUL_COMPONENT : isFunction(type) ? ShapeFlags.FUNCTIONAL_COMPONENT : 0 // 3. 創(chuàng)建 vnode 對象 const vnode = { __v_isVNode: true, [ReactiveFlags.SKIP]: true, type, // 把函數(shù)入?yún)?type 賦值給 vnode props, children: null, component: null, staticCount: 0, shapeFlag, // 把 vnode 類型信息賦值給 vnode // 還有很多屬性 } // 4. 標(biāo)準(zhǔn)化子節(jié)點(diǎn) children normalizeChildren(vnode, children) return vnode }
createVNode 做了 4 件事
細(xì)心的同學(xué)會發(fā)現(xiàn):在標(biāo)準(zhǔn) mount 函數(shù)中執(zhí)行 createVNode(rootComponent, rootProps) 時,參數(shù)是根組件 rootComponent 和根組件屬性 rootProps,但是在 _createVNode 在定義時函數(shù)簽名的前兩個參數(shù)確實(shí) type 和 props。rootComponent 與 type 的關(guān)系是什么呢?函數(shù)名為什么差了一個 _ 呢?
首先函數(shù)名的差異,是由于在定義函數(shù)時,基于代碼運(yùn)行環(huán)境做了一個判斷:
export const createVNode = (__DEV__ ? createVNodeWithArgsTransform : _createVNode) as typeof _createVNode
其次,rootComponent 與 type 的關(guān)系我們可以從 type 的類型定義中得到答案:
function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null ): VNode { }
當(dāng) createVNode把這 4 件事情做好后,會返回已經(jīng)創(chuàng)建好 vnode,接下來做的事情是渲染 vnode。
渲染 vnode:render(vnode, rootContainer)
即使不看具體源碼實(shí)現(xiàn),我們其實(shí)大致可以用一句話總結(jié)出渲染 vnode 過程做了什么事情:把 vnode 轉(zhuǎn)化為真實(shí) DOM。
前文我們提過,**渲染器是一個包含了平臺渲染核心邏輯的 JavaScript 對象。**渲染 vnode 正是通過調(diào)用渲染器的 render 方法做的。
// 返回渲染器對象 return { render, hydrate, createApp: createAppAPI(render, hydrate) }
我們來看一下 render 函數(shù)的定義(簡化版源碼):**
// packages/runtime-core/src/renderer.ts const render = (vnode, container) => { if (vnode == null) { // 如果 vnode 為 null,但是容器中有 vnode,則銷毀組件 if (container._vnode) { unmount(container._vnode, null, null, true) } } else { // 創(chuàng)建或更新組件 patch(container._vnode || null, vnode, container) } // packages/runtime-core/src/scheduler.ts flushPostFlushCbs() // 緩存 vnode 節(jié)點(diǎn)(標(biāo)識該 vnode 已經(jīng)完成渲染) container._vnode = vnode }
抽象來看, render 做的事情是:如果傳入的 vnode 為空,則銷毀組件,否則就創(chuàng)建或者更新組件。其中有兩個關(guān)鍵函數(shù):patch 和 unmount(patch、unmount 和 render 都是在baseCreateRenderer函數(shù)內(nèi)部的方法)。
可以從 patch 著手,看一下是如何將 vnode 轉(zhuǎn)化為 DOM 的。
patch
// packages/runtime-core/src/renderer.ts const patch = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false ) => { // 1. 如果是更新 vnode 并且新舊 vnode 類型不一致,則銷毀舊的 vnode if (n1 && !isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1) unmount(n1, parentComponent, parentSuspense, true) n1 = null } // 2. 處理不同類型節(jié)點(diǎn)的渲染 const { type, ref, shapeFlag } = n2 switch (type) { case Text: // 處理文本節(jié)點(diǎn) processText(n1, n2, container, anchor) break case Comment: // 處理注釋節(jié)點(diǎn) break case Static: // 處理靜態(tài)節(jié)點(diǎn) break case Fragment: // 處理 Fragment 元素(https://v3.vuejs.org/guide/migration/fragments.html#fragments) break default: if (shapeFlag & ShapeFlags.ELEMENT) { // 處理普通 DOM 元素 } else if (shapeFlag & ShapeFlags.COMPONENT) { // 處理組件 } else if (shapeFlag & ShapeFlags.TELEPORT) { // 處理 TELEPORT } else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) { // 處理 SUSPENSE } else if (__DEV__) { warn('Invalid VNode type:', type, `(${typeof type})`) } } }
patch 函數(shù)做了 2 件事情:
在 patch 函數(shù)的多個參數(shù)中,我們優(yōu)先關(guān)注前 3 個參數(shù):
以新建文本 DOM 節(jié)點(diǎn)為例,此時 n1 為 null,n2 類型為 Text,所以會走分支邏輯:processText(n1, n2, container, anchor)。processText 內(nèi)部會去調(diào)用 hostCreateText 和 hostSetText。
hostCreateText 和 hostSetText 是從 baseCreateRenderer 函數(shù)入?yún)?options 中解析出來的方法:
// packages/runtime-core/src/renderer.ts const { insert: hostInsert, remove: hostRemove, patchProp: hostPatchProp, forcePatchProp: hostForcePatchProp, createElement: hostCreateElement, createText: hostCreateText, createComment: hostCreateComment, setText: hostSetText, setElementText: hostSetElementText, parentNode: hostParentNode, nextSibling: hostNextSibling, setScopeId: hostSetScopeId = NOOP, cloneNode: hostCloneNode, insertStaticContent: hostInsertStaticContent } = options
來看看 options 是怎么來的:
// packages/runtime-core/src/renderer.ts // 在調(diào)用 baseCreateRenderer 時,傳入了渲染參數(shù) function baseCreateRenderer(options: RendererOptions) { }
還記得前文提到的我們在哪里調(diào)用了 baseCreateRenderer 嗎?
// packages/runtime-dom/src/index.ts // 創(chuàng)建應(yīng)用 const createApp = ((...args) => { // 1. 創(chuàng)建 app 對象 const app = ensureRenderer().createApp(...args) return app }) // packages/runtime-dom/src/index.ts const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps) function ensureRenderer() { return renderer || (renderer = createRenderer<Node, Element>(rendererOptions)) } // packages/runtime-core/src/renderer.ts export function createRenderer< HostNode = RendererNode, HostElement = RendererElement >(options: RendererOptions<HostNode, HostElement>) { return baseCreateRenderer<HostNode, HostElement>(options) }
可以看到在創(chuàng)建渲染器時,我們調(diào)用了 baseCreateRenderer 并傳入了 rendererOptions。rendererOptions 的值為extend({ patchProp, forcePatchProp }, nodeOps)。
我們?nèi)绻懒?nodeOps 中的 createText、setText 等方法做了什么事情,就清楚了某一個確定類型的 vnode 是如何轉(zhuǎn)變?yōu)?DOM 的。先看一下 nodeOps 的定義:
// packages/runtime-dom/src/nodeOps.ts export const nodeOps = { createText: text => doc.createTextNode(text), setText: (node, text) => {}, // 其他方法 }
此時已經(jīng)非常接近問題的答案了,關(guān)鍵是看一下 doc 變量是什么:
const doc = (typeof document !== 'undefined' ? document : null) as Document
至此,我們知道了答案:先把組件轉(zhuǎn)化為 vnode,針對特定類型的 vnode 執(zhí)行不同的渲染邏輯,最終調(diào)用 document 上的方法將 vnode 渲染成 DOM。**抽象一下,從組件到渲染生成 DOM 需要經(jīng)歷 3 個過程:創(chuàng)建 vnode - 渲染 vnode - 生成 DOM。
在渲染 vnode 部分,我們以一個簡單的 Text 類型的 vnode 為例來找到了答案。其實(shí)在 baseCreateRenderer 中有 30+ 個函數(shù)來處理不同類型的 vnode 的渲染。 比如:用來處理組件類型的 processComponent 函數(shù)、用來處理普通 DOM 元素類型的processElement 函數(shù)等。由于 vnode 是一個樹形數(shù)據(jù)結(jié)構(gòu),在處理過程中還應(yīng)用到了遞歸思想。建議感興趣的同學(xué)自行查看。
總結(jié)
最后,我們來做個總結(jié):
關(guān)于Vue.js組件實(shí)現(xiàn)渲染DOM的原理解析就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。