当被问到虚拟的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的作用
- 维护视图和状态的关系
- 复杂视图情况下提升渲染性能
- 除了渲染DOM以外,还可以实现服务端渲染SSR(Nuxt.js/Next.js)、原生应用(WeexReact Native)、小程序(mpvue/uni-app)等
- Nuxt.js基于vue的服务端渲染框架
- Next.js基于react的服务端渲染框架
- 服务端渲染:把普通的虚拟DOM(就是js对象)转换成普通的js字符串
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:
- 创建虚拟DOM对象;
- 需要对比按钮点击前后两次的差异
(所以这里性能反而低了...)
模板转换成视图的过程
在正式介绍 Virtual Dom之前,我们有必要先了解下模板转换成视图的过程整个过程(如图):
- Vue.js通过编译将template 模板转换成渲染函数(render ) ,执行渲染函数就可以得到一个虚拟节点树。
- 在对 Model 进行操作的时候,会触发对应 Dep 中的 Watcher 对象。Watcher 对象会调用对应的 update 来修改视图。这个过程主要是将新旧虚拟节点进行差异对比,然后根据对比结果进行DOM操作来更新视图。
即:在Vue的底层实现上,Vue将模板编译成虚拟DOM渲染函数。结合Vue自带的响应系统,在状态改变时,Vue能够智能地计算出重新渲染组件的最小代价并应到DOM操作上。
图上几个概念解释:
渲染函数
- 是用来生成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+入口文件
注:这里报错是因为在snabbdom源码(使用ts语法)中没有用exports default导出,而是使用了export function
所以可以使用require
或者import { }
的方式去导入
不能
使用import 变量名
的方式导入;
export function init (modules: Array<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
中放置子元素 h1
,p
,并清空页面元素
/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)
清空页面元素:
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后发生了变化
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)
静态节点与动态节点
静态节点:就是你自己手动添加的,而不是通过后台绑定的。
动态节点:相反,是通过你写代码实现节点绑定数据。
简单的说 静态是手动添加创建,动态则是后台代码帮你实现数据绑定。