【源碼】902- 探索 Snabbdom 模塊系統原理

近幾年隨着 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:

npm install snabbdom -D

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

<div id="app"></div>

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

// demo1.js
import { h } from 'snabbdom/src/package/h'
import { init } from 'snabbdom/src/package/init'

const patch = init([])
let vnode = h('div#app''Hello Leo')
const app = document.getElementById('app')
patch(app, vnode)

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

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

二、Snabbdom-demo 分析

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

$ git clone https://github.com/zyycode/snabbdom-demo.git
$ npm install

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

  • 01-basicusage.js
  • 02-basicusage.js
  • 03-modules.js  -> 本文核心介紹

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

$ npm run dev

1. 簡單示例分析

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

// src/01-basicusage.js

import { h } from 'snabbdom/src/package/h'
import { init } from 'snabbdom/src/package/init'

const patch = init([])

let vnode = h('div#container.cls''Hello World')
const app = document.getElementById('app'// 入口元素

const oldVNode = patch(app, vnode)

// 假設時刻
vnode = h('div''Hello Snabbdom')
patch(oldVNode, vnode)

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

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

vnode = h('div''Hello Snabbdom')
patch(oldVNode, vnode)

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

2. VNode 介紹

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

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

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

export interface VNode {
  sel: string | undefined// selector的縮寫
  data: VNodeData | undefined// 下面VNodeData接口的內容
  children: Array<VNode | string> | undefined// 子節點
  elm: Node | undefined// element的縮寫,存儲了真實的HTMLElement
  text: string | undefined// 如果是文本節點,則存儲text
  key: Key | undefined// 節點的key,在做列表時很有用
}

export interface VNodeData {
  props?: Props
  attrs?: Attrs
  class?: Classes
  style?: VNodeStyle
  dataset?: Dataset
  on?: On
  hero?: Hero
  attachData?: AttachData
  hook?: Hooks
  key?: Key
  ns?: string // for SVGs
  fn?: () => VNode // for thunks
  args?: any[] // for thunks
  [key: string]: any // for any other 3rd party module
}

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

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

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

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

3. init() 函數分析

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

// node_modules/snabbdom/src/package/init.ts

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI{
 // 省略其他代碼
}

其參數類型如下:

function init(modules: Array<Partial<Module>>, domApi?: DOMAPI): (oldVnode: VNode | Element, vnode: VNode) => VNode

export type Module = Partial<
{
  pre: PreHook
  create: CreateHook
  update: UpdateHook
  destroy: DestroyHook
  remove: RemoveHook
  post: PostHook
}>
  
export interface DOMAPI {
  createElement: (tagName: any) => HTMLElement
  createElementNS: (namespaceURI: string, qualifiedName: string) => Element
  createTextNode: (text: string) => Text
  createComment: (text: string) => Comment
  insertBefore: (parentNode: Node, newNode: Node, referenceNode: Node | null) => void
  removeChild: (node: Node, child: Node) => void
  appendChild: (node: Node, child: Node) => void
  parentNode: (node: Node) => Node | null
  nextSibling: (node: Node) => Node | null
  tagName: (elm: Element) => string
  setTextContent: (node: Node, text: string | null) => void
  getTextContent: (node: Node) => string | null
  isElement: (node: Node) => node is Element
  isText: (node: Node) => node is Text
  isComment: (node: Node) => node is Comment
}

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

4. patch() 函數分析

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

// node_modules/snabbdom/src/package/init.ts

patch(oldVnode: VNode | Element, vnode: VNode) => VNode

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

5. h() 函數分析

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

// node_modules/snabbdom/src/package/h.ts

export function h(sel: string): VNode
export function h(sel: string, data: VNodeData | null): VNode
export function h(sel: string, children: VNodeChildren): VNode
export function h(sel: string, data: VNodeData | null, children: VNodeChildren): VNode
export function h (sel: any, b?: any, c?: any): VNode{
 // 省略其他代碼
}

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

6. 小結

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

三、深入 Snabbdom 模塊系統

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

1. Modules 介紹

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

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

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

模塊名稱 模塊功能 示例代碼
attributesModule 爲 DOM 元素設置屬性,在屬性添加和更新時使用 setAttribute 方法。 h('a', { attrs: { href: '/foo' } }, 'Go to Foo')
classModule 用來動態設置和切換 DOM 元素上的 class 名稱。 h('a', { class: { active: true, selected: false } }, 'Toggle')
datasetModule 爲 DOM 元素設置自定義數據屬性(data- *)。然後可以使用 HTMLElement.dataset[5] 屬性訪問它們。 h('button', { dataset: { action: 'reset' } }, 'Reset')
eventListenersModule 爲 DOM 元素綁定事件監聽器。 h('div', { on: { click: clickHandler } })
propsModule 爲 DOM 元素設置屬性,如果同時使用 attributesModule,則會被 attributesModule 覆蓋。 h('a', { props: { href: '/foo' } }, 'Go to Foo')
styleModule 爲 DOM 元素設置 CSS 屬性。 h('span', {style: { color: '#c0ffee'}}, 'Say my name')

2. Hooks 介紹

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

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

鉤子名稱 觸發時機 回調參數
pre patch 階段開始。 none
init 已添加一個 VNode。 vnode
create 基於 VNode 創建了一個 DOM 元素。 emptyVnode, vnode
insert 一個元素已添加到 DOM 元素中。 vnode
prepatch 一個元素即將進入 patch 階段。 oldVnode, vnode
update 一個元素開始更新。 oldVnode, vnode
postpatch 一個元素完成 patch 階段。 oldVnode, vnode
destroy 一個元素直接或間接被刪除。 vnode
remove 一個元素直接從 DOM 元素中刪除。 vnode, removeCallback
post patch 階段結束。 none

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

Snabbdom 是這麼定義鉤子的:

// snabbdom/src/package/hooks.ts

export type PreHook = () => any
export type InitHook = (vNode: VNode) => any
export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any
export type InsertHook = (vNode: VNode) => any
export type PrePatchHook = (oldVNode: VNode, vNode: VNode) => any
export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any
export type PostPatchHook = (oldVNode: VNode, vNode: VNode) => any
export type DestroyHook = (vNode: VNode) => any
export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any
export type PostHook = () => any

export interface Hooks {
  pre?: PreHook
  init?: InitHook
  create?: CreateHook
  insert?: InsertHook
  prepatch?: PrePatchHook
  update?: UpdateHook
  postpatch?: PostPatchHook
  destroy?: DestroyHook
  remove?: RemoveHook
  post?: PostHook
}

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

// src/03-modules.js

import { h } from 'snabbdom/src/package/h'
import { init } from 'snabbdom/src/package/init'

// 1. 導入模塊
import { styleModule } from 'snabbdom/src/package/modules/style'
import { eventListenersModule } from 'snabbdom/src/package/modules/eventlisteners'

// 2. 註冊模塊
const patch = init([ styleModule, eventListenersModule ])

// 3. 使用 h() 函數的第二個參數傳入模塊需要的數據(對象)
let vnode = h('div', {
  style: { backgroundColor'#4fc08d'color'#35495d' },
  on: { click: eventHandler }
}, [
  h('h1''Hello Snabbdom'),
  h('p''This is p tag')
])

function eventHandler({
  console.log('clicked.')
}

const app = document.getElementById('app')
patch(app, vnode)

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

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

// snabbdom/src/package/modules/style.ts

function updateStyle (oldVnode: VNode, vnode: VNode): void {
 // 省略其他代碼
}

function forceReflow ({
  // 省略其他代碼
}

function applyDestroyStyle (vnode: VNode): void {
  // 省略其他代碼
}

function applyRemoveStyle (vnode: VNode, rm: () => void): void {
  // 省略其他代碼
}

export const styleModule: Module = {
  pre: forceReflow,
  create: updateStyle,
  update: updateStyle,
  destroy: applyDestroyStyle,
  remove: applyRemoveStyle
}

在看看  eventListenersModule 模塊源碼:

// snabbdom/src/package/modules/eventlisteners.ts

function updateEventListeners (oldVnode: VNode, vnode?: VNode): void {
 // 省略其他代碼
}

export const eventListenersModule: Module = {
  create: updateEventListeners,
  update: updateEventListeners,
  destroy: updateEventListeners
}

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

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

// snabbdom/src/package/modules/style.ts

function updateStyle (oldVnode: VNode, vnode: VNode): void {
  var cur: any
  var name: string
  var elm = vnode.elm
  var oldStyle = (oldVnode.data as VNodeData).style
  var style = (vnode.data as VNodeData).style

  if (!oldStyle && !style) return
  if (oldStyle === style) return
  
  // 1. 設置新舊 style 默認值
  oldStyle = oldStyle || {}
  style = style || {}
  var oldHasDel = 'delayed' in oldStyle

  // 2. 比較新舊 style
  for (name in oldStyle) {
    if (!style[name]) {
      if (name[0] === '-' && name[1] === '-') {
        (elm as any).style.removeProperty(name)
      } else {
        (elm as any).style[name] = ''
      }
    }
  }
  for (name in style) {
    cur = style[name]
    if (name === 'delayed' && style.delayed) {
      // 省略部分代碼
    } else if (name !== 'remove' && cur !== oldStyle[name]) {
      if (name[0] === '-' && name[1] === '-') {
        (elm as any).style.setProperty(name, cur)
      } else {
        // 3. 設置新 style 到元素
        (elm as any).style[name] = cur
      }
    }
  }
}

3. init() 分析

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

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

// snabbdom/src/package/init.ts

const hooks: Array<keyof Module> = ['create''update''remove''destroy''pre''post']

接着看 hooks 是如何使用的:

// snabbdom/src/package/init.ts

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI{
  let i: number
  let j: number
  const cbs: ModuleHooks = {  // 創建 cbs 對象,用於收集 module 中的 hook
    create: [],
    update: [],
    remove: [],
    destroy: [],
    pre: [],
    post: []
  }
 // 收集 module 中的 hook,並保存在 cbs 中
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      const hook = modules[j][hooks[i]]
      if (hook !== undefined) {
        (cbs[hooks[i]] as any[]).push(hook)
      }
    }
  }
 // 省略其他代碼,稍後介紹
}

上面代碼中,創建 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 處理函數列表。

// snabbdom/src/package/init.ts

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI{
 // 省略其他代碼
  return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
    let i: number, elm: Node, parent: Node
    const insertedVnodeQueue: VNodeQueue = []
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]( "i")  // [Hooks]遍歷 pre Hooks 處理函數列表

    if (!isVnode(oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode) // 當 oldVnode 參數不是 VNode 則創建一個空的 VNode
    }

    if (sameVnode(oldVnode, vnode)) {  // 當兩個 VNode 爲同一個 VNode,則進行比較和更新
      patchVnode(oldVnode, vnode, insertedVnodeQueue)
    } else {
      createElm(vnode, insertedVnodeQueue) // 當兩個 VNode 不同,則創建新元素

      if (parent !== null) {  // 當該 oldVnode 有父節點,則插入該節點,然後移除原來節點
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
        removeVnodes(parent, [oldVnode], 00)
      }
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]( "i")  // [Hooks]遍歷 post Hooks 處理函數列表
    return vnode
  }
}

patchVnode() 函數定義如下:

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

createVnode() 函數定義如下:

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

removeNodes() 函數定義如下:

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

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

四、自定義 Snabbdom 模塊

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

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

// snabbdom/src/package/init.ts

export type Module = Partial<{
  pre: PreHook
  create: CreateHook
  update: UpdateHook
  destroy: DestroyHook
  remove: RemoveHook
  post: PostHook
}>

// snabbdom/src/package/hooks.ts
export type PreHook = () => any
export type CreateHook = (emptyVNode: VNode, vNode: VNode) => any
export type UpdateHook = (oldVNode: VNode, vNode: VNode) => any
export type DestroyHook = (vNode: VNode) => any
export type RemoveHook = (vNode: VNode, removeCallback: () => void) => any
export type PostHook = () => any

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

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

1. 初始化代碼

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

import { VNode, VNodeData } from '../vnode'
import { Module } from './module'

const replaceTagPre = () => {
    console.log("run replaceTagPre!")
}

const updateReplaceTag = (oldVnode: VNode, vnode: VNode): void => {
    console.log("run updateReplaceTag!", oldVnode, vnode)
}

const removeReplaceTag = (vnode: VNode): void => {
    console.log("run removeReplaceTag!", vnode)
}

export const replaceTagModule: Module = {
    pre: replaceTagPre,
    create: updateReplaceTag,
    update: updateReplaceTag,
    remove: removeReplaceTag
}

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

import { h } from 'snabbdom/src/package/h'
import { init } from 'snabbdom/src/package/init'

// 1. 導入模塊
import { styleModule } from 'snabbdom/src/package/modules/style'
import { eventListenersModule } from 'snabbdom/src/package/modules/eventlisteners'
import { replaceTagModule } from 'snabbdom/src/package/modules/replaceTag';

// 2. 註冊模塊
const patch = init([
  styleModule,
  eventListenersModule,
  replaceTagModule
])

// 3. 使用 h() 函數的第二個參數傳入模塊需要的數據(對象)
let vnode = h('div''<h1>Hello Leo</h1>')

const app = document.getElementById('app')
const oldVNode = patch(app, vnode)

let newVNode = h('div''<div>Hello Leo</div>')

patch(oldVNode, newVNode)

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

2. 實現 updateReplaceTag() 函數

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

import { VNode, VNodeData } from '../vnode'
import { Module } from './module'

const regFunction = str => str && str.replace(/\<|\>|\//g"");

const updateReplaceTag = (oldVnode: VNode, vnode: VNode): void => {
    const oldVnodeReplace = regFunction(oldVnode.text);
    const vnodeReplace = regFunction(vnode.text);
    if(oldVnodeReplace === vnodeReplace) return;
    vnode.text = vnodeReplace;
}

export const replaceTagModule: Module = {
    create: updateReplaceTag,
    update: updateReplaceTag,
}
  

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

其中有個細節:

vnode.text = vnodeReplace;

這裏直接對 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



【JS】646- 1.2w字 | 初中級前端 JavaScript 自測清單 - 1


【JS】676- 1.1w字 | 初中級前端 JavaScript 自測清單 - 2


回覆“加羣”與大佬們一起交流學習~

點擊“閱讀原文”查看 100+ 篇原創文章

本文分享自微信公衆號 - 前端自習課(FE-study)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章