溫馨提示×

溫馨提示×

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

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

Vue.js組件實現渲染DOM的原理解析

發布時間:2020-11-11 14:47:00 來源:億速云 閱讀:376 作者:Leah 欄目:開發技術

這篇文章給大家介紹Vue.js組件實現渲染DOM的原理解析,內容非常詳細,感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

本文主要是講述 Vue.js 3.0 中一個組件是如何轉變為頁面中真實 DOM 節點的。對于任何一個基于 Vue.js 的應用來說,一切的故事都要從應用初始化「根組件(通常會命名為 APP)掛載到 HTML 頁面 DOM 節點(根組件容器)上」說起。所以,我們可以從應用的根組件為切入點。

主線思路:聚焦于一個組件是如何轉變為 DOM 的。

輔助思路:

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

應用初始化

在 Vue.js 3.0 中,初始化一個應用的方式和 Vue.js 2.x 有差別但是差別不大(本質上都是把 App 組件掛載到 id 為 app 的 DOM 節點上),在 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
// 創建應用
const createApp = ((...args) => {
 // 1. 創建 app 對象
 const app = ensureRenderer().createApp(...args)
 
 const { mount } = app
 // 2. 重寫 mount 方法
 app.mount = (containerOrSelector) => {
  // ...
 }
 
 return app
})

createApp 方法中主要做了兩件事:

  • 創建 app 對象
  • 重寫 app.mount 方法

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

創建 app 對象

從 ensureRenderer() 著手。在 Vue.js 3.0 中有一個「渲染器」的概念,我們先對渲染器有一個初步的印象:**渲染器可以用于跨平臺渲染,是一個包含了平臺渲染核心邏輯的 JavaScript 對象。**接下來,我們通過簡化版源碼來驗證這個結論:

// packages/runtime-dom/src/index.ts
// 定義渲染器變量
let renderer

// 創建一個渲染器對象
// 惰性創建渲染器(當用戶只依賴響應式包的時候可以通過 tree-shaking 的方式移除核心渲染邏輯相關的代碼)
function ensureRenderer() {
 return renderer || (renderer = createRenderer(rendererOptions))
}

// packages/runtime-core/src/renderer.ts
export function createRenderer(options) {
 return baseCreateRenderer(options)
}

// 創建不同平臺渲染器的函數,在其內部都會調用 baseCreateRenderer
function baseCreateRenderer(options, createHydrationFns) {
 // 一系列內部函數
 const render = (vnode, container) => {
  // 組件渲染的核心邏輯
 }
 
 // 返回渲染器對象
 return {
  render,
  hydrate,
  createApp: createAppAPI(render, hydrate)
 }
}

可以看出渲染器最終由 baseCreateRenderer 函數生成,是一個包含 render 和createApp 函數的 JS 對象。其中 createApp 函數是由 createAppAPI 函數返回的。那 createApp 接收的參數有哪些呢?為了尋求答案,我們需要看一下 createAppAPI  做了什么事情。

// packages/runtime-core/src/apiCreateApp.ts
// 接收一個渲染器 render 作為參數,接收一個可選參數 hydrate,返回一個用于創建 app 的函數
export function createAppAPI(render, hydrate) {
 // createApp 接收兩個參數:根組件對象和根組件的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 {
    // 創建根組件的 vnode
    const vnode = createVNode(rootComponent, rootProps)
    // 利用函數參數傳入的渲染器渲染 vnode
    render(vnode, rootContainer)
    app._container = rootContainer
    return vnode.component.proxy
   },
   unmount() {},
   provide(key, value) {}
  }
 return app
 }
}

渲染器對象的 createApp 方法接收兩個參數:根組件對象和根組件的prop。這和應用初始化 demo 中 createApp(App) 的使用方式是吻合的。還可以看到的是:createApp 返回的 app 對象在最初定義時包含了 _uid 、 use 、 mixin 、 component 、mount 等屬性。

此時,我們可以得出結論:在應用層調用的 createApp 方法內部,首先會生成一個渲染器,然后調用渲染器的 createApp 方法創建 app 對象。app 對象中具有一系列我們在日常開發應用時已經很熟悉的屬性。

在應用層調用的 createApp 方法內部創建好 app 對象后,接下來便是對 app.mount 方法重寫。

重寫 app.mount 方法

先看一下簡化版的 app.mount  源碼:

// packages/runtime-dom/src/index.ts
const { mount } = app
app.mount = (containerOrSelector): any => {
 // 1. 標準化容器(將傳入的 DOM 對象或者節點選擇器統一為 DOM 對象)
 const container = normalizeContainer(containerOrSelector)
 if (!container) return
 
 const component = app._component
 // 2. 標準化組件(如果根組件不是函數,并且沒有 render 函數和 template 模板,則把根組件 innerHTML 作為 template)
 if (!isFunction(component) && !component.render && !component.template) {
  component.template = container.innerHTML
 }

 // 3. 掛載前清空容器的內容
 container.innerHTML = ''
 
 // 4. 執行渲染器創建 app 對象時定義的 mount 方法(在后文中稱之為「標準 mount 函數」)來渲染根組件
 const proxy = mount(container)
 
 return proxy
}

瀏覽器平臺 app.mount 方法重寫主要做了 4 件事情:

  1. 標準化容器
     
  2. 標準化組件
     
  3. 掛載前清空容器的內容
  4. 執行標準 mount 函數渲染組件

此時可能會有人思考一個問題:為什么要重寫app.mount 呢?答案是因為 Vue.js 需要支持跨平臺渲染。
支持跨平臺渲染的思路:不同的平臺具有不同的渲染器,不同的渲染器中會調用標準的 baseCreateRenderer 來保證核心(標準)的渲染流程是一致的。

以瀏覽器端和服務端渲染的代碼實現為例:

createApp 流程圖

在分別了解了 創建 app 對象和重寫 app.mount 過程后,我們來以整體的視角看一下 createApp 函數的實現:

目前為止,只是對應用的初始化有了一個初步的印象,但是還沒有涉及到具體的組件渲染過程??梢钥吹礁M件的渲染是在標準 mount 函數中進行的。所以接下來需要去深入了解標準 mount 函數。

標準 mount 函數

簡化版源碼

// packages/runtime-core/src/apiCreateApp.ts
// createAppAPI 函數內部返回的 createApp 函數中定義了 app 對象,mount 函數是 app 對象的方法之一
mount(rootContainer, isHydrate) {
 // 1. 創建根組件的 vnode
 const vnode = createVNode(rootComponent, rootProps)
 // 2. 利用函數參數傳入的渲染器渲染 vnode
 render(vnode, rootContainer)
 
 app._container = rootContainer
 
 return vnode.component.proxy
},

createVNode 方法做了兩件事:

  1. 基于根組件「創建 vnode」
  2. 在根組件容器中「渲染 vnode」

vnode 大致可以理解為 Virtual DOM(虛擬 DOM)概念的一個具體實現,是用普通的 JS 對象來描述 DOM 對象。因為不是真實的 DOM 對象,所以叫做 Virtual DOM。

我們來一起看一下創建 vnode 和渲染 vnode 的具體過程。

創建 vnode:createVNode(rootComponent, rootProps)

簡化版源碼(已經把分支邏輯拿掉)

// packages/runtime-core/src/vnode.ts
function _createVNode(type, props, children, 
            patchFlag, dynamicProps, isBlockNode = false) {
 // 1. 對 VNodeTypes 或 ClassComponent 類型的 type 進行各種標準化處理:規范化 vnode、規范化 component、規范化 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. 創建 vnode 對象
 const vnode = {
  __v_isVNode: true,
  [ReactiveFlags.SKIP]: true,
  type, // 把函數入參 type 賦值給 vnode 
  props,
  children: null,
  component: null,
  staticCount: 0,
  shapeFlag, // 把 vnode 類型信息賦值給 vnode
  // 還有很多屬性
 }

 // 4. 標準化子節點 children
 normalizeChildren(vnode, children)

 return vnode
}

createVNode 做了 4 件事

  1. 對 VNodeTypes 或 ClassComponent 類型的 type 進行各種標準化處理
  2. 將 vnode 類型信息編碼為位圖
  3. 創建 vnode 對象
  4. 標準化子節點 children

細心的同學會發現:在標準 mount 函數中執行 createVNode(rootComponent, rootProps) 時,參數是根組件 rootComponent 和根組件屬性 rootProps,但是在 _createVNode 在定義時函數簽名的前兩個參數確實 type 和 props。rootComponent 與 type 的關系是什么呢?函數名為什么差了一個 _ 呢?

首先函數名的差異,是由于在定義函數時,基于代碼運行環境做了一個判斷:

export const createVNode = (__DEV__
 ? createVNodeWithArgsTransform
 : _createVNode) as typeof _createVNode

其次,rootComponent 與 type 的關系我們可以從 type 的類型定義中得到答案:

function _createVNode(
 type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
 props: (Data & VNodeProps) | null = null
): VNode { }

當 createVNode把這 4 件事情做好后,會返回已經創建好 vnode,接下來做的事情是渲染 vnode。

渲染 vnode:render(vnode, rootContainer)

即使不看具體源碼實現,我們其實大致可以用一句話總結出渲染 vnode 過程做了什么事情:把 vnode 轉化為真實 DOM。

前文我們提過,**渲染器是一個包含了平臺渲染核心邏輯的 JavaScript 對象。**渲染 vnode 正是通過調用渲染器的 render 方法做的。

// 返回渲染器對象
return {
 render,
 hydrate,
 createApp: createAppAPI(render, hydrate)
}

我們來看一下 render 函數的定義(簡化版源碼):**

// 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 {
  // 創建或更新組件
  patch(container._vnode || null, vnode, container)
 }

 // packages/runtime-core/src/scheduler.ts
 flushPostFlushCbs()
 
 // 緩存 vnode 節點(標識該 vnode 已經完成渲染)
 container._vnode = vnode
}

抽象來看, render 做的事情是:如果傳入的 vnode 為空,則銷毀組件,否則就創建或者更新組件。其中有兩個關鍵函數:patch 和 unmount(patch、unmount 和 render 都是在baseCreateRenderer函數內部的方法)。

可以從 patch 著手,看一下是如何將 vnode 轉化為 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. 處理不同類型節點的渲染
 const { type, ref, shapeFlag } = n2
 switch (type) {
  case Text:
   // 處理文本節點
   processText(n1, n2, container, anchor)
   break
  case Comment:
   // 處理注釋節點
   break
  case Static:
   // 處理靜態節點
   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 函數做了 2 件事情:

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

在 patch 函數的多個參數中,我們優先關注前 3 個參數:

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

以新建文本 DOM 節點為例,此時 n1 為 null,n2 類型為 Text,所以會走分支邏輯:processText(n1, n2, container, anchor)。processText 內部會去調用 hostCreateText 和 hostSetText。

hostCreateText 和 hostSetText 是從 baseCreateRenderer 函數入參 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
// 在調用 baseCreateRenderer 時,傳入了渲染參數
function baseCreateRenderer(options: RendererOptions) { }

還記得前文提到的我們在哪里調用了 baseCreateRenderer 嗎?

// packages/runtime-dom/src/index.ts
// 創建應用
const createApp = ((...args) => {
 // 1. 創建 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)
}

可以看到在創建渲染器時,我們調用了 baseCreateRenderer 并傳入了 rendererOptions。rendererOptions 的值為extend({ patchProp, forcePatchProp }, nodeOps)。

我們如果知道了 nodeOps  中的 createText、setText 等方法做了什么事情,就清楚了某一個確定類型的 vnode 是如何轉變為 DOM 的。先看一下 nodeOps 的定義:

// packages/runtime-dom/src/nodeOps.ts
export const nodeOps = {
 createText: text => doc.createTextNode(text),
 setText: (node, text) => {},
 // 其他方法
}

此時已經非常接近問題的答案了,關鍵是看一下 doc 變量是什么:

const doc = (typeof document !== 'undefined' &#63; document : null) as Document

Vue.js組件實現渲染DOM的原理解析

至此,我們知道了答案:先把組件轉化為 vnode,針對特定類型的 vnode 執行不同的渲染邏輯,最終調用 document 上的方法將 vnode 渲染成 DOM。**抽象一下,從組件到渲染生成 DOM 需要經歷 3 個過程:創建 vnode - 渲染 vnode - 生成 DOM。

在渲染 vnode 部分,我們以一個簡單的 Text 類型的 vnode 為例來找到了答案。其實在 baseCreateRenderer 中有 30+ 個函數來處理不同類型的 vnode 的渲染。 比如:用來處理組件類型的 processComponent 函數、用來處理普通 DOM 元素類型的processElement 函數等。由于 vnode 是一個樹形數據結構,在處理過程中還應用到了遞歸思想。建議感興趣的同學自行查看。

總結

最后,我們來做個總結:

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

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

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

亚洲午夜精品一区二区_中文无码日韩欧免_久久香蕉精品视频_欧美主播一区二区三区美女