{
sel: "div",
data: {},
children: undefined,
text: "Hello Virtual DOM",
elm: undefined,
key: undefined
}
注意:创建一个虚拟DOM比创建一个真实的DOM成本低很多
1. 虚拟DOM可以维护程序的状态,跟踪上一次的状态
2. 通过比较前后两次状态的差异更新真实DOM
Virtual DOM库
Snabbdom基本使用
创建项目
# 创建项目目录
md snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建 package.json
yarn init -y
# 本地安装 parcel
yarn add parcel-bundler
"scripts": {
"dev": "parcel index.html --open",
"build": "parcel build index.html"
}
index.html
package.json
01-basicuage.js
导入Snabbdom
中文文档:https://github.com/coconilu/Blog/issues/152
yarn add snabbdom
import {init, h, thunk} from 'snabbdom'
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
export {h} from './h'
export {thunk} from './thunk'
export function init(modules: Array>, domApi?: DOMAPI)
}
两个案例来展示init()和h()这两个函数的功能
01-basicusage.js:
import { h, init} from 'snabbdom'
// 1. hello world
// 参数: 数组,模块
// 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
let patch = init([])
// 第一个参数: 标签+ 选择器
// 第二个参数: 如果是字符串的话就是标签中的内容
let vnode = h('div#container.cls','Hello World')
let app = document.querySelector('#app')
// 第一个参数: 可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数: VNode
// 返回值:VNode
let oldVNode = patch(app, vnode)
// 假设的时刻
vnode = h('div', 'Hello Snabbdom')
patch(oldVNode, vnode)
02-basicusage.js:
// 2. div中放置子元素 h1,p
import { h , init } from 'snabbdom'
let patch = init([])
let vnode = h('div#container',[
h('h1', 'Hello Snabbdom'),
h('p', '这是一个p标签')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
setTimeout(() => {
vnode = h('div#container',[
h('h1', 'Hello World'),
h('p', 'Hello P')
])
patch(oldVnode, vnode)
// 清空页面元素 -- 错误
// patch(oldVnode, null)
patch(oldVnode, h('!'))
}, 2000);
Snabbdom的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块
常用模块
模块使用
代码演示:
import { init, h } from 'snabbdom'
// 1. 导入模块
import style from 'snabbdom/modules/style'
import eventlisteners from 'snabbdom/modules/eventlisteners'
// 2. 注册模块
let patch = init([
style,
eventlisteners
])
// 3. 使用h() 函数的第二个参数传入模块需要的数据(对象)
let vnode = h('div', {
style: {
backgroundColor: 'red'
},
on: {
click: eventHandler
}
},[
h('h1', 'Hello Snabbdom'),
h('p', '这是p标签')
])
function eventHandler () {
console.log('点击我了')
}
let app = document.querySelector('#app')
patch(app, vnode)
概述
如何学习源码
Snabbdom的核心
Snabbdom源码
h函数
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
function add(a, b) {
console.log(a + b)
}
function add (a, b, c) {
console.log(a + b + c)
}
add(1, 2)
add(1, 2, 3)
源码位置: src/h.ts (作用:调用vnode函数返回虚拟节点)
import { vnode, VNode, VNodeData } from './vnode'
import * as is from './is'
export type VNodes = VNode[]
export type VNodeChildElement = VNode | string | number | undefined | null
export type ArrayOrElement = T | T[]
export type VNodeChildren = ArrayOrElement
function addNS (data: any, children: VNodes | undefined, sel: string | undefined): void {
data.ns = 'http://www.w3.org/2000/svg'
if (sel !== 'foreignObject' && children !== undefined) {
for (let i = 0; i < children.length; ++i) {
const childData = children[i].data
if (childData !== undefined) {
addNS(childData, (children[i] as VNode).children as VNodes, children[i].sel)
}
}
}
}
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 {
var data: VNodeData = {}
var children: any
var text: any
var i: number
if (c !== undefined) {
if (b !== null) {
data = b
}
if (is.array(c)) {
children = c
} else if (is.primitive(c)) {
text = c
} else if (c && c.sel) {
children = [c]
}
} else if (b !== undefined && b !== null) {
if (is.array(b)) {
children = b
} else if (is.primitive(b)) {
text = b
} else if (b && b.sel) {
children = [b]
} else { data = b }
}
if (children !== undefined) {
for (i = 0; i < children.length; ++i) {
if (is.primitive(children[i])) children[i] = vnode(undefined, undefined, undefined, children[i], undefined)
}
}
if (
sel[0] === 's' && sel[1] === 'v' && sel[2] === 'g' &&
(sel.length === 3 || sel[3] === '.' || sel[3] === '#')
) {
addNS(data, children, sel)
}
return vnode(sel, data, children, text, undefined)
};
必备快捷键
vnode
export interface VNode {
sel: string | undefined
data: VNodeData | undefined
children: Array | undefined
elm: Node | undefined
text: string | undefined
key: Key | undefined
}
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
}
export function vnode (sel: string | undefined,
data: any | undefined,
children: Array | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}
snabbdom
init函数
设置模块,传入modules和domApi;没有传第二个参数domApi时,使用htmlDomApi(把虚拟DOM转换成真实DOM)
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
}
function createElement (tagName: any): HTMLElement {
return document.createElement(tagName)
}
function createElementNS (namespaceURI: string, qualifiedName: string): Element {
return document.createElementNS(namespaceURI, qualifiedName)
}
function createTextNode (text: string): Text {
return document.createTextNode(text)
}
function createComment (text: string): Comment {
return document.createComment(text)
}
function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node | null): void {
parentNode.insertBefore(newNode, referenceNode)
}
function removeChild (node: Node, child: Node): void {
node.removeChild(child)
}
function appendChild (node: Node, child: Node): void {
node.appendChild(child)
}
function parentNode (node: Node): Node | null {
return node.parentNode
}
function nextSibling (node: Node): Node | null {
return node.nextSibling
}
function tagName (elm: Element): string {
return elm.tagName
}
function setTextContent (node: Node, text: string | null): void {
node.textContent = text
}
function getTextContent (node: Node): string | null {
return node.textContent
}
function isElement (node: Node): node is Element {
return node.nodeType === 1
}
function isText (node: Node): node is Text {
return node.nodeType === 3
}
function isComment (node: Node): node is Comment {
return node.nodeType === 8
}
export const htmlDomApi: DOMAPI = {
createElement,
createElementNS,
createTextNode,
createComment,
insertBefore,
removeChild,
appendChild,
parentNode,
nextSibling,
tagName,
setTextContent,
getTextContent,
isElement,
isText,
isComment,
}
// 初始化转换虚拟节点的api
const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi
// 把传入的所以模块的钩子函数,统一存储到cbs对象中
// 最终构建的cbs对象的形式 cbs = { crete: [fn1, fn2], update: [], ...}
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)
}
}
}
module:
import { PreHook, CreateHook, UpdateHook, DestroyHook, RemoveHook, PostHook } from '../hooks'
export type Module = Partial<{
pre: PreHook
create: CreateHook
update: UpdateHook
destroy: DestroyHook
remove: RemoveHook
post: PostHook
}>
patch函数
// init 内部返回 patch函数,把 vnode 渲染成真实 dom,并返回vnode
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode {
let i: number, elm: Node, parent: Node
// 保存新插入节点的队列,为了触发钩子函数
const insertedVnodeQueue: VNodeQueue = []
// 执行模块的pre 钩子函数
for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]()
// 如果 oldVnode不是VNode,创建VNode 并设置 elm
if (!isVnode(oldVnode)) {
// 把DOM元素转换成空的 VNode
oldVnode = emptyNodeAt(oldVnode)
}
// 如果新旧节点是相同节点(key 和sel 相同)
if (sameVnode(oldVnode, vnode)) {
// 找节点的差异并更新 DOM
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
// 如果新旧节点不同,vnode创建对应的 DOM
// 获取当前的 DOM元素
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node
// 创建 vnode对应的DOM元素,并触发 init/reate 钩子函数
createElm(vnode, insertedVnodeQueue)
if (parent !== null) {
// 如果父节点不为空,把 vnode对应的 DOM插入到文档中
api.insertBefore(parent, vnode.elm!, api.nextSibling(elm))
// 移除老节点
removeVnodes(parent, [oldVnode], 0, 0)
}
}
// 执行用户设置的insert 钩子函数
for (i = 0; i < insertedVnodeQueue.length; ++i) {
insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i])
}
// 执行模块的post钩子函数
for (i = 0; i < cbs.post.length; ++i) cbs.post[i]()
// 返回 vnode
return vnode
}
createElm
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any
let data = vnode.data
if (data !== undefined) {
const init = data.hook?.init
if (isDef(init)) {
init(vnode)
data = vnode.data
}
}
// 把 vnode 转换成真实 DOM对象(没有渲染到页面)
const children = vnode.children
const sel = vnode.sel
if (sel === '!') {
// 如果选择器是!,创建注释节点
if (isUndef(vnode.text)) {
vnode.text = ''
}
vnode.elm = api.createComment(vnode.text!)
} else if (sel !== undefined) {
// 如果选择器不为空
// 解析选择器
// Parse selector
const hashIdx = sel.indexOf('#')
const dotIdx = sel.indexOf('.', hashIdx)
const hash = hashIdx > 0 ? hashIdx : sel.length
const dot = dotIdx > 0 ? dotIdx : sel.length
const tag = hashIdx !== -1 || dotIdx !== -1 ? sel.slice(0, Math.min(hash, dot)) : sel
const elm = vnode.elm = isDef(data) && isDef(i = data.ns)
? api.createElementNS(i, tag)
: api.createElement(tag)
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot))
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' '))
// 执行模块的 create 钩子函数
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode)
// 如果 vnode中有子节点,创建子vnode对应的DOM元素并追加到 DOM 树上
if (is.array(children)) {
for (i = 0; i < children.length; ++i) {
const ch = children[i]
if (ch != null) {
api.appendChild(elm, createElm(ch as VNode, insertedVnodeQueue))
}
}
} else if (is.primitive(vnode.text)) {
// 如果 vnode的text值是 string/number,创建文本节点并追加到 DOM 树
api.appendChild(elm, api.createTextNode(vnode.text))
}
const hook = vnode.data!.hook
if (isDef(hook)) {
// 执行用户传入的钩子 create
hook.create?.(emptyNode, vnode)
if (hook.insert) {
// 把vnode添加到队列中,为后续执行 insert 钩子做准备
insertedVnodeQueue.push(vnode)
}
}
} else {
// 如果选择器为空,创建文本节点
vnode.elm = api.createTextNode(vnode.text!)
}
// 返回新创建的 DOM
return vnode.elm
}
addVnodes和removeVnodes
function addVnodes (
parentElm: Node,
before: Node | null,
vnodes: VNode[],
startIdx: number,
endIdx: number,
insertedVnodeQueue: VNodeQueue
) {
for (; startIdx <= endIdx; ++startIdx) {
const ch = vnodes[startIdx]
if (ch != null) {
api.insertBefore(parentElm, createElm(ch, insertedVnodeQueue), before)
}
}
}
// 批量删除节点
function removeVnodes (parentElm: Node,
vnodes: VNode[],
startIdx: number,
endIdx: number): void {
for (; startIdx <= endIdx; ++startIdx) {
let listeners: number
let rm: () => void
const ch = vnodes[startIdx]
if (ch != null) {
if (isDef(ch.sel)) {
invokeDestroyHook(ch)
listeners = cbs.remove.length + 1
rm = createRmCb(ch.elm!, listeners)
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
const removeHook = ch?.data?.hook?.remove
if (isDef(removeHook)) {
removeHook(ch, rm)
} else {
rm()
}
} else { // Text node
api.removeChild(parentElm, ch.elm!)
}
}
}
}
patchVnode
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
const hook = vnode.data?.hook
hook?.prepatch?.(oldVnode, vnode)
const elm = vnode.elm = oldVnode.elm!
const oldCh = oldVnode.children as VNode[]
const ch = vnode.children as VNode[]
if (oldVnode === vnode) return
if (vnode.data !== undefined) {
// 执行模块的 update钩子函数
for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 执行用户设置的 update 钩子函数
vnode.data.hook?.update?.(oldVnode, vnode)
}
// 如果vnode.text 未定义
if (isUndef(vnode.text)) {
// 如果新老节点都有 children
if (isDef(oldCh) && isDef(ch)) {
// 使用diff算法对比子节点,老节点没有children
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue)
} else if (isDef(ch)) {
// 如果新节点有children,老节点没有children
// 如果老节点有text,清空dom元素的内容
if (isDef(oldVnode.text)) api.setTextContent(elm, '')
// 批量添加子节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// 如果没有设置vnode.text
} else if (isDef(oldCh)) {
// 如果老节点有chaldren移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
api.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
if (isDef(oldCh)) {
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 设置DOM元素的 textContent 为vnode.text
api.setTextContent(elm, vnode.text!)
}
// 最后执行用户设置的 postpatch 钩子函数
hook?.postpatch?.(oldVnode, vnode)
}