從 8-7K 的 VDOM 開源項目我學到了什麼

近幾年隨着 React、Vue 等前端框架不斷興起,Virtual DOM 概念也越來越火,被用到越來越多的框架、庫中。Virtual DOM 是基於真實 DOM 的一層抽象,用簡單的 JS 對象描述真實 DOM。本文要介紹的 Snabbdom[1] 就是 Virtual DOM 的一種簡單實現,並且 Vue 的 Virtual DOM 也參考了 Snabbdom 實現方式。

對於想要深入學習 Vue Virtual DOM 的朋友,建議先學習 Snabbdom,對理解 Vue 會很有幫助,其核心代碼只有 200 多行。

本文挑選 Snabbdom 模塊系統作爲主要核心點介紹,其他內容可以查閱官方文檔《Snabbdom》[2]。

一、Snabbdom 是什麼

Snabbdom 是一個專注於簡單性、模塊化、強大特性和性能的虛擬 DOM 庫。其中有幾個核心特性:

  1. 核心代碼 200 行,並且提供豐富的測試用例;

  2. 擁有強大模塊系統,並且支持模塊拓展和靈活組合;

  3. 在每個 VNode 和全局模塊上,都有豐富的鉤子,可以在 Diff 和 Patch 階段使用。

接下來從一個簡單示例來體驗一下 Snabbdom。

1. 快速上手

安裝 Snabbdom:

1npm install snabbdom -D
2
3

接着新建 index.html,設置入口元素:

1<div id="app"></div>
2
3

然後新建 demo1.js 文件,並使用 Snabbdom 提供的函數:

 1// demo1.js
 2import { h } from 'snabbdom/src/package/h'
 3import { init } from 'snabbdom/src/package/init'
 4
 5const patch = init([])
 6let vnode = h('div#app', 'Hello Leo')
 7const app = document.getElementById('app')
 8patch(app, vnode)
 9
10

這樣就實現一個簡單示例,在瀏覽器打開 index.html,頁面將顯示 “Hello Leo” 文本。

接下來,我會以 snabbdom-demo[3] 項目作爲學習示例,從簡單示例到模塊系統使用的示例,深入學習和分析 Snabbdom 源碼,重點分析 Snabbdom 模塊系統。

二、Snabbdom-demo 分析

Snabbdom-demo[4] 項目中的三個演示代碼,爲我們展示如何從簡單到深入 Snabbdom。首先克隆倉庫並安裝:

1$ git clone https://github.com/zyycode/snabbdom-demo.git
2$ npm install
3
4

雖然本項目沒有 README.md 文件,但項目目錄比較直觀,我們可以輕鬆的從 src 目錄找到這三個示例代碼的文件:

接着在 index.html 中引入想要學習的代碼文件,默認 <script src="./src/01-basicusage.js"></script>  ,通過 package.json 可知啓動命令並啓動項目:

1$ npm run dev
2
3

1. 簡單示例分析

當我們要研究一個庫或框架等比較複雜的項目,可以通過官方提供的簡單示例代碼進行分析,我們這裏選擇該項目中最簡單的 01-basicusage.js 代碼進行分析,其代碼如下:

 1// src/01-basicusage.js
 2
 3import { h } from 'snabbdom/src/package/h'
 4import { init } from 'snabbdom/src/package/init'
 5
 6const patch = init([])
 7
 8let vnode = h('div#container.cls', 'Hello World')
 9const app = document.getElementById('app') // 入口元素
10
11const oldVNode = patch(app, vnode)
12
13// 假設時刻
14vnode = h('div', 'Hello Snabbdom')
15patch(oldVNode, vnode)
16
17

運行項目以後,可以看到頁面展示了 “Hello Snabbdom” 文本,這裏你會覺得奇怪,前面的 “Hello World” 文本去哪了

原因很簡單,我們把 demo 中的下面兩行代碼註釋後,頁面便顯示文本是 “Hello World”:

1vnode = h('div', 'Hello Snabbdom')
2patch(oldVNode, vnode)
3
4

這裏我們可以猜測 patch() 函數可以將 VNode 渲染到頁面。更進一步可以理解爲,這邊第一個執行 patch() 函數爲首次渲染,第二次執行 patch() 函數爲更新操作

2. VNode 介紹

這裏可能會有小夥伴疑惑,示例中的 VNode 是什麼?這裏簡單解釋下:

VNode,該對象用於描述節點的信息,它的全稱是虛擬節點(virtual node)。與 “虛擬節點” 相關聯的另一個概念是 “虛擬 DOM”,它是我們對由 Vue 組件樹建立起來的整個 VNode 樹的稱呼。“虛擬 DOM” 由 VNode 組成的。—— 全棧修仙之路 《Vue 3.0 進階之 VNode 探祕》

其實 VNode 就是一個 JS 對象,在 Snabbdom 中是這麼定義 VNode 的類型:

 1export interface VNode {
 2  sel: string | undefined; // selector的縮寫
 3  data: VNodeData | undefined; // 下面VNodeData接口的內容
 4  children: Array<VNode | string> | undefined; // 子節點
 5  elm: Node | undefined; // element的縮寫,存儲了真實的HTMLElement
 6  text: string | undefined; // 如果是文本節點,則存儲text
 7  key: Key | undefined; // 節點的key,在做列表時很有用
 8}
 9
10export interface VNodeData {
11  props?: Props
12  attrs?: Attrs
13  class?: Classes
14  style?: VNodeStyle
15  dataset?: Dataset
16  on?: On
17  hero?: Hero
18  attachData?: AttachData
19  hook?: Hooks
20  key?: Key
21  ns?: string // for SVGs
22  fn?: () => VNode // for thunks
23  args?: any[] // for thunks
24  [key: string]: any // for any other 3rd party module
25}
26
27

在 VNode 對象中含描述節點選擇器 sel 字段、節點數據 data 字段、節點所包含的子節點 children 字段等。

在這個 demo 中,我們似乎並沒有看到模塊系統相關的代碼,沒事,因爲這是最簡單的示例,下一節會詳細介紹。

我們在學習一個函數時,可以重點了解該函數的 “入參” 和“出參”,大致就能判斷該函數的作用。

從這個 demo 主要執行過程可以看出,主要用到有三個函數:init() / patch() / h() ,它們到底做什麼用的呢?我們分析一下 Snabbdom 源碼中這三個函數的入參和出參情況:

3. init() 函數分析

init() 函數被定義在 package/init.ts 文件中:

1// node_modules/snabbdom/src/package/init.ts
2
3export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
4 // 省略其他代碼
5}
6
7

其參數類型如下:

 1function init(modules: Array<Partial<Module>>, domApi?: DOMAPI): (oldVnode: VNode | Element, vnode: VNode) => VNode
 2
 3export type Module = Partial<{
 4  pre: PreHook
 5  create: CreateHook
 6  update: UpdateHook
 7  destroy: DestroyHook
 8  remove: RemoveHook
 9  post: PostHook
10}>
11  
12export interface DOMAPI {
13  createElement: (tagName: any) => HTMLElement
14  createElementNS: (namespaceURI: string, qualifiedName: string) => Element
15  createTextNode: (text: string) => Text
16  createComment: (text: string) => Comment
17  insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void
18  removeChild: (node: Node, child: Node) => void
19  appendChild: (node: Node, child: Node) => void
20  parentNode: (node: Node) => Node | null
21  nextSibling: (node: Node) => Node | null
22  tagName: (elm: Element) => string
23  setTextContent: (node: Node, text: string | null) => void
24  getTextContent: (node: Node) => string | null
25  isElement: (node: Node) => node is Element
26  isText: (node: Node) => node is Text
27  isComment: (node: Node) => node is Comment
28}
29
30

init() 函數接收一個模塊數組 modules 和可選的 domApi 對象作爲參數,返回一個函數,即 patch() 函數。domApi 對象的接口包含了很多 DOM 操作的方法。這裏的 modules 參數本文將重點介紹。

4. patch() 函數分析

init() 函數返回了一個 patch() 函數,其類型爲:

1// node_modules/snabbdom/src/package/init.ts
2
3patch(oldVnode: VNode | Element, vnode: VNode) => VNode
4
5

patch() 函數接收兩個 VNode 對象作爲參數,並返回一個新 VNode。

5. h() 函數分析

h() 函數被定義在 package/h.ts 文件中:

 1// node_modules/snabbdom/src/package/h.ts
 2
 3export function h(sel: string): VNode
 4export function h(sel: string, data: VNodeData | null): VNode
 5export function h(sel: string, children: VNodeChildren): VNode
 6export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode
 7export function h (sel: any, b?: any, c?: any): VNode{
 8 // 省略其他代碼
 9}
10
11

h() 函數接收多種參數,其中必須有一個 sel 參數,作用是將節點內容掛載到該容器中,並返回一個新 VNode。

6. 小結

通過前面介紹,我們在回過頭看看這個 demo 的代碼,大致調用流程如下:

三、深入 Snabbdom 模塊系統

學習完前面這些基礎知識後,我們已經知道 Snabbdom 使用方式,並且知道其中三個核心方法入參出參情況和大致作用,接下來開始看本文核心 Snabbdom 模塊系統。

1. Modules 介紹

Snabbdom 模塊系統是 Snabbdom 提供的一套可拓展可靈活組合的模塊系統,用來爲 Snabbdom 提供操作 VNode 時的各種模塊支持,如我們組建需要處理 style 則引入對應的 styleModule,需要處理事件,則引入 eventListenersModule 既可,這樣就達到靈活組合,可以支持按需引入的效果。

Snabbdom 模塊系統的特點可以概括爲:支持按需引入、獨立管理、職責單一、方便組合複用、可維護性強。

當然 Snabbdom 模塊系統還有其他內置模塊:

2. Hooks 介紹

Hooks 也稱鉤子,是 DOM 節點生命週期的一種方法。Snabbdom 提供豐富的鉤子選擇。模塊既使用鉤子來擴展 Snabbdom,也在普通代碼中使用鉤子,用來在 DOM 節點生命週期中執行任意代碼。

這裏大致介紹一下所有的 Hooks:

模塊中可以使用這些鉤子:precreateupdatedestroyremovepost。單個元素可以使用這些鉤子:initcreateinsertprepatchupdatepostpatchdestroyremove

Snabbdom 是這麼定義鉤子的:

 1// snabbdom/src/package/hooks.ts
 2
 3export type PreHook = () => any
 4export type InitHook = (vNode: VNode) => any
 5export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any
 6export type InsertHook = (vNode: VNode) => any
 7export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any
 8export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any
 9export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any
10export type DestroyHook = (vNode: VNode) => any
11export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any
12export type PostHook = () => any
13
14export interface Hooks {
15  pre?: PreHook
16  init?: InitHook
17  create?: CreateHook
18  insert?: InsertHook
19  prepatch?: PrePatchHook
20  update?: UpdateHook
21  postpatch?: PostPatchHook
22  destroy?: DestroyHook
23  remove?: RemoveHook
24  post?: PostHook
25}
26
27

接下來我們通過 03-modules.js 文件的示例代碼,我們需要樣式處理事件操作,因此引入這兩個模塊,並進行靈活組合

 1// src/03-modules.js
 2
 3import { h } from 'snabbdom/src/package/h'
 4import { init } from 'snabbdom/src/package/init'
 5
 6// 1. 導入模塊
 7import { styleModule } from 'snabbdom/src/package/modules/style'
 8import { eventListenersModule } from 'snabbdom/src/package/modules/eventlisteners'
 9
10// 2. 註冊模塊
11const patch = init([ styleModule, eventListenersModule ])
12
13// 3. 使用 h() 函數的第二個參數傳入模塊需要的數據(對象)
14let vnode = h('div', {
15  style: { backgroundColor: '#4fc08d', color: '#35495d' },
16  on: { click: eventHandler }
17}, [
18  h('h1', 'Hello Snabbdom'),
19  h('p', 'This is p tag')
20])
21
22function eventHandler() {
23  console.log('clicked.')
24}
25
26const app = document.getElementById('app')
27patch(app, vnode)
28
29

上面代碼中,引入了 styleModule 和 eventListenersModule 兩個模塊,並且作爲參數組合,傳入 init() 函數中。此時我們可以看到頁面上顯示的內容已經有包含樣式,並且點擊事件也能正常輸出日誌 'clicked.'

這裏我們看下 styleModule 模塊源碼,把代碼精簡一下:

 1// snabbdom/src/package/modules/style.ts
 2
 3function updateStyle (oldVnode: VNode, vnode: VNode): void {
 4 // 省略其他代碼
 5}
 6
 7function forceReflow () {
 8  // 省略其他代碼
 9}
10
11function applyDestroyStyle (vnode: VNode): void {
12  // 省略其他代碼
13}
14
15function applyRemoveStyle (vnode: VNode, rm: () => void): void {
16  // 省略其他代碼
17}
18
19export const styleModule: Module = {
20  pre: forceReflow,
21  create: updateStyle,
22  update: updateStyle,
23  destroy: applyDestroyStyle,
24  remove: applyRemoveStyle
25}
26
27

在看看  eventListenersModule 模塊源碼:

 1// snabbdom/src/package/modules/eventlisteners.ts
 2
 3function updateEventListeners (oldVnode: VNode, vnode?: VNode): void {
 4 // 省略其他代碼
 5}
 6
 7export const eventListenersModule: Module = {
 8  create: updateEventListeners,
 9  update: updateEventListeners,
10  destroy: updateEventListeners
11}
12
13

明顯可以看出,兩個模塊返回的都是個對象,並且每個屬性爲一種鉤子,如 pre/create 等,值爲對應的處理函數,每個處理函數有統一的入參。

繼續看下 styleModule 中,樣式是如何綁定上去的。這裏分析它的 updateStyle 方法,因爲元素創建(create 鉤子)和元素更新(update 鉤子)階段都是通過這個方法處理:

 1// snabbdom/src/package/modules/style.ts
 2
 3function updateStyle (oldVnode: VNode, vnode: VNode): void {
 4  var cur: any
 5  var name: string
 6  var elm = vnode.elm
 7  var oldStyle = (oldVnode.data as VNodeData).style
 8  var style = (vnode.data as VNodeData).style
 9
10  if (!oldStyle && !style) return
11  if (oldStyle === style) return
12  
13  // 1. 設置新舊 style 默認值
14  oldStyle = oldStyle || {}
15  style = style || {}
16  var oldHasDel = 'delayed' in oldStyle
17
18  // 2. 比較新舊 style
19  for (name in oldStyle) {
20    if (!style[name]) {
21      if (name[0] === '-' && name[1] === '-') {
22        (elm as any).style.removeProperty(name)
23      } else {
24        (elm as any).style[name] = ''
25      }
26    }
27  }
28  for (name in style) {
29    cur = style[name]
30    if (name === 'delayed' && style.delayed) {
31      // 省略部分代碼
32    } else if (name !== 'remove' && cur !== oldStyle[name]) {
33      if (name[0] === '-' && name[1] === '-') {
34        (elm as any).style.setProperty(name, cur)
35      } else {
36        // 3. 設置新 style 到元素
37        (elm as any).style[name] = cur
38      }
39    }
40  }
41}
42
43

3. init() 分析

接着我們看下 init() 函數內部如何處理這些 Module。

首先在 init.ts 文件中,可以看到聲明瞭默認支持的 Hooks 鉤子列表:

1// snabbdom/src/package/init.ts
2
3const hooks: Array<keyof Module> = ['create', 'update', 'remove', 'destroy', 'pre', 'post']
4
5

接着看 hooks 是如何使用的:

 1// snabbdom/src/package/init.ts
 2
 3export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
 4  let i: number
 5  let j: number
 6  const cbs: ModuleHooks = {  // 創建 cbs 對象,用於收集 module 中的 hook
 7    create: [],
 8    update: [],
 9    remove: [],
10    destroy: [],
11    pre: [],
12    post: []
13  }
14 // 收集 module 中的 hook,並保存在 cbs 中
15  for (i = 0; i < hooks.length; ++i) {
16    cbs[hooks[i]] = []
17    for (j = 0; j < modules.length; ++j) {
18      const hook = modules[j][hooks[i]]
19      if (hook !== undefined) {
20        (cbs[hooks[i]] as any[]).push(hook)
21      }
22    }
23  }
24 // 省略其他代碼,稍後介紹
25}
26
27

上面代碼中,創建 hooks 變量用來聲明默認支持的 Hooks 鉤子,在 init() 函數中,創建 cbs 對象,通過兩層循環,保存每個 module 中的 hook 函數到 cbs 對象的指定鉤子中。

通過斷點可以看到這是 demo 中,cbs 對象是下面這個樣子:

這裏 cbs 對象收集了每個 module 中的 Hooks 處理函數,保存到對應 Hooks 數組中。比如這裏的 create 鉤子中保存了 updateStyle 函數和 updateEventListeners 函數。

到這裏, init() 函數已經保存好所有 module 的 Hooks 處理函數,接下來就要看看 init() 函數返回的 patch() 函數,這裏面將用到前面保存好的 cbs 對象。

4. patch() 分析

init() 函數中最終返回一個 patch() 函數,這邊形成一個閉包,閉包裏面可以使用到 init() 函數作用域定義的變量和方法,因此在 patch() 函數中能使用 cbs 對象。

patch() 函數會在不同時機點(可以參照前面的 Hooks 介紹),遍歷 cbs 對象中不同 Hooks 處理函數列表。

 1// snabbdom/src/package/init.ts
 2
 3export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
 4 // 省略其他代碼
 5  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
 6    let i: number, elm: Node, parent: Node
 7    const insertedVnodeQueue: VNodeQueue = []
 8    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]( "i")  // [Hooks]遍歷 pre Hooks 處理函數列表
 9
10    if (!isVnode(oldVnode)) {
11      oldVnode = emptyNodeAt(oldVnode) // 當 oldVnode 參數不是 VNode 則創建一個空的 VNode
12    }
13
14    if (sameVnode(oldVnode, vnode)) {  // 當兩個 VNode 爲同一個 VNode,則進行比較和更新
15      patchVnode(oldVnode, vnode, insertedVnodeQueue)
16    } else {
17      createElm(vnode, insertedVnodeQueue) // 當兩個 VNode 不同,則創建新元素
18
19      if (parent !== null) {  // 當該 oldVnode 有父節點,則插入該節點,然後移除原來節點
20        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
21        removeVnodes(parent, [oldVnode], 0, 0)
22      }
23    }
24    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]( "i")  // [Hooks]遍歷 post Hooks 處理函數列表
25    return vnode
26  }
27}
28
29

patchVnode() 函數定義如下:

1  function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
2    // 省略其他代碼
3    if (vnode.data !== undefined) {
4      for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode "i")  // [Hooks]遍歷 update Hooks 處理函數列表
5    }
6  }
7
8

createVnode() 函數定義如下:

 1  function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
 2    // 省略其他代碼
 3    const sel = vnode.sel
 4    if (sel === '!') {
 5      // 省略其他代碼
 6    } else if (sel !== undefined) {
 7      for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode "i")  // [Hooks]遍歷 create Hooks 處理函數列表
 8      const hook = vnode.data!.hook
 9    }
10    return vnode.elm
11  }
12
13

removeNodes() 函數定義如下:

 1  function removeVnodes (parentElm: Node,vnodes: VNode[],startIdx: number,endIdx: number): void {
 2    // 省略其他代碼
 3    for (; startIdx <= endIdx; ++startIdx) {
 4      const ch = vnodes[startIdx]
 5      if (ch != null) {
 6        rm = createRmCb(ch.elm!, listeners)
 7        for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm "i") // [Hooks]遍歷 remove Hooks 處理函數列表
 8      }
 9    }
10  }
11
12

這部分代碼跳轉較多,總結一下這個過程,如下圖:

四、自定義 Snabbdom 模塊

前面我們介紹了 Snabbdom 模塊系統是如何收集 Hooks 並保存下來,然後在不同時機點執行不同的 Hooks。

在 Snabbdom 中,所有模塊獨立在 src/package/modules 下,使用的時候可以靈活組合,也方便做解耦和跨平臺,並且所有 Module 返回的對象中每個 Hooks 類型如下:

 1// snabbdom/src/package/init.ts
 2
 3export type Module = Partial<{
 4  pre: PreHook
 5  create: CreateHook
 6  update: UpdateHook
 7  destroy: DestroyHook
 8  remove: RemoveHook
 9  post: PostHook
10}>
11
12// snabbdom/src/package/hooks.ts
13export type PreHook = () => any
14export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any
15export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any
16export type DestroyHook = (vNode: VNode) => any
17export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any
18export type PostHook = () => any
19
20

因此,如果開發者需要自定義模塊,只需實現不同 Hooks 並導出即可。

接下來我們實現一個簡單的模塊 replaceTagModule,用來將節點文本自動過濾掉 HTML 標籤

1. 初始化代碼

考慮到方便調試,我們直接在 node_modules/snabbdom/src/package/modules/ 目錄中新建 replaceTag.ts 文件,然後寫個最簡單的 demo 框架:

 1import { VNode, VNodeData } from '../vnode'
 2import { Module } from './module'
 3
 4const replaceTagPre = () => {
 5    console.log("run replaceTagPre!")
 6}
 7
 8const updateReplaceTag = (oldVnode: VNode, vnode: VNode): void => {
 9    console.log("run updateReplaceTag!", oldVnode, vnode)
10}
11
12const removeReplaceTag = (vnode: VNode): void => {
13    console.log("run removeReplaceTag!", vnode)
14}
15
16export const replaceTagModule: Module = {
17    pre: replaceTagPre,
18    create: updateReplaceTag,
19    update: updateReplaceTag,
20    remove: removeReplaceTag
21}
22
23

接下來引入到 03-modules.js 代碼中,並簡化下代碼:

 1import { h } from 'snabbdom/src/package/h'
 2import { init } from 'snabbdom/src/package/init'
 3
 4// 1. 導入模塊
 5import { styleModule } from 'snabbdom/src/package/modules/style'
 6import { eventListenersModule } from 'snabbdom/src/package/modules/eventlisteners'
 7import { replaceTagModule } from 'snabbdom/src/package/modules/replaceTag';
 8
 9// 2. 註冊模塊
10const patch = init([
11  styleModule,
12  eventListenersModule,
13  replaceTagModule
14])
15
16// 3. 使用 h() 函數的第二個參數傳入模塊需要的數據(對象)
17let vnode = h('div', '<h1>Hello Leo</h1>')
18
19const app = document.getElementById('app')
20const oldVNode = patch(app, vnode)
21
22let newVNode = h('div', '<div>Hello Leo</div>')
23
24patch(oldVNode, newVNode)
25
26

刷新瀏覽器,就可以看到 replaceTagModule 的每個鉤子都被正常執行:

2. 實現 updateReplaceTag() 函數

我們刪除掉多餘代碼,接下來實現 updateReplaceTag() 函數,當 vnode 創建和更新時,都會調用該方法。

 1import { VNode, VNodeData } from '../vnode'
 2import { Module } from './module'
 3
 4const regFunction = str => str && str.replace(/\<|\>|\//g, "");
 5
 6const updateReplaceTag = (oldVnode: VNode, vnode: VNode): void => {
 7    const oldVnodeReplace = regFunction(oldVnode.text);
 8    const vnodeReplace = regFunction(vnode.text);
 9    if(oldVnodeReplace === vnodeReplace) return;
10    vnode.text = vnodeReplace;
11}
12
13export const replaceTagModule: Module = {
14    create: updateReplaceTag,
15    update: updateReplaceTag,
16}
17  
18
19

updateReplaceTag() 函數中,比較新舊 vnode 的文本內容是否一致,如果一致則直接返回,否則將新的 vnode 的替換後的文本設置到 vnode 的 text 屬性,完成更新。

其中有個細節:

1vnode.text = vnodeReplace;
2
3

這裏直接對 vnode.text 進行賦值,頁面上的內容也隨之發生變化。這是因爲 vnode 是個響應式對象,通過調用其 setter 方法,會觸發響應式更新,這樣就實現頁面內容更新。

於是我們看到頁面內容中的 HTML 標籤被清空了。

3. 小結

這個小節中,我們實現一個簡單的 replaceTagModule 模塊,體驗了一下 Snabbdom 模塊靈活組合的特點,當我們需要自定義某些模塊時,便可以按照 Snabbdom 的模塊開發方式,開發自定義模塊,然後通過 Snabbdom 的 init() 函數注入模塊即可。

我們再回顧一下 Snabbdom 模塊系統特點:支持按需引入、獨立管理、職責單一、方便組合複用、可維護性強。

五、通用模塊生命週期模型

下面我將前面 Snabbdom 的模塊系統,抽象爲一個通用模塊生命週期模型,其中包含三個核心層:

  1. 模塊定義層

在本層可以按照模塊開發規範,自定義各種模塊。

  1. 模塊應用層

一般是在業務開發層或組件層中,用來導入模塊。

  1. 模塊初始化層

一般是在開發的模塊系統的插件中,提供初始化函數(init 函數),執行初始化函數會遍歷每個 Hooks,並執行對應處理函數列表的每個函數。

抽象後的模型如下:

在使用 Module 的時候就可以靈活組合搭配使用啦,在模塊初始化層,就會做好調用。

六、總結

本文主要以 Snabbdom-demo 倉庫爲學習示例,學習了 Snabbdom 運行流程和 Snabbdom 模塊系統的運行流程,還通過手寫一個簡單的 Snabbdom 模塊,帶大家領略一下 Snabbdom 模塊的魅力,最後爲大家總結了一個通用模塊插件模型。

大家好好掌握 Snabbdom 對理解 Vue 會很有幫助。

參考資料

[1]

Snabbdom: https://github.com/snabbdom/snabbdom

[2]

《Snabbdom》: https://github.com/snabbdom/snabbdom

[3]

snabbdom-demo: https://github.com/zyycode/snabbdom-demo

[4]

Snabbdom-demo: https://github.com/zyycode/snabbdom-demo

[5]

HTMLElement.dataset: https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/dataset

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/devPUU1yb-q7GbqAIN-3Ww