近幾年隨着 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:
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 |
模塊中可以使用這些鉤子:pre
, create
, update
, destroy
, remove
, post
。單個元素可以使用這些鉤子:init
, create
, insert
, prepatch
, update
, postpatch
, destroy
, remove
。
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], 0, 0)
}
}
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 的模塊系統,抽象爲一個通用模塊生命週期模型,其中包含三個核心層:
-
模塊定義層
在本層可以按照模塊開發規範,自定義各種模塊。
-
模塊應用層
一般是在業務開發層或組件層中,用來導入模塊。
-
模塊初始化層
一般是在開發的模塊系統的插件中,提供初始化函數(init 函數),執行初始化函數會遍歷每個 Hooks,並執行對應處理函數列表的每個函數。
抽象後的模型如下:
在使用 Module 的時候就可以靈活組合搭配使用啦,在模塊初始化層,就會做好調用。
六、總結
本文主要以 Snabbdom-demo 倉庫爲學習示例,學習了 Snabbdom 運行流程和 Snabbdom 模塊系統的運行流程,還通過手寫一個簡單的 Snabbdom 模塊,帶大家領略一下 Snabbdom 模塊的魅力,最後爲大家總結了一個通用模塊插件模型。
大家好好掌握 Snabbdom 對理解 Vue 會很有幫助。
參考資料
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源創計劃”,歡迎正在閱讀的你也加入,一起分享。