虚拟DOM简述

179 阅读10分钟

当被问到虚拟的DOM的原理是什么?是怎么实现的?此时就需要系统的来学习一下啦!

什么是Virtual DOMVirtual DOM(虚拟DOM)

  • Virtual DOM(虚拟 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",     //通过sel描述标签

"data": {},

"children": undefined,

"text": "Hello Virtual DOM",    //标签内显示的文本

"elm": undefined,

"key": undefined

}

为什么要使用虚拟DOM

  • 手动操作 DOM 比较麻烦,还需要考虑浏览器兼容性问题
  • 虽然有 jQuery等库 简化DOM操作,但是随着项目的复杂,DOM操作复杂提升
  • 为了简化DOM的复杂操作于是出现了各种MVVM框架, MVVM框架解决了视图和状态的同步问题
  • 模板引擎(简化视图的操作),但没有解决跟踪状态变化的问题,于是Virtual DOM出现了

Virtual DOM的好处

原本的方式(没有VDom时):

案例:使用Jquery的列表,当进行新增和排序时,列表都会闪烁,应该DOM实际上都是进行删除然后重排的(重新渲染的)

(每次更新DOM都是:将DOM全部删除,重新添加DOM)

使用虚拟DOM:

只会更新DOM发生变化的元素,例如新增时,以前的DOM是不变的,只会增加一条DOM元素(所以列表不会闪烁)

(每次更新DOM:只新增改变的DOM,其他不变的DOM不发生变化)

总结:

手动DOM操作比较麻烦,并且很难跟踪以前的DOM状态。

解决方法:编写代码。像在状态更改时重新创建整个DOM一样。

当然,如果每次更改应用程序状态时实际上都重新创建了整个DOM,则应用程序将非常缓慢,并且输入字段将失去焦点。

virtual-dom是模块的集合,旨在提供声明性的方式来表示应用程序的DOM。

因此,无需在应用程序状态更改时更新DOM,只需创建一个虚拟树或VTree它看起来像你想要的DOM状态。

然后,virtual-dom将弄清楚如何有效地使DOM看起来像这样,而无需重新创建所有DOM节点。virtual-dom允许你在状态变化时更新视图。方法:创建视图的完整VTree,然后有效地修补DOM以使其完全符合你的描述。

这样可以避免在应用程序代码中进行手动D0M操作和先前状态跟踪, 从而为Web应用程序提供简洁且可维护的呈现逻辑。

虚拟DOM的作用

  1. 维护视图和状态的关系
  2. 复杂视图情况下提升渲染性能
  3. 除了渲染DOM以外,还可以实现服务端渲染SSR(Nuxt.js/Next.js)、原生应用(WeexReact Native)、小程序(mpvue/uni-app)等
    • Nuxt.js基于vue的服务端渲染框架
    • Next.js基于react的服务端渲染框架
    • 服务端渲染:把普通的虚拟DOM(就是js对象)转换成普通的js字符串

image.png Vue.js 2.0引入Virtual DOM,比Vue.js 1.0的初始渲染速度提升了2-4倍,并大大降低了内存消耗。

注:并不是所有的时候虚拟DOM都是提高性能的。

如:点击按钮更换文字内容。

let div = document.querySelector("#app");
let btn = document.querySelector("#btn");

btn.onclick =function() { 
    div.textContent = "Hello Virtual DOM";
} ; 

如果使用虚拟DOM:

  1. 创建虚拟DOM对象;
  2. 需要对比按钮点击前后两次的差异

(所以这里性能反而低了...)

模板转换成视图的过程

在正式介绍 Virtual Dom之前,我们有必要先了解下模板转换成视图的过程整个过程(如图):

  • Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树。
  • 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。

即:在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。

image.png

图上几个概念解释:

  • 渲染函数
    • 是用来生成Virtual DOM的。Vue推荐使用模板来构建我们的应用界面,在底层实现中Vue会将模板编译成渲染函数,当然也可以不写模板,直接写渲染函数,以获得更好的控制。
  • VNode 虚拟节点
    • 它可以代表一个真实的 dom 节点。通过 createElement 方法能将 VNode 渲染成 dom 节点。简单地说,vnode可以理解成节点描述对象。,它描述了应该怎样去创建真实的DOM节点。
  • patch(也叫做patching算法)
    • 虚拟DOM最核心的部分,它可以将vnode渲染成真实的DOM,这个过程是对比新旧虚拟节点之间有哪些不同,然后根据对比结果找出需要更新的的节点进行更新。
    • patch本身就有补丁、修补的意思,其实际作用是在现有DOM上进行修改来实现更新视图的目的。Vue的Virtual DOM Patching算法是基于Snabbdom的实现,并在些基础上作了很多的调整和改进。

Virtual DOM库

1、 Snabbdom

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

2、 virtual-dom

案例演示

Snabbdom

中文翻译

Snabbdom 基本使用 

这里使用的打包工具是parcel,也可以使用webpack。

这里使用了parcel(简单,几乎零配置)。注重简单性、模块化、强大特性和性能的虚拟DOM库。打包工具为了方便使用 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"
    
}

创建完项目之后,通过code .命令打开——直接打开vscode

配置package.json中:

  • 配置入口文件index.html
  • 编译:通过parcel+build+入口文件

image.png

image.png

注:这里报错是因为在snabbdom源码(使用ts语法)中没有用exports default导出,而是使用了export function

所以可以使用require或者import { } 的方式去导入

不能使用import 变量名 的方式导入;

export function init (modulesArray<Partial<Module>>, domApi?: DOMAPI) {

 ...

}

如果遇到下面的错误:

Cannot resolve dependency ‘snabbdom/init’

解决方法一:安装 Snabbdom@v0.7.4 版本

解决方法二:导入 init、h,以及模块只要把把路径补全即可。

import { h } from 'snabbdom/build/package/h';

import { init } from 'snabbdom/build/package/init';

import { classModule } from 'snabbdom/build/package/modules/class';

/src/01-basicuage.js

// var snabbdom = require('snabbdom');

// 使用require时可以,是因为在CommonJS中所有模块都会导出一个对象,所以我们可以始终使用一个变量来接收

// 导入snabbdom

// import snabbdom from 'snabbdom'

// console.log(snabbdom)

//使用import是使用了es6中的语法,没有使用export default的话就不能使用这种方式,必须使用{}方式来导入

// import {h, thunk, init} from 'snabbdom'

import { h } from 'snabbdom/build/package/h';

import { init } from 'snabbdom/build/package/init';

import { thunk } from 'snabbdom/build/package/thunk';

console.log(h, thunk, init)

使用

  • init() 是一个高阶函数,返回 patch()
  • h() 返回虚拟节点 VNode,这个函数我们在使用 Vue.js 的时候见过
new Vue({

router,

store,

render: (h) => h(App),}).$mount('#app');

thunk() 是一种优化策略,可以在处理不可变数据时使用

Hello World案例

index.html中

<body>
  <div id="app">
  </div>
  <script src="./src/01-basicusage.js"></script>
</body>

/src/01-basicusage.js中,使用了Init()、patch()

// import {h, thunk, init} from 'snabbdom'

import { h } from 'snabbdom/build/package/h';

import { init } from 'snabbdom/build/package/init';

// import { thunk } from 'snabbdom/build/package/thunk';

// console.log(h, thunk, init)


// 1.hello world

//patch 对比两个虚拟DOM,将差异更新到dom中

let patch  = init([])

//h()

  //第一个参数:标签+选择器

  //  #container表示有id选择器;.cls表示有类选择器

  //第二个参数:如果是字符串的话就是标签中的内容

let vnode = h('div#container.cls','Hello World')

let app = document.querySelector('#app')

//patch()

  //第一个参数:可以是DOM元素,内部会把DOM元素转换成VNode

  //第二个参数:VNode

  //返回值:VNode

let oldVnode = patch(app,vnode)

//到这里之后,在页面上看,已经打印出Hello World了

//假设的时刻

vnode = h('div','Hello Snabbdom')

//对比两者的差异,并将其更新到真实的DOM中

patch(oldVnode,vnode)

//到这里,在页面上看,已经打印出 Hello Snabbdom 了

div 中放置子元素 h1p,并清空页面元素

/src/02-basicusage.js中

// 2.div中放置子元素 h1,p

import { h } from 'snabbdom/build/package/h';

import { init } from 'snabbdom/build/package/init';

let patch = init([])


let vnode = h('div#container',[

  h('h1','Hello Snabbdom'),

  h('p','这是一个p标签')

])

let app = document.querySelector('#app')

patch(app,vnode)

image.png

清空页面元素:

let app = document.querySelector('#app')

let oldVnode = patch(app,vnode)

setTimeout(()=>{

  vnode = h('div#container',[

    h('h1','Hello World'),

    h('p','Hello P')

  ])

  //Patch(上一次的vnode,这次的vnode),对比这两次

  patch(oldVnode,vnode)

  //清空页面元素

    //错误做法:patch(旧DOM,null)

    //正确做法:可以通过注释节点来实现,使用h()创建注释节点,并写上!

    patch(oldVnode,h('!'))

      //效果:2s后页面该节点位置变为<!-- -->标签,即新生成的标签为空的注释节点

},2000)

//效果:页面2s后发生了变化

image.png

Snabbdom 模块使用

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

常用模块

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

/src/03-modules.js

/模块

import { h } from 'snabbdom/build/package/h';

import { init } from 'snabbdom/build/package/init';

//1、导入模块

import { styleModule } from 'snabbdom/build/package/modules/style';

import { eventListenersModule } from 'snabbdom/build/package/modules/eventlisteners';

//2、注册模块

  // 通过行内样式给div设置背景颜色

  // 注册一个点击事件

let patch = init([

  styleModule,

  eventListenersModule

])

//3、使用h()函数时,通过第二个参数设置模块中需要的数据

let vnode = h('div',{

  // 设置 DOM 元素的行内样式

  style: {

    backgroundColor'#999',

  },

  on:{

    click:eventHandler

  }

},[

  h('h1','Hello Snabbdom'),

  h('p','这是p标签')

]);

function eventHandler(){

  console.log('点击我了');

}

//将上面的渲染到页面中

let app = document.querySelector('#app');

//通过patch函数渲染

patch(app,vnode)

image.png

静态节点与动态节点

静态节点:就是你自己手动添加的,而不是通过后台绑定的。

动态节点:相反,是通过你写代码实现节点绑定数据。

简单的说 静态是手动添加创建,动态则是后台代码帮你实现数据绑定。