Virtual DOM的实现原理

您所在的位置:网站首页 什么是virtualDOM Virtual DOM的实现原理

Virtual DOM的实现原理

2022-04-18 11:35| 来源: 网络整理| 查看: 265

文章说明:本文章为拉钩大前端训练营所做笔记和心得,若有不当之处,还望各位指出与教导,谢谢 !

Virtual DoM

什么是Virtual DoM

是由普通的JS对象来描述DOM对象,因为不是真实的DOM对象,所以叫做Virtual DOM。真实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 可以使用Virtual DOM来描述真实DOM,实例: { sel: "div", //标签 data: {}, children: undefined, text: "Hello Virtual DOM", //标签内的文本 elm: undefined, key: undefined }

创建虚拟DOM的开销要比创建真实DOM的开销小很多。

为什么使用Virtual DOM

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

虚拟DOM的作用

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

在这里插入图片描述 Virtual DOM库

Snabbdom 1.Vue 2.x 内部使用的 Virtual DOM 就是改造的 Snabbdom 2.大约 200 SLOC(single line of code) 3.通过模块可扩展 4.源码使用 TypeScript 开发 5.最快的 Virtual DOM 之一virtual-dom Snabbdom

基本使用 创建项目:

打包工具为了方便使用parcel创建项目,并安装parcel # 创建项目目录 $ md snabbdom-demo # 进入项目目录 $ cd snabbdom-demo # 创建 package.json $ yarn init -y # 本地安装 parcel $ yarn add parcel-bundler 配置package.json的scripts { "scripts": { "dev": "parcel index.html --open", "build": "parcel build index.html" } } 创建目录结构: 在这里插入图片描述

导入snabbdom Snabbdom 文档

看文档的意义 1.学习任何一个库都要先看文档 2.通过文档了解库的作用 3.看文档中提供的示例,自己快速实现一个 demo 4.通过文档查看 API 的使用文档地址 GitHub地址 中文翻译

安装Snabbdom

# 版本 0.7.4 $ yarn add snabbdom

导入 Snabbdom - Snabbdom 的官网 demo 中导入使用的是 commonjs 模块化语法,我们使用更流行的 ES6 模块化的语法 import; - 关于模块化的语法请参考阮一峰老师的 Module 的语法; - ES6 模块与 CommonJS 模块的差异

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 导出默认输出 在这里插入图片描述 基本案例 index.html

snabbdom-demo

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) // 2.div中放置子元素 h1,p

02-basicusage.js:

// 2.div 中放置子元素 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中的模块

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

常用模块:

官方提供了 6 个模块:

attributes

设置 DOM 元素的属性,使用 setAttribute () 处理布尔类型的属性

props 和 attributes 模块相似,设置 DOM 元素的属性 element[attr] = value 不处理布尔类型的属性

class 切换类样式 注意:给元素设置类样式是通过 sel 选择器

dataset 设置 data-* 的自定义属性

eventlisteners 注册和移除事件

style 设置行内样式,支持动画 delayed/remove/destroy

模块使用:

模块使用步骤: 1.导入需要的模块 2.init()中注册模块 3.使用 h() 函数创建 VNode 的时候,可以把第二个参数设置为对象,其他参数往后移

案例实现: index.html:

snabbdom-demo 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()函数创建JavaScript对象(Vnode)描述真实DOMinit() 设置模块,创建 patch()patch() 比较新旧两个 VNode把变化的内容更新到真实 DOM 树上

Snabbdom 源码

源码地址src目录结构 │ h.ts h() 函数,用来创建 VNode │ hooks.ts 所有钩子函数的定义 │ htmldomapi.ts 对 DOM API 的包装 │ is.ts 判断数组和原始值的函数 │ jsx-global.d.ts jsx 的类型声明文件 │ jsx.ts 处理 jsx │ snabbdom.bundle.ts 入口,已经注册了模块 │ snabbdom.ts 初始化,返回 init/h/thunk │ thunk.ts 优化处理,对复杂视图不可变值得优化 │ tovnode.ts DOM 转换成 VNode │ vnode.ts 虚拟节点定义 │ ├─helpers │ attachto.ts 定义了 vnode.ts 中 AttachData 的数据结构 │ └─modules 所有模块定义 attributes.ts class.ts dataset.ts eventlisteners.ts hero.ts example 中使用到的自定义钩子 module.ts 定义了模块中用到的钩子函数 props.ts style.ts

h函数介绍

作用:创建VNode对象Vue中的h函数:

在这里插入图片描述

h函数最早见于hyperscript,使用JavaScript创建超文本

函数重载:

参数个数或类型不同的函数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): VNode; export function h(sel: string, children: VNodeChildren): VNode; export function h(sel: string, data: VNodeData, children: VNodeChildren): VNode; export function h(sel: any, b?: any, c?: any): VNode { var data: VNodeData = {}, children: any, text: any, i: number; // 处理参数,实现重载的机制 if (c !== undefined) { // 处理三个参数的情况 // sel、data、children/text data = b; if (is.array(c)) { children = c; } // 如果 c 是字符串或者数字 else if (is.primitive(c)) { text = c; } // 如果 c 是VNode else if (c && c.sel) { children = [c]; } } else if (b !== undefined) { // 处理两个参数的情况 // 如果 b 是数组 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 // 如果是 svg,添加命名空间 addNS(data, children, sel); } // 返回 VNode return vnode(sel, data, children, text, undefined); }; // 导出模块 export default h;

VNode

一个 VNode 就是一个虚拟节点用来描述一个 DOM 元素,如果这个 VNode 有 children 就是Virtual DOM源码位置:src/vnode.ts // interface 接口, // 目的:约束实现这个接口的所有对象都拥有相同的属性 export interface VNode { // 选择器 sel: string | undefined; // 模块,节点数据:属性/样式/事件等 data: VNodeData | undefined; // 子节点,和 text 互斥 children: Array | undefined; // 记录 vnode 对应的真实 DOM elm: Node | undefined; // 节点中的内容,和 children 互斥 text: string | undefined; // 优化用 key: Key | undefined; } export function vnode(sel: string | undefined, data: any | undefined, children: Array | undefined, text: string | undefined, elm: Element | Text | undefined): VNode { let key = data === undefined ? undefined : data.key; return {sel, data, children, text, elm, key}; } export default vnode;

patch整体过程分析

patch(oldVnode, newVnode)打补丁,把新节点中变化的内容渲染到真实 DOM,最后返回新节点作为下一次处理的旧节点对比新旧 VNode 是否相同节点(节点的 key 和 sel 相同,key是节点的唯一标识,sel是节点的选择器)如果不是相同节点,删除之前的内容,重新渲染如果是相同节点,再判断新的 VNode 是否有 text,如果有并且和 oldVnode 的 text 不同,直接更新文本内容如果新的 VNode 有 children,判断子节点是否有变化,判断子节点的过程使用的就是 diffff 算法diffff 过程只进行同层级比较 在这里插入图片描述 init 函数功能:init(modules,domApi),返回patch()函数(高阶函数)为什么要使用高阶函数? 1.因为 patch() 函数再外部会调用多次,每次调用依赖一些参数,比如:modules/domApi/cbs 2.通过高阶函数让 init() 内部形成闭包,返回的 patch() 可以访问到 modules/domApi/cbs,而不需要重新创建源码位置:src/init.ts // 存储了钩子函数的名字 const hooks: (keyof Module)[] = ['create', 'update', 'remove', 'destroy', 'pre', 'post']; // domAPI 执行DOM操作 export function init(modules: Array, domApi?: DOMAPI) { let i: number, j: number, cbs = ({} as ModuleHooks); // 初始化转换虚拟节点的 api const api: DOMAPI = domApi !== undefined ? domApi : htmlDomApi; // 把传入的所有模块的钩子函数,统一存储到 cbs 对象中 // 最终构建的 cbs 对象的形式 cbs = { create: [], update: [], ... } for (i = 0; i // modules 传入的模块数组 // 获取模块中的 hook 函数 // hook = modules[0][create]... const hook = modules[j][hooks[i]]; if (hook !== undefined) { // 把获取到的hook函数放入到 cbs 对应的钩子函数数组中 (cbs[hooks[i]] as Array).push(hook); } } } ...... ...... ...... // init 内部返回 patch 函数,把vnode渲染成真实 dom,并返回vnode // 高阶函数,在一个函数内部返回一个函数 return function patch(oldVnode: VNode | Element, vnode: VNode): VNode { ..... }; }

patch函数

1.传入新旧VNode,对比差异,把差异渲染到DOM 2.返回新的VNode,作为下一次patch()的oldVnode 执行过程: 1.首先执行模块中的钩子函数 pre 2.如果 oldVnode 和 vnode 相同(key 和 sel 相同)

调用 patchVnode(),找节点的差异并更新 DOM

3.如果oldVNode是DOM元素

把 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 钩子函数,pre 预处理 for (i = 0; i // 找节点的差异并更新 DOM patchVnode(oldVnode, vnode, insertedVnodeQueue); } else { // 如果新旧节点不同,vnode 创建对应的 DOM // 获取当前的 DOM 元素 elm = oldVnode.elm!; parent = api.parentNode(elm); // 创建 vnode 对应的 DOM 元素,并触发 init/create 钩子函数 createElm(vnode, insertedVnodeQueue); if (parent !== null) { // 如果父节点不为空,把 vnode 对应的 DOM 插入到文档中 // ! typescript 语法,告诉编译器vnode.elm是百分百有值的 api.insertBefore(parent, vnode.elm!, api.nextSibling(elm)); // 移除老节点 removeVnodes(parent, [oldVnode], 0, 0); } } // 执行用户设置的 insert 钩子函数 for (i = 0; i let i: any, data = vnode.data; if (data !== undefined) { // 执行用户设置的 init 的钩子函数 const init = data.hook?.init; if (isDef(init)) { init(vnode); data = vnode.data; } } // 把 vnode 转换成真实 DOM 对象(没有渲染到页面) let children = vnode.children, 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; // data.ns 是否有命名空间 const elm = vnode.elm = isDef(data) && isDef(i = (data as VNodeData).ns) ? api.createElementNS(i, tag) : api.createElement(tag); if (hash 0) elm.setAttribute('class', sel.slice(dot + 1).replace(/\./g, ' ')); // 执行模块的 create 钩子函数 for (i = 0; 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)对比 oldVnode 和 vnode 的差异,把差异渲染到 DOM

执行过程:

首先执行用户设置的 prepatch 钩子函数

执行 create 钩子函数

首先执行模块的 create 钩子函数 然后执行用户设置的 create 钩子函数

如果 vnode.text 未定义:

如果oldVnode.children 和 vnode.children 都有值

调用 updateChildren() 使用 diff 算法对比子节点,更新子节点

vnode.children 有值, oldVnode.children 无值

清空 DOM 元素 调用 addVnodes() ,批量添加子节点

如果 oldVnode.children 有值, vnode.children 无值

调用 removeVnodes() ,批量移除子节点

如果oldVnode.text有值

清空 DOM 元素的内容

如果设置了 vnode.text 并且和和 oldVnode.text 不等

如果老节点有子节点,全部移除 设置 DOM 元素的 textContent 为 vnode.text

最后执行用户设置的 postpatch 钩子函数

源码位置:src/snabbdom.ts

function patchVnode(oldVnode: VNode, vnode: VNode, insertedVnodeQueue: VNodeQueue) { const hook = vnode.data?.hook; // 首先执行用户设置的 prepatch 钩子函数 hook?.prepatch?.(oldVnode, vnode); const elm = vnode.elm = oldVnode.elm!; let oldCh = oldVnode.children as VNode[]; let ch = vnode.children as VNode[]; // 如果新老 vnode 相同,直接返回 if (oldVnode === vnode) return; if (vnode.data !== undefined) { // 执行模块的 update 钩子函数 for (i = 0; i // 使用 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 (旧结束节点 / 新结束节点)

如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同)

调用 patchVnode() 对比和更新节点 把旧开始和新开始索引往后移动 oldStartIdx++ / oldEndIdx++

在这里插入图片描述

oldStartVnode / newEndVnode (旧开始节点 / 新结束节点) 相同

调用 patchVnode() 对比和更新节点 把 oldStartVnode 对应的 DOM 元素,移动到右边 更新索引: 在这里插入图片描述

oldEndVnode / newStartVnode (旧结束节点 / 新开始节点) 相同

调用 patchVnode() 对比和更新节点; 把 oldEndVnode 对应的 DOM 元素,移动到左边; 更新索引; 在这里插入图片描述

如果不是以上四种情况

遍历新节点,使用 newStartNode 的 key 在老节点数组中找相同节点; 如果没有找到,说明 newStartNode 是新节点: 1、创建新节点对应的 DOM 元素,插入到 DOM 树中 如果找到了: 1.判断新节点和找到的老节点的 sel 选择器是否相同; 2.如果不相同,说明节点被修改了: 重新创建对应的 DOM 元素,插入到 DOM 树中 3.如果相同,把 elmToMove 对应的 DOM 元素,移动到左边; 在这里插入图片描述

循环结束

当老节点的所有子节点先遍历完 (oldStartIdx > oldEndIdx),循环结束 新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),循环结束

如果老节点的数组先遍历完(oldStartIdx > oldEndIdx),说明新节点有剩余,把剩余节点批量插入到右边 在这里插入图片描述

如果新节点的数组先遍历完(newStartIdx > newEndIdx),说明老节点有剩余,把剩余节点批量删除 在这里插入图片描述

源码位置:src/snabbdom.ts

// VNode 的核心 function updateChildren(parentElm: Node, oldCh: Array, newCh: Array, insertedVnodeQueue: VNodeQueue) { // 新老开始节点的索引 let oldStartIdx = 0, 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: any; let idxInOld: number; let elmToMove: VNode; let before: any; // 对比所有的新旧子节点 while (oldStartIdx // 节点为空移动索引 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]; // 比较开始和结束节点的四种情况 } else if (sameVnode(oldStartVnode, newStartVnode)) { // 1. 比较老的开始节点和新的开始节点 patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue); oldStartVnode = oldCh[++oldStartIdx]; newStartVnode = newCh[++newStartIdx]; } else if (sameVnode(oldEndVnode, newEndVnode)) { // 2. 比较老的结束节点和新的结束节点 patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue); oldEndVnode = oldCh[--oldEndIdx]; newEndVnode = newCh[--newEndIdx]; } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // 3. 比较老的开始节点和新的结束节点 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 // 4. 比较老的结束节点和新的开始节点 patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue); api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!); oldEndVnode = oldCh[--oldEndIdx]; newStartVnode = newCh[++newStartIdx]; } else { // 开始节点和结束节点都不相同 // 使用 newStartNode 的 key 在老的节点数组中找相同节点 // 先设置记录 key 和 index 的对象 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); } // 遍历 newStartVnode,从老的节点中找相同 key 的 oldVnode 的索引 idxInOld = oldKeyToIdx[newStartVnode.key as string]; // 如果是新的 vnode if (isUndef(idxInOld)) { // New element // 如果没找到,newStartVnode 是新节点 // 创建元素插入 DOM 树 api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!); // 重新给 newStartVnode 赋值,指向下一个新节点 newStartVnode = newCh[++newStartIdx]; } else { // 如果找到相同 key 相同的老节点,记录到 elmToMove 遍历 elmToMove = oldCh[idxInOld]; if (elmToMove.sel !== newStartVnode.sel) { // 如果新旧节点的选择器不同 // 创建新开始节点对应的 DOM 元素,插入到 DOM 树中 api.insertBefore(parentElm, createElm(newStartVnode, insertedVnodeQueue), oldStartVnode.elm!); } else { // 如果相同,patchVnode() // 把 elmToMove 对应的 DOM 元素,移动到左边 patchVnode(elmToMove, newStartVnode, insertedVnodeQueue); oldCh[idxInOld] = undefined as any; api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!); } // 重新给 newStartVnode 赋值,指向下一个新节点 newStartVnode = newCh[++newStartIdx]; } } } // 循环结束,老节点数组先遍历完成或者新节点数组先遍历完成 if (oldStartIdx // 如果老节点数组先遍历完成,说明有新的节点剩余 // 把剩余的新节点都插入到右边 before = newCh[newEndIdx+1] == null ? null : newCh[newEndIdx+1].elm; addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue); } else { // 如果新节点数组先遍历完成,说明老节点有剩余 // 批量删除老节点 removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx); } } }

调试 updateChildren 在这里插入图片描述 在这里插入图片描述 调试带 key 的情况 在这里插入图片描述 总结 通过以上调试 updateChildren,我们发现不带 key 的情况需要进行两次 DOM 操作,带 key 的情况只需要更新一次 DOM 操作(移动 DOM 项),所以带 key 的情况可以减少 DOM 的操作,如果 li 中的子项比较多,更能体现出带 key 的优势。

Modules 源码 patch() -> patchVnode() -> updateChildren()Snabbdom 为了保证核心库的精简,把处理元素的属性/事件/样式等工作,放置到模块中模块可以按照需要引入模块的使用可以查看官方文档模块实现的核心是基于 Hooks

Hooks

预定义的钩子函数的名称源码位置:src/hooks.ts export interface Hooks { // patch 函数开始执行的时候触发 pre?: PreHook; // createElm 函数开始之前的时候触发 // 在把 VNode 转换成真实 DOM 之前触发 init?: InitHook; // createElm 函数末尾调用 // 创建完真实 DOM 后触发 create?: CreateHook; // patchVnode 函数末尾执行 // 真实 DOM 添加到 DOM 树中触发 insert?: InsertHook; // patchVnode 函数开头调用 // 开始对比两个 VNode 的差异之前触发 prepatch?: PrePatchHook; // patchVnode 函数开头调用 // 两个 VNode 对比过程中触发,比 prepatch 稍晚 update?: UpdateHook; // patchVnode 的最末尾调用 // 两个 VNode 对比结束执行 postpatch?: PostPatchHook; // removeVnodes -> inVokeDestroyHook 中调用 // 在删除元素之前触发,子节点的 destroy 也被触发 destroy?: DestroyHook; // removeVnodes 中调用 // remove?: RemoveHook; post?: PostHook; }

Modules 模块文件的定义 Snabbdom 提供的所有模块在:src/modules 文件夹下,主要模块有:

attributes.ts 使用 setAttribute/removeAttribute 操作属性 能够处理 boolean 类型的属性class.ts 切换类样式dataset.ts 操作元素的 data-* 属性eventlisteners.ts 注册和移除事件module.ts 定义模块遵守的钩子函数props.ts 和 attributes.ts 类似,但是是使用 elm[attrName] = value 的方式操作属性style.ts 操作行内样式 可以使动画更平滑hero.ts 自定义的模块,examples/hero 示例中使用

attributes.ts

模块到出成员 export const attributesModule = { create: updateAttrs, update: updateAttrs } as Module; export default attributesModule; updateAttrs 函数功能 更新节点属性 如果节点属性值是 true 设置空置 如果节点属性值是 false 移除属性updateAttrs 实现 function updateAttrs(oldVnode: VNode, vnode: VNode): void { var key: string, elm: Element = vnode.elm as Element, oldAttrs = (oldVnode.data as VNodeData).attrs, attrs = (vnode.data as VNodeData).attrs; // 新老节点没有 attrs 属性,返回 if (!oldAttrs && !attrs) return; // 新老节点的 attrs 属性相同,返回 if (oldAttrs === attrs) return; oldAttrs = oldAttrs || {}; attrs = attrs || {}; // update modified attributes, add new attributes // 遍历新节点的属性 for (key in attrs) { // 新老节点的属性值 const cur = attrs[key]; const old = oldAttrs[key]; // 如果新老节点的属性值不同 if (old !== cur) { // 布尔类型值的处理 if (cur === true) { elm.setAttribute(key, ""); } else if (cur === false) { elm.removeAttribute(key); } else { // xChar -> x // if (key.charCodeAt(0) !== xChar) { elm.setAttribute(key, cur); } else if (key.charCodeAt(3) === colonChar) { // colonChar -> : // Assume xml namespace elm.setAttributeNS(xmlNS, key, cur); } else if (key.charCodeAt(5) === colonChar) { // Assume xlink namespace // elm.setAttributeNS(xlinkNS, key, cur); } else { elm.setAttribute(key, cur); } } } } // remove removed attributes // use `in` operator since the previous `for` iteration uses it (.i.e. add even attributes with undefined value) // the other option is to remove all attributes with value == undefined // 如果老节点的属性在新节点中不存在,移除 for (key in oldAttrs) { if (!(key in attrs)) { elm.removeAttribute(key); } } }


【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3