导航
导航
文章目录󰁋
  1. 什么是 Virtual DOM
  2. 为什么使用 Virtual DOM
  3. 虚拟 DOM 的作用
  4. Virtual DOM 库
  5. Snabbdom 基本使用
    1. 创建项目
    2. 基本使用
    3. 模块
  6. Snabbdom 源码解析
    1. Snabbdom 的核心
    2. h 函数
    3. VNode
    4. snabbdom
    5. init
    6. patch
    7. createElm
    8. patchVnode
    9. updateChildren

关注作者公众号

和万千小伙伴一起学习

虚拟DOM原理分析

什么是 Virtual DOM

  1. Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象,因为不是真实的 DOM 对象,所以叫 Virtual DOM
let element = document.querySelector('#app') 
let s = ''
for (var key in element) {
s += key + ','
}
console.log(s)

// 打印结果 align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,aut ocapitalize,contentEditable,isContentEditable,inputMode,offsetParent,off setTop,offsetLeft,offsetWidth,offsetHeight,style,innerText,outerText,onc opy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,onc hange,onclick,onclose,oncontextmenu,oncuechange,ondblclick,ondrag,ondrag end,ondragenter,ondragleave,ondragover,ondragstart,ondrop,ondurationchan ge,onemptied,onended,onerror,onfocus,oninput,oninvalid,onkeydown,onkeypr ess,onkeyup,onload,onloadeddata,onloadedmetadata,onloadstart,onmousedown ,onmouseenter,onmouseleave,onmousemove,onmouseout,onmouseover,onmouseup, onmousewheel,onpause,onplay,onplaying,onprogress,onratechange,onreset,on resize,onscroll,onseeked,onseeking,onselect,onstalled,onsubmit,onsuspend ,ontimeupdate,ontoggle,onvolumechange,onwaiting,onwheel,onauxclick,ongot pointercapture,onlostpointercapture,onpointerdown,onpointermove,onpointe rup,onpointercancel,onpointerover,onpointerout,onpointerenter,onpointerl eave,onselectstart,onselectionchange,onanimationend,onanimationiteration ,onanimationstart,ontransitionend,dataset,nonce,autofocus,tabIndex,click ,focus,blur,enterKeyHint,onformdata,onpointerrawupdate,attachInternals,n amespaceURI,prefix,localName,tagName,id,className,classList,slot,part,at tributes,shadowRoot,assignedSlot,innerHTML,outerHTML,scrollTop,scrollLef t,scrollWidth,scrollHeight,clientTop,clientLeft,clientWidth,clientHeight ,attributeStyleMap,onbeforecopy,onbeforecut,onbeforepaste,onsearch,eleme ntTiming,previousElementSibling,nextElementSibling,children,firstElement Child,lastElementChild,childElementCount,onfullscreenchange,onfullscreen error,onwebkitfullscreenchange,onwebkitfullscreenerror,setPointerCapture ,releasePointerCapture,hasPointerCapture,hasAttributes,getAttributeNames ,getAttribute,getAttributeNS,setAttribute,setAttributeNS,removeAttribute ,removeAttributeNS,hasAttribute,hasAttributeNS,toggleAttribute,getAttrib uteNode,getAttributeNodeNS,setAttributeNode,setAttributeNodeNS,removeAtt ributeNode,closest,matches,webkitMatchesSelector,attachShadow,getElement sByTagName,getElementsByTagNameNS,getElementsByClassName,insertAdjacentE lement,insertAdjacentText,insertAdjacentHTML,requestPointerLock,getClien tRects,getBoundingClientRect,scrollIntoView,scroll,scrollTo,scrollBy,scr ollIntoViewIfNeeded,animate,computedStyleMap,before,after,replaceWith,re move,prepend,append,querySelector,querySelectorAll,requestFullscreen,web kitRequestFullScreen,webkitRequestFullscreen,createShadowRoot,getDestina tionInsertionPoints,ELEMENT_NODE,ATTRIBUTE_NODE,TEXT_NODE,CDATA_SECTION_ NODE,ENTITY_REFERENCE_NODE,ENTITY_NODE,PROCESSING_INSTRUCTION_NODE,COMME NT_NODE,DOCUMENT_NODE,DOCUMENT_TYPE_NODE,DOCUMENT_FRAGMENT_NODE,NOTATION _NODE,DOCUMENT_POSITION_DISCONNECTED,DOCUMENT_POSITION_PRECEDING,DOCUMEN T_POSITION_FOLLOWING,DOCUMENT_POSITION_CONTAINS,DOCUMENT_POSITION_CONTAI NED_BY,DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC,nodeType,nodeName,baseU RI,isConnected,ownerDocument,parentNode,parentElement,childNodes,firstCh ild,lastChild,previousSibling,nextSibling,nodeValue,textContent,hasChild Nodes,getRootNode,normalize,cloneNode,isEqualNode,isSameNode,compareDocu mentPosition,contains,lookupPrefix,lookupNamespaceURI,isDefaultNamespace ,insertBefore,appendChild,replaceChild,removeChild,addEventListener,remo veEventListener,dispatchEvent
  1. 可以使用 Virtual DOM 来描述真实 DOM
{
sel: "div",
data: {},
children: undefined,
text: "Hello Virtual DOM",
elm: undefined,
key: undefined
}

为什么使用 Virtual DOM

  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题,虽然有 jQuery 等库简化 DOM 操作,但是随着项目的复杂 DOM 操作复杂提升
  • 为了简化 DOM 的复杂操作于是出现了各种 MVVM 框架,MVVM 框架解决了视图和状态的同步问题
  • 为了简化视图的操作我们可以使用模板引擎,但是模板引擎没有解决跟踪状态变化的问题,于是Virtual DOM 出现了
  • Virtual DOM 的好处是当状态改变时不需要立即更新 DOM,只需要创建一个虚拟树来描述DOMVirtual DOM 内部将弄清楚如何有效(diff)的更新 DOM
  • 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
  • 通过比较前后两次状态的差异更新真实 DOM

虚拟 DOM 的作用

  • 维护视图和状态的关系
  • 复杂视图情况下提升渲染性能
  • 除了渲染 DOM 以外,还可以实现 SSR(Nuxt.js/Next.js)、原生应用(Weex/React Native)、小程序(mpvue/uni-app)等

Virtual DOM 库

  • Snabbdom
    • Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom
    • 通过模块可扩展
    • 源码使用 TypeScript 开发
    • 最快的 Virtual DOM 之一
  • virtual-dom

Snabbdom 基本使用

创建项目

# 创建项目目录
md snabbdom-demo
# 进入项目目录
cd snabbdom-demo
# 创建 package.json yarn init -y
# 本地安装 parcel
yarn add parcel-bundler

配置 package.jsonscripts

"scripts": {
"dev": "parcel index.html --open", "build": "parcel build index.html"
}

创建目录结构

yarn add snabbdom
import{init,h,thunk}from'snabbdom'

snabbdom 的核心仅提供最基本的功能,只导出了三个函数 init()h()thunk()

  • init() 是一个高阶函数,返回 patch()
  • h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
  • thunk() 是一种优化策略,可以在处理不可变数据时使用

注意:导入时候不能使用 import snabbdom from 'snabbdom'。原因:node_modules/src/snabbdom.ts 末尾导出使用的语法是 export 导出 API,没有使用 export default 导出默认输出

基本使用

例子1

import { h, init } from 'snabbdom'

// 1. hello world
// 参数:数组,模块
// 返回值:patch函数,作用对比两个vnode的差异更新到真实DOM
let patch = init([])
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串的话就是标签中的内容
let vnode = h('div#container.cls', {
hook: {
init (vnode) {
console.log(vnode.elm)
},
create (emptyVnode, vnode) {
console.log(vnode.elm)
}
}
}, 'Hello World')

let app = document.querySelector('#app')
// 第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode
// 第二个参数:VNode
// 返回值:VNde
let oldVnode = patch(app, vnode)

// 假设的时刻
vnode = h('div', 'Hello Snabbdom')

patch(oldVnode, vnode)

例子2

// 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);

例子3 debug-patchVnode

import { h, init } from 'snabbdom'

let patch = init([])

// 首次渲染
let vnode = h('div', 'Hello World')
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

// patchVnode 的执行过程
vnode = h('div', 'Hello Snabbdom')
patch(oldVnode, vnode)

例子4 debug-updateChildren

import { h, init } from 'snabbdom'

let patch = init([])

// 首次渲染
let vnode = h('ul', [
h('li', '首页'),
h('li', '视频'),
h('li', '微博')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

// updateChildren 的执行过程
vnode = h('ul', [
h('li', '首页'),
h('li', '微博'),
h('li', '视频')
])
patch(oldVnode, vnode)

例子5 debug-updateChildren-key

import { h, init } from 'snabbdom'

let patch = init([])

// 首次渲染
let vnode = h('ul', [
h('li', { key: 'a' }, '首页'),
h('li', { key: 'b' }, '视频'),
h('li', { key: 'c' }, '微博')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)

// updateChildren 的执行过程
vnode = h('ul', [
h('li', { key: 'a' }, '首页'),
h('li', { key: 'c' }, '微博'),
h('li', { key: 'b' }, '视频')
])
patch(oldVnode, vnode)

模块

Snabbdom 的核心库并不能处理元素的属性/样式/事件等,如果需要处理的话,可以使用模块

常用模块

官方提供了 6 个模块

  • attributes
    • 设置 DOM 元素的属性,使用 setAttribute ()
    • 处理布尔类型的属性
  • props
    • attributes 模块相似,设置 DOM 元素的属性 element[attr] = value
    • 不处理布尔类型的属性
  • class
    • 切换类样式
    • 注意:给元素设置类样式是通过 sel 选择器
  • dataset
    • 设置 data-* 的自定义属性 eventlisteners
    • 注册和移除事件
  • style
    • 设置行内样式,支持动画
    • delayed/remove/destroy

模块使用

模块使用步骤:

  • 导入需要的模块
  • init() 中注册模块
  • 使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移
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')

let oldVnode = patch(app, vnode)


vnode = h('div', 'hello')
patch(oldVnode, vnode)

Snabbdom 源码解析

Snabbdom 的核心

  • 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
  • init() 设置模块,创建 patch()
  • patch() 比较新旧两个 VNode
  • 把变化的内容更新到真实 DOM 树上

Snabbdom 源码

源码地址: https://github.com/snabbdom/snabbdom

src 目录结构

h 函数

  • h() 函数介绍: 在使用 Vue 的时候见过 h() 函数
new Vue({
router,
store,
render: h => h(App)
}).$mount('#app')
  • h() 函数最早见于 hyperscript,使用 JavaScript 创建超文本
  • Snabbdom 中的 h() 函数不是用来创建超文本,而是创建 VNode

函数重载

  • 概念
    • 参数个数或类型不同的函数
    • JavaScript 中没有重载的概念
    • TypeScript 中有重载,不过重载的实现还是通过代码调整参数
  • 重载的示意
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

// 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 {
let data: VNodeData = {}
let children: any
let text: any
let i: number
// 处理参数,实现重载的机制
if (c !== undefined) {
// 处理三个参数的情况
// sel、data、children/text
if (b !== null) {
data = b
}
if (is.array(c)) {
children = c
// 如果 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
// 如果 b 是字符串或者数字
} else if (is.primitive(b)) {
text = b
// 如果 b 是 VNode
} else if (b && b.sel) {
children = [b]
} else { data = b }
}
if (children !== undefined) {
// 处理 children 中的原始值(string/number)
for (i = 0; i < children.length; ++i) {
// 如果 child 是 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)
};

// 导出模块
export default h;

VNode

一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNodechildren 就是 Virtual DOM

源码位置: src/vnode.ts

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
is?: string // for custom elements v1
[key: string]: any // for any other 3rd party module
}

export interface VNode {
// 选择器
sel: string | undefined
// 节点数据:属性/样式/事件等
data: VNodeData | undefined
// 子节点,和 text 只能互斥
children: Array<VNode | string> | undefined
// 记录 vnode 对应的真实 DOM
elm: Node | undefined
// 节点中的内容,和 children 只能互斥
text: string | undefined
// 优化用
key: Key | undefined
}

export function vnode (sel: string | undefined,
data: any | undefined,
children: Array<VNode | string> | undefined,
text: string | undefined,
elm: Element | Text | undefined): VNode {
const key = data === undefined ? undefined : data.key
return { sel, data, children, text, elm, key }
}

snabbdom

  • patch(oldVnode, newVnode)
  • 打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点
  • 对比新旧 VNode 是否相同节点(节点的 keysel 相同)
  • 如果不是相同节点,删除之前的内容,重新渲染
  • 如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnodetext 不同,直接更 新文本内容
  • 如果新的 VNodechildren,判断子节点是否有变化,判断子节点的过程使用的就是 diff 算法
  • diff 过程只进行同层级比较

init

  • 功能: init(modules, domApi),返回 patch()函数(高阶函数)
  • 为什么要使用高阶函数?
    • 因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如: modules/domApi/cbs
    • 通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而 不需要重新创建
  • init() 在返回 patch() 之前,首先收集了所有模块中的钩子函数存储到 cbs 对象中

源码位置:src/init.ts

export function init (modules: Array<Partial<Module>>, domApi?: DOMAPI) {
let i: number
let j: number
const cbs: ModuleHooks = {
create: [],
update: [],
remove: [],
destroy: [],
pre: [],
post: []
}

const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi

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)
}
}
}
...

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]()

if (!isVnode(oldVnode)) {
oldVnode = emptyNodeAt(oldVnode)
}

if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue)
} else {
elm = oldVnode.elm!
parent = api.parentNode(elm) as Node

createElm(vnode, insertedVnodeQueue)

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
}
}

patch

  • 功能:
    • 传入新旧 VNode,对比差异,把差异渲染到 DOM
    • 返回新的 VNode,作为下一次 patch()oldVnode
  • 执行过程:
    • 首先执行模块中的钩子函数 pre
    • 如果 oldVnodevnode 相同(keysel 相同)
      • 调用 patchVnode(),找节点的差异并更新 DOM
    • 如果 oldVnodeDOM 元素
      • DOM 元素转换成 oldVnode
      • 调用 createElm()vnode 转换为真实 DOM,记录到 vnode.elm
      • 把刚创建的 DOM 元素插入到 parent
      • 移除老节点
      • 触发用户设置的 create 钩子函数

源码位置:src/snabbdom.ts

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
// 触发 init/create 钩子函数,创建 DOM
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

  • 功能:

    • createElm(vnode, insertedVnodeQueue),返回创建的 DOM 元素
    • 创建 vnode 对应的 DOM 元素
  • 执行过程:

    • 首先触发用户设置的 init 钩子函数
    • 如果选择器是!,创建评论节点
    • 如果选择器为空,创建文本节点
    • 如果选择器不为空
      • 解析选择器,设置标签的 idclass 属性
      • 执行模块的 create 钩子函数
      • 如果 vnodechildren,创建子 vnode 对应的 DOM,追加到 DOM
      • 如果 vnodetext 值是 string/number,创建文本节点并追击到 DOM
      • 执行用户设置的 create 钩子函数
      • 如果有用户设置的 insert 钩子函数,把 vnode 添加到队列中
function createElm (vnode: VNode, insertedVnodeQueue: VNodeQueue): Node {
let i: any
let data = vnode.data
if (data !== undefined) {
// 执行用户设置的 init 钩子函数
const init = data.hook?.init
if (isDef(init)) {
init(vnode)
data = vnode.data
}
}
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
// 如果选择器不为空
// 解析选择器
// 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, data)
: api.createElement(tag, data)
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
}

patchVnode

  • 功能:
    • patchVnode(oldVnode, vnode, insertedVnodeQueue)
    • 对比 oldVnodevnode 的差异,把差异渲染到 DOM
  • 执行过程:
    • 首先执行用户设置的 prepatch 钩子函数
    • 执行 create 钩子函数
      • 首先执行模块的 create钩子函数
      • 然后执行用户设置的 create 钩子函数
    • 如果 vnode.text 未定义
      • 如果 oldVnode.childrenvnode.children 都有值
        • 调用 updateChildren()
        • 使用 diff 算法对比子节点,更新子节点
      • 如果 vnode.children 有值, oldVnode.children 无值
        • 清空 DOM 元素
        • 调用 addVnodes() ,批量添加子节点
      • 如果 oldVnode.children 有值, vnode.children 无值
        • 调用 removeVnodes() ,批量移除子节点
      • 如果 oldVnode.text 有值
        • 清空 DOM 元素的内容
    • 如果设置了 vnode.text 并且和和 oldVnode.text 不等
      • 如果老节点有子节点,全部移除
      • 设置 DOM 元素的 textContentvnode.text
    • 最后执行用户设置的 postpatch 钩子函数
function patchVnode (oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) {
const hook = vnode.data?.hook
// 首先执行用户设置的 prepatch 钩子函数
hook?.prepatch?.(oldVnode, vnode)
const elm = vnode.elm = oldVnode.elm!
const oldCh = oldVnode.children as VNode[]
const ch = vnode.children as VNode[]
// 如果新老 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 算法对比子节点,更新子节点
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)
} else if (isDef(oldCh)) {
// 如果老节点有children,新节点没有children
// 批量移除子节点
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果老节点有 text,清空 DOM 元素
api.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果没有设置 vnode.text
if (isDef(oldCh)) {
// 如果老节点有 children,移除
removeVnodes(elm, oldCh, 0, oldCh.length - 1)
}
// 设置 DOM 元素的 textContent 为 vnode.text
api.setTextContent(elm, vnode.text!)
}
// 最后执行用户设置的 postpatch 钩子函数
hook?.postpatch?.(oldVnode, vnode)
}

updateChildren

  • 功能:
    • diff 算法的核心,对比新旧节点的 children,更新 DOM
  • 执行过程:
    • 要对比两棵树的差异,我们可以取第一棵树的每一个节点依次和第二课树的每一个节点比 较,但是这样的时间复杂度为 O(n^3)
    • DOM 操作的时候我们很少很少会把一个父节点移动/更新到某一个子节点
    • 因此只需要找同级别的子节点依次比较,然后再找下一级别的节点比较,这样算法的时间复 杂度为 O(n)

  • 在进行同级别节点比较的时候,首先会对新老节点数组的开始和结尾节点设置标记索引,遍 历的过程中移动索引
  • 在对开始和结束节点比较的时候,总共有四种情况
    • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
    • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
    • oldStartVnode / oldEndVnode (旧开始节点 / 新结束节点)
    • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点)

  • 开始节点和结束节点比较,这两种情况类似
    • oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
    • oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
  • 如果 oldStartVnodenewStartVnodesameVnode (keysel 相同)
    • 调用 patchVnode() 对比和更新节点
    • 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++

  • oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同
    • 调用 patchVnode() 对比和更新节点
    • oldStartVnode 对应的 DOM 元素,移动到右边
      • 更新索引

  • oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同
    • 调用 patchVnode() 对比和更新节点
    • oldEndVnode 对应的 DOM 元素,移动到左边
    • 更新索引

  • 如果不是以上四种情况
    • 遍历新节点,使用 newStartNodekey 在老节点数组中找相同节点
    • 如果没有找到,说明 newStartNode 是新节点
      • 创建新节点对应的 DOM 元素,插入到 DOM 树中
    • 如果找到了
      • 判断新节点和找到的老节点的 sel 选择器是否相同
      • 如果不相同,说明节点被修改了
        • 重新创建对应的 DOM 元素,插入到 DOM 树中
      • 如果相同,把 elmToMove 对应的 DOM 元素,移动到左边

  • 循环结束
    • 当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束
    • 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束
  • 如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边

  • 如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批 量删除

支持一下
扫一扫,支持poetries
  • 微信扫一扫
  • 支付宝扫一扫