抽空补充中 内容仅为学习笔记,如果有错误欢迎指出,谢谢
Virtual DOM
1.基本知识
-
virtual DOM指的是用JS模拟的DOM结构,将DOM变化的对比放在JS层来做。换而言之,vdom就是JS对象。
-
浏览器操作DOM的开销很大,Virtual DOM用来解决此问题
-
模板引擎可以简化试图操作,到时无法跟踪状态。而虚拟DOm可以跟踪状态。
-
虚拟DOM作用
- 维护视图和状态的关系,保存视图的状态
- 复杂视图情况下提升渲染性能
- 跨平台
-
开源库
- Snabbdom
- Vue.js 2.x内部使用的虚拟DOm就是改造 Snabbdom
- 源代码比较小
- 通过模块可扩展
- 源码使用TS开发
- 快
- Virtual-dom
- Snabbdom
2.Snabbdom基本使用
// 注意路径问题
import {init} from "snabbdom/build/package/init.js";
import {h} from "snabbdom/build/package/h.js";
const patch = init([])
// h函数接收一个字符串形式的标签/选择器、一个可选的数据对象、一个可选的字符串或数组作为子代。
let vnode = h('div#container.cls',[
h('h1','Hello snabbdom'),
h('p','这里是一个p')
])
// 获取占位容器系欸点
let app = document.querySelector('#app')
// patch反法旧时对比 old和new的dom的差异,把数据渲染到页面上,第一个参数可以传入一个真实的dom,petch会自动转化
// patch返回一个新vnod
let oldVnode = patch(app, vnode)
// 两秒后更新
setTimeout(() => {
vnode = h('div#container.cls',[
h('h1','Hello 51C'),
h('p','这里是一个新的p')
])
oldVnode = patch(oldVnode,vnode)
},2000)
// 5秒后清空 html变成注释文本
setTimeout(() => {
patch(oldVnode,h('!')) // h('!')创建一个空的节点
},5000)
- 模块使用
- Snabbdom的核心库并不能处理DOM元素的属性、样式、事件等,可以通过注册Snabbdom默认提供的模块来实现的
- Snabbdom中的模块可以用来扩展Snabbdom的功能
- Snabbdom中的模块的实现是通过组测全局的钩子函数来实现的
- 官方提供的模块
- attributes,设置DOM的属性,通过setAttribute()方法。处理布尔类型的属性。
- props,处理非布尔类型的属性,通过对象.属性的方法实现
- class,切换样式
- dataset,设置自定义data-*属性。
- eventlisteners
- style ,设置行内样式
- 模块的使用
- 导入模块。
- init()函数注册模块
- 使用h()函数创建VNode时,在第二个参数中传入对象。
import {init} from "snabbdom/build/package/init.js";
import {h} from "snabbdom/build/package/h.js";
// 1.导入模块
import { styleModule } from "snabbdom/build/package/modules/style";
import { eventListenersModule } from "snabbdom/build/package/modules/eventlisteners";
// 2.注册模块
const petch = init([
styleModule,
eventListenersModule
])
// 3.使用h()函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div',[
h('h1',{style:{backgroundColor:'red'}},'Hello Snabbdom'),
h('p',{on:{click: handleClick}},'Change P')
])
function handleClick(){
console.log('点击了p');
}
let app = document.querySelector('#app')
petch(app,vnode)
3.Snabbdom源码解析(2.0版本)
3.1 Snabbdom的核心
- init()设置模块,创建Patch()函数
- 使用h()函数创建JavaScript对象(Vnode)描述真实的DOM
- patch()比较新旧两个Vnode
- 把变化内容更新到真实的Dom中
3.2 h函数
- 作用:创建V-node对象
- 知识补充:函数重载
- 参数个数或参数类型不同的函数
- JavaScript中没有重载的概念
- TypeScript中有重载,不过重载的实现还是通过代码调整参数
- h函数的主要作用根据,参数的不同去执行相关的操作
// h函数重加载
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) {
// 处理三个参数的情况
// sel data children/text
if (b !== null) {
data = b
}
// 如果c时字符串或数字
if (is.array(c)) {
children = c
} else if (is.primitive(c)) {
text = c
// 如果c时node节点
} 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) {
// 处理CHildren中的原始值(string/number)
for (i = 0; i < children.length; ++i) {
// 如果chuld时String/number就创建文本节点
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] === '#')
) {
// 如果时Svg,添加命名空间
addNS(data, children, sel)
}
// 返回Vnode
return vnode(sel, data, children, text, undefined)
};
3.2 vnode函数
vode属性
export interface VNode {
sel: string | undefined // 选择器
data: VNodeData | undefined // 描述模块中所需要的数据
children: Array<VNode | string> | undefined // 描述对应子节点
elm: Node | undefined // 存储vonde对象装欢的dom元素
text: string | undefined // 描述文本节点中的文本内容,与childeren是互排斥
key: Key | undefined
}
3.3 init函数
-
作用
- 用来处理跨平台的对应API
- 初始化模块
- 定义一个cbs变量用来储存处理钩子函数的回调函数数组
-
过程
-
init函数将不同模块的钩子函数存储起来,并且返回一个patch函数,这里是高阶函数的做法,处理一部分的变量,返回一个函数。
-
入参中
domApi,是可以指定任意api对象(好处是可以跨平台),若没有指定就是默认是操作浏览器的DOM的api -
入参
modules -
cbs对象用来储存模块中的处理钩子的回调函数,后面会在合适的时机调用
-
// 遍历moudle中的钩子函数并保存下来 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) // 存储回调函数 } } }
-
3.4 patch函数
-
整体过程分析(首次渲染)
return function patch (oldVnode: VNode | Element, vnode: VNode): VNode { let i: number, elm: Node, parent: Node const insertedVnodeQueue: VNodeQueue = [] // pre是处理vonde前触发的函数(预处理) for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]() if (!isVnode(oldVnode)) { // 判断是不是一个vnode对象 oldVnode = emptyNodeAt(oldVnode) // 将DOM对象转化为Vnode对象 } if (sameVnode(oldVnode, vnode)) { // 判断是否是相同节点 patchVnode(oldVnode, vnode, insertedVnodeQueue) // 相同节点调用函数,对比这个节点中的变化 } else { // 会新建一个DOM元素,并把这个元素插入到DOM树上,并移除老节点 elm = oldVnode.elm! // 获取旧的DOm元素 parent = api.parentNode(elm) as Node //获取父元素 createElm(vnode, insertedVnodeQueue) // 创建DOM元素 if (parent !== null) { api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)) // 向老节点的,插入下一个兄弟节点 removeVnodes(parent, [oldVnode], 0, 0) // 移除老节点 } } for (i = 0; i < insertedVnodeQueue.length; ++i) { insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]) } for (i = 0; i < cbs.post.length; ++i) cbs.post[i]() return vnode }
3.5 createElm函数
- 作用:把Vnode节点转换成DOM元素,把DOM元素存储再vnode的elm属性上,但是没有挂载到dom树上
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
// 执行用户的设置的init钩子函数
let i: any
let data = vnode.data
if (data !== undefined) {
const init = data.hook?.init
if (isDef(init)) { // init 使用同通过hook传入的钩子函数
init(vnode)
data = vnode.data // 重新赋值避免在钩子函数中改变值
}
}
const children = vnode.children
const sel = vnode.sel
// 把Vnode转换成真实DOM对象(没有渲染到页面)
if (sel === '!') { // 如果是!,创建注释节点
if (isUndef(vnode.text)) { // isUndef判断文本内容是否为空
vnode.text = ''
}
vnode.elm = api.createComment(vnode.text!) // 创建注释节点,并保存再vnode.elm属性上,再petch通过insertBefore插入节点
} else if (sel !== undefined) { // 创建对应的DOM元素
// 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 // 处理选择器 tag值标签名
const elm = vnode.elm = isDef(data) && isDef(i = data.ns) // ns是命名空间,判断创建对应对应的DOM元素
? api.createElementNS(i, tag)
: api.createElement(tag)
if (hash < dot) elm.setAttribute('id', sel.slice(hash + 1, dot)) // 添加ID
if (dotIdx > 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')) // 添加CLass
for (i = 0; i < cbs.create.length; ++i) cbs.create[i](emptyNode, vnode) // 执create钩子函数
if (is.array(children)) { // 判断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)) { // primitive 判断text节点是否有改变
api.appendChild(elm, api.createTextNode(vnode.text)) //
}
const hook = vnode.data!.hook // 执行对应的钩子
if (isDef(hook)) {
hook.create?.(emptyNode, vnode)
if (hook.insert) {
insertedVnodeQueue.push(vnode) // insertedVnodeQueue是插入到DOM所执行的函数,先暂存起来
}
}
} else { // 如果选择器sel是空值,那么插入的就是文本节点
vnode.elm = api.createTextNode(vnode.text!)
}
// 返回创建的DOM
return vnode.elm
}
3.6 removeVnodes/addVnode函数
- 作用:添加和移除节点
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) // 触发vnode destroy钩子函数
listeners = cbs.remove.length + 1 // listeners 用来防止内除重复删除dom
rm = createRmCb(ch.elm!, listeners) // 返回一个真实删除的函数是一个高阶函数,并且会在listeners(计数器)等于0的时候执行移除
for (let i = 0; i < cbs.remove.length; ++i) cbs.remove[i](ch, rm)
const removeHook = ch?.data?.hook?.remove // 使用用户传入的romove钩子函数
if (isDef(removeHook)) {
removeHook(ch, rm) // 用户在hook如果传入remove了钩子函数,需要手动调用给rm函数
} else {
rm()
}
} else { // Text node
api.removeChild(parentElm, ch.elm!) // 文本节点直接移除
}
}
}
}
// 添加系节点
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)
}
}
}
3.7 pachVnode
-
作用:对比新旧两个vnode差异,然后新DOM更新到页面上
-
-
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { // 第一过程:触发prepatch 和updata钩子函数 const hook = vnode.data?.hook // 获取用户传入的hook属性 hook?.prepatch?.(oldVnode, vnode) // 如果用户传入的prepatch函数就是 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函数 =》用户定义的updata函数 for (let i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) vnode.data.hook?.update?.(oldVnode, vnode) } // 第二过程 真正对比新旧vnode差异的地方 if (isUndef(vnode.text)) { // 判断是是否有文本节点,注意是很children互斥的 if (isDef(oldCh) && isDef(ch)) { // 新旧节点都有子节点就触发,yupdateChildren函数 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) } else if (isDef(ch)) { // 如果新节点有子元素, if (isDef(oldVnode.text)) api.setTextContent(elm, '') // 有文本就清空 addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) // 直接在老节点上添加子元素 } else if (isDef(oldCh)) { // 旧节点有子节点,新节点没有直接移除 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) } api.setTextContent(elm, vnode.text!) // 如果没有子节点就直接更新文本 } // 第三过程 触发postpatch钩子函数,获取最新的数据 hook?.postpatch?.(oldVnode, vnode) }
3.8 DOM Diff算法
- 真实DOM和虚拟DOM的区别
- 真实DOM对比每个节点,发现不同旧重新绘制整个页面
- 虚拟DOM进行频繁修改,然后一次性比较并修改真实DOM中需要改的部分,最后并在真实DOM中进行排版与重绘,减少过多DOM节点排版与重绘损耗
- 虚拟DOM有效降低大面积(真实DOM节点)的重绘与排版,因为最终与真实DOM比较差异,可以只渲染局部】
- 参考文章
- juejin.cn/post/694785…
- 注释DOM Diff是采用同级比较
3.9 updateChildren函数
- 理解key的作用(后续补充),如果没有key就最大程度复用当前的虚拟DOM
function updateChildren (parentElm: Node,
oldCh: VNode[],
newCh: VNode[],
insertedVnodeQueue: VNodeQueue) {
// 第一步 定义函数所用使用变量
let oldStartIdx = 0
let newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0] // 旧的开始节点
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx: KeyToIndexMap | undefined
let idxInOld: number
let elmToMove: VNode
let before: any
// 第二部 同级别比较
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { // 循环条件 结束标记位大于开始标记位置
// 处理节点为空情况
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode might have been moved left
} else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
} else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
// 比较开始和结束的4种情况
} else if (sameVnode(oldStartVnode, newStartVnode)) { //比较新旧开始位置。相同右移动
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) { //比较新旧结束位置。相同左移动
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldStartVnode.elm!, api.nextSibling(oldEndVnode.elm!)) // 斜向对比,然后再调换值相同位置
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 斜向对比结束
if (oldKeyToIdx === undefined) {
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 根据key找老索引。返回一个map对象、键名为key值为index
}
idxInOld = oldKeyToIdx[newStartVnode.key as string] // 用newVnode的key值去找oldKeyToIdx对象有没有对应值
if (isUndef(idxInOld)) { // New element
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
elmToMove = oldCh[idxInOld] // 取这个相同key值的老节点
if (elmToMove.sel !== newStartVnode.sel) { // 如果不相同也时重新创建
api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!)
} else {
patchVnode(elmToMove, newStartVnode, insertedVnodeQueue) // 递归调用pachVnode
oldCh[idxInOld] = undefined as any // 处理过的节点转成空
api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!) // 移动节点
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// 第三步 循环结束收尾工作
if (oldStartIdx <= oldEndIdx || newStartIdx <= newEndIdx) { // 处理新旧节点没遍历完的情况
if (oldStartIdx > oldEndIdx) {
// 老节点数组遍历完,新节点数组有剩余
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm // 参考节点
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) // 添加新虚拟DOM
} else {
// 新节点数组遍历完,老节点数组有剩余
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) // 移除虚拟DOm
}
}
}