從 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 庫。其中有幾個核心特性:
-
核心代碼 200 行,並且提供豐富的測試用例;
-
擁有強大模塊系統,並且支持模塊拓展和靈活組合;
-
在每個 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 目錄找到這三個示例代碼的文件:
-
01-basicusage.js
-
02-basicusage.js
-
03-modules.js -> 本文核心介紹
接着在 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:
模塊中可以使用這些鉤子:pre
, create
, update
, destroy
, remove
, post
。單個元素可以使用這些鉤子:init
, create
, insert
, prepatch
, update
, postpatch
, destroy
, remove
。
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 的模塊系統,抽象爲一個通用模塊生命週期模型,其中包含三個核心層:
- 模塊定義層
在本層可以按照模塊開發規範,自定義各種模塊。
- 模塊應用層
一般是在業務開發層或組件層中,用來導入模塊。
- 模塊初始化層
一般是在開發的模塊系統的插件中,提供初始化函數(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