“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第3篇文章,点击查看活动详情”
1. 虚拟DOM的由来
DOM(Document Object Model,文档对象模型)是 HTML 和 XML 文档的编程接口。它提供了对文档的结构化的表述,并定义了一种方式可以使从程序中对该结构进行访问,从而改变文档的结构,样式和内容。
更简单的说,浏览器渲染页面就是基于DOM实现的,它将web页面和脚本或程序语言连接起来了。
1.1 真实DOM的存在形式
align,title,lang,translate,dir,hidden,accessKey,draggable,spellcheck,autocapitalize,contentEditable
,isContentEditable,inputMode,offsetParent,offsetTop,offsetLeft,offsetwidth,offsetHeight,style,
innerText,outerText,oncopy,oncut,onpaste,onabort,onblur,oncancel,oncanplay,oncanplaythrough,
onchange, onclick,onclose,oncontextmenu,oncuechange,ondblclick, ondrag, ondragend, ondragenter,
ondragleave, ondr agover,ondragstart,ondrop,ondurationchange,onemptied, onended, onerror,
onfocus, oninput, oninvalid, onk eydown, onkeypress, onkeyup, onload, onloadeddata,
onloadedmetadata, onloadstart, onmousedown, onmouseente r, onmouseleave, onmousemove,
onmouseout, onmouseover, onmouseup, onmousewheel, onpause, onplay, onplay, onplaying,
onprogress, onratechange, onreset, onresize, onscroll, onseeked, onseeking, onselect,
onstalled, onsubmit, onsuspend, ontimeupdate, ontoggle, onvolumechange, onkaiting, onwheel,
onauxclick, ongotpointercapture, onlostpointercapture, onpointerdown, onpointermove,
onpointerup, onpointercancel, onpointerover, onpointero ut, onpointerenter, onpointerleave,
onselectstart, onselectionchange, onanimationend, onanimationiterati on, onanimationstart,
ontransitionend, dataset, nonce, autofocus, tabIndex, click, focus, blur, enterKeyHint,
onformdata, onpointerrawupdate, attachInternals, namespaceURI, prefix, localName, tagName, id,
className, classList, slot,part, attributes, shadowRoot, assignedSlot, innerHTML, outerHTML,
scrollTop, scrollLeft,scrollwidth, scrollHeight, clientTop, clientLeft, clientwidth,
clientHeight, attributeStyleMap, onbeforecopy, onbeforecut, onbeforepaste, onsearch,
elementTiming, previousElementSibling, nextElementSibling, children, firstElementChild,
LastElementChild, childElementCount, onfullscreenchange, onfullscreenerror,
onwebkitfullscreenchange, onwebkitfullscreenerror, setPointerCapture, releasePointerCapture,
hasPointerCapture, hasAttributes, getAttributeNames, getAttribute, getAttributeNS,
setAttribute, setAttributeNS, removeAttribute, removeAttributeNS, hasAttribute, hasAttributeNS,
toggleAttribute, getAttributeNode, getAttributeNodeNS, setAttributeNode, setAttributeNodeNS,
removeAttributeNode,closest,matches,webkitMatchesSelector, attachShadow, getElementsByTagName,
getElementsByTagNameNS,getElementsByClassName,insertAdjacentElement,insertAdjacentText,
insertAdjacentHTML, requestPointerLock,getClientRects,getBoundingClientRect,scrollIntoView,scroll,
scrollTo,scrollBy,scrollIntoViewIfNeeded, animate, computedStyleMap, before, after, replaceWith,
remove, prepend, append, querySelector, querySelectorAll, requestFullscreen, webkitRequestFullScreen,
webkitRequestFullscreen, createShadowRoot, getDestinationInsertionPoints, ELEMENT_NODE,
ATTRIBUTE_NODE, TEXT_NODE, CDATA_SECTION_NODE, ENTITY_REFERENCE_NODE, ENTITY_NODE,
PROCESSING_INSTRUCTION_NODE, COMMENT_NODE, DOCUMENT_NODE, DOCUMENT_TYPE_NODE,
DOCUMENT_FRAGMENT_NODE, NOTATION_NODE, DOCUMENT_POSITION_DISCONNECTED,
DOCUMENT_POSITION_PRECEDING, DOCUMENT_POSITION_FOLLOWING, DOCUMENT_POSI TION_CONTAINS,
DOCUMENT_POSITION_CONTAINED_BY, DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC, nodeType, nodeName,
baseURI, isConnected, ownerDocument, parentNode, parentElement, childNodes, firstChild,
lastChild, previoussibling, nextSibling, nodeValue, textContent, hasChildNodes, getRootNode,
normalize, cloneNode, isEqualNode, isSameNode, compareDocumentPosition, contains, lookupPrefix,
lookupNamespaceURI, isDefaultNamespace, insertBefore, appendChild, replaceChild, removeChild,
addEventListener, removeEventListener, dispatchEvent...
大家瞅一瞅,dom中内容太多了,多到有点密集恐惧症了;因此对于操作dom时消耗的性能有多大可想而知,所以对于dom操作是有严格的流程(优化手段):
- 尽量避免或减少改变DOM(增删改)
- 尽量减少改变DOM的样式类
- 尽量进行批量修改DOM(最主要的操作方式)
但实际上每次开发时都考虑批量修改DOM,会大大降低效率, 因此通过封装这个操作非常重要,且保证了程序的统一性,使得开发者更加注重于业务开发!!(**注意:**如果不需要修改DOM就不要考虑下面的了)
封装批量修改DOM操作需要从多个角度进行分析:
- 如何判断需要被操作的DOM元素 ==> 新老元素进行比较,如果相同就不处理,如果不同就处理--> diff算法
- 什么时候自动批量操作DOM元素 ==> 使用队列存储更新操作,当没有更多的更新后再执行批量操作DOM元素
- 最好还要手动执行批量操作DOM元素 ==> 提供forceUpdate/forceRender等的操作
针对新老元素比较,如果使用真实dom对象,需要比对的东西太多(无效属性太多),所以采用一个新的简单对象来描述真实dom——这就是虚拟DOM。
1.2 虚拟DOM的形式
虚拟DOM对象是用来描述HTML和CSS的简单对象,而HTML从词法出发,包括标签、属性、文本、注释、左/右尖括号等,考虑到子元素和实际意义的内容,虚拟DOM需要至少包含:
- 标签:tag
- 属性:data
- 文本:text
- 子元素:children
- 父元素:children
- 唯一标识:key
interface IVnodeData{
style: {
[key: string]: any;
},
[key: string]: any;
}
interface IVnode {
tag?: string;
data: IVnodeData | undefined;
text?: string;
children?: Array<IVnode> | null;
parent: IVnode | undefined | null;
key: string | number | undefined;
}
1.3 虚拟DOM的特点
github中Virtual-dom的动机描述:
- 虚拟DOM可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态差异更新真实DOM
虚拟DOM的作用有:
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 跨平台
- 浏览器平台渲染DOM
- 服务端渲染SSR (Nuxt.js/Next.js)
- 原生应用(Weex/React Native)
- 小程序(mpvue/uni-app)等
2. 虚拟DOM实现原理
虚拟DOM相当于在js和真实DOM之间加了一层缓存,利用dom diff算法避免了不必要的DOM操作,从而提升性能。
具体的实现原理:
- 用JavaScript对象结构描述DOM树的结构;然后用这个树构建出一个真正的DOM,插入到文档中
- 当状态变化后,重新构建一颗新的对象树。通过diff算法,比较新旧虚拟DOM树的差异;
- 根据差异,对真实的DOM进行增、删、改
3. 虚拟DOM实现流程
虚拟DOM的原理十分简单,即首先获取虚拟DOM对象,然后基于虚拟DOM对象进行比对获取需要更新的DOM,最后批量更新真实DOM,其中最重要就是DOM对象对比策略和更新策略两大策略模型。
目前前端领域中的两大框架vue和react都使用到虚拟DOM,其中的策略也略有不同,他们在获取DOM对象阶段进行了复杂的操作,比如获取模板、解析模板生成ast语法树、根据ast语法树生成渲染函数,根据渲染函数获取到虚拟DOM对象,具体如下:
为了更好的研究vue和react虚拟DOM阶段的操作和逻辑,推荐先研究一下snabbdom和preact两个npm包,它分别对应这vue和react的处理方式。
同时在npm仓库中,可以看到目前最火的三大虚拟DOM实现分别是preact、virtual-dom和snabbdom,都可以认真了解一下。
4. 总结
在学习虚拟DOM的过程中,从它产生的原因出发,分析它的特点和优势以及应用场景,研究虚拟DOM对象属性,并引出虚拟DOM的原理和实践方式。最后从react和vue的内部实现引出snabbdom和preact两个第三方包,后续将先从这两个包出发,学习各自的虚拟DOM原理,为阅读react和vue的源码做准备。