溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Vue.js組件實(shí)現(xiàn)渲染DOM的原理解析

發(fā)布時間:2020-11-11 14:47:00 來源:億速云 閱讀:336 作者:Leah 欄目:開發(fā)技術(shù)

這篇文章給大家介紹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 的。

輔助思路:

  • 涉及到源代碼的地方,需要明確標(biāo)記源碼所在文件,同時將 TS 簡化為 JS 以便于直觀理解
  • 思路每前進(jìn)一步要能夠得出結(jié)論
  • 盡量總結(jié)歸納出流程圖

應(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 對象
  • 重寫 app.mount 方法

接下來會分別看一下這兩個過程都做了什么事情。

創(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 件事情:

  1. 標(biāo)準(zhǔn)化容器
     
  2. 標(biāo)準(zhǔn)化組件
     
  3. 掛載前清空容器的內(nèi)容
  4. 執(zhí)行標(biāo)準(zhǔn) mount 函數(shù)渲染組件

此時可能會有人思考一個問題:為什么要重寫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 方法做了兩件事:

  1. 基于根組件「創(chuàng)建 vnode」
  2. 在根組件容器中「渲染 vnode」

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 件事

  1. 對 VNodeTypes 或 ClassComponent 類型的 type 進(jìn)行各種標(biāo)準(zhǔn)化處理
  2. 將 vnode 類型信息編碼為位圖
  3. 創(chuàng)建 vnode 對象
  4. 標(biāo)準(zhǔn)化子節(jié)點(diǎn) children

細(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 件事情:

  1.  如果是更新 vnode 并且新舊 vnode 類型不一致,則銷毀舊的 vnode
  2. 處理不同類型節(jié)點(diǎn)的渲染

在 patch 函數(shù)的多個參數(shù)中,我們優(yōu)先關(guān)注前 3 個參數(shù):

  1. n1 表示舊的 vnode,當(dāng) n1 為 null 的時候,表示是一次新建(掛載)的過程
  2. n2 表示新的 vnode 節(jié)點(diǎn),后續(xù)會根據(jù)這個 vnode 類型執(zhí)行不同的處理邏輯
  3. container 表示 DOM 容器,也就是 vnode 渲染生成 DOM 后,會掛載到 container 下面

以新建文本 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' &#63; document : null) as Document

Vue.js組件實(shí)現(xiàn)渲染DOM的原理解析

至此,我們知道了答案:先把組件轉(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é):

  • 在 Vue.js 中, vnode 是對抽象事物的描述。
  • 從組件到渲染生成 DOM 需要經(jīng)歷 3 個過程:創(chuàng)建 vnode - 渲染 vnode - 生成 DOM。
  • 組件是如何轉(zhuǎn)變?yōu)?DOM 的:先把組件轉(zhuǎn)化為 vnode,針對特定類型的 vnode 執(zhí)行不同的渲染邏輯,最終調(diào)用 document 上的方法將 vnode 渲染成 DOM。
  • 渲染器是一個包含了平臺渲染核心邏輯的 JavaScript 對象,可以用于跨平臺渲染。
  • 渲染器對象中的 createApp 方法,創(chuàng)建了一個具有 mount 方法的 app 實(shí)例。app.mount 方法中先是用根組件創(chuàng)建了 vnode,然后調(diào)用渲染器對象中的 render 方法去渲染 vnode,最終通過 DOM API 將 vnode 轉(zhuǎn)化為 DOM。

關(guān)于Vue.js組件實(shí)現(xiàn)渲染DOM的原理解析就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(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)容。

AI