我们都知道像vue、react都有用到虚拟dom,那么虚拟dom到底是什么?框架为什么不直接操作真实dom而要在中间要引入虚拟dom呢?vue和react的虚拟dom又有什么异同呢?我们就从虚拟dom开始讲起,再来逐步引入讲解vue与react的部分原理及其异同,这里会顺便讲解到数据驱动视图及视图驱动数据,顺便明白MVVM、MVC这两种模式,顺便引入相关的八股文知识点,好好阅读这篇文章你会不小心一下子就明白了一大堆知识Σ(っ °Д °;)っ。
所谓的虚拟dom、真实dom到底是什么?
实际上,我们在html文件里写的标签即为真实dom(能直接被浏览器解析渲染在页面呈现相应元素)。而虚拟 dom 就是一个普通的 js 对象,这个对象描述了界面的渲染内容。虚拟dom与真实dom可以一一对应。如图:
//真实dom
<div id="app">
<p class="text">木鱼</p>
</div>
//虚拟dom本质就是一个js对象,用来描述界面渲染内容,上述真实dom对应的虚拟dom结构如下:
{
tag: 'div',
props: {
id: 'app'
},
chidren: [
{
tag: 'p',
props: {
className: 'text'
},
chidren: [
'木鱼'
]
}
]
}
虚拟dom的作用:
如果没有虚拟dom:作为通用框架,很难去预知数据跟界面之间到底是如何对应的(比如一个数据变了,框架不知道是界面哪个区域发生变动,数据是数据,界面是界面,框架能修改数据,也能通过render函数让界面重新渲染,但框架无法确定数据影响界面的具体区域),所以在没有虚拟 dom 的情况下,框架只能在数据发生变化了就直接将整个界面重新生成一次,这样没有发生变化的元素也会重新生成一遍,如此操作真实 dom 的代价就非常昂贵,会触发到浏览器的重排重绘让浏览器对整个页面所有元素都重新渲染,就会严重引发效率问题。
为了减少对真实 dom 无意义的操作,因此不得不引入虚拟 dom。当数据变化之后,由于框架无法知道数据与界面的一一对应关系,不知道界面哪个区域要更新,而全量生成真实dom导致的渲染代价较大,既然要全量生成,框架就选择全量生成虚拟 dom(虚拟dom是 js 对象,给 js 对象属性赋值修改属性值、改动对象结构,并不涉及界面,仅仅只是修改一个对象属性而已,效率自然是非常高的),然后通过跟之前的虚拟 dom 进行对比,找到有差异的虚拟dom节点,改动该虚拟节点所对应的真实 dom即可(注意前面提过虚拟dom与真实dom可以一一对应),如此,真实dom就无需全量生成渲染,框架仅需根据虚拟dom对比后的结果做对应的节点更新即可。
除了以上情况,框架引入虚拟dom还有第二个原因,就是建立了一个抽象层。特别是像 react 这种框架,它在设计之时并没有将自己定位为一个页面级别应用框架,而是把自己定位为一个 UI 库(UI 表示用户界面,而用户界面不一定是网页,也可能是移动端、桌面应用程序等,而不同平台差异很大,我们平时说的 dom 通常是指在页面应用中的,而像小程序、移动端 app、桌面应用都没有所谓的 dom),框架为了消除平台之间的差异,于是抽象了一个UI的表达方式,它使用一个普通对象(即虚拟 dom)来表达 UI 界面,然后根据不同的平台去具体生成真实的界面,即建立了一个抽象层,也就是用一套虚拟 dom,可以对接不同平台的渲染逻辑,实现一次编码多端运行。
虚拟dom树在框架中的存在形式
vue中的虚拟dom
- vue 是怎么得到虚拟 dom 树的呢?
- 实际上是 vue 有配置一个 render 函数,vue 运行了 render 函数返回的结果就是虚拟 dom(这个 render 函数有一个参数 h,h是个函数,h传入一个代表标签名和标签内容的参数,即可生成一个虚拟 dom,标签内容可再继续嵌入 h 函数表示其子节点信息)。当改动数据后,render 依赖了这个被 vue 监听的,则会再执行一次 render 函数(响应式,具体原理我在这篇文章描述过《关于Vue的数据响应式》原文链接:http://t.csdnimg.cn/KYlEg)。
- 而 vue 给我们提供了模板,我们在Vue上是通过Vue的模板(在template标签或属性里写)书写HTML信息,而vue模板上的内容实际上并不是真实 dom,vue 会根据模板编译成 render 方法(逻辑是看是否有 render 方法,有的话则略过模板内容直接把 render 方法的返回结果作为虚拟节点树。如果没有 render 方法,则查找模板,即 template 这个配置,把模板编译成 render 方法;如果模板 template 这个配置也没有的话,它会查找 el 这个配置对应的元素,将这个元素直接作为模板,再将模板编译成 render,再通过 render 生成虚拟节点树,才能进行渲染真实节点)。由于树形结构只有一个根节点(一个根节点才能保证是一棵树),因此 vue 会要求模板也必须只有个根节点,这样编译成 render 函数生成的虚拟节点数才是单根的。
react中的虚拟dom
- react 又是怎么得到虚拟 dom 树的呢?
- 无论是使用react 的 class 或 function 写法, 本质都是生成 render 函数,其返回结果为描述页面信息结构的jsx语法。
- react 是使用 jsx 语法,在组件中直接编写虚拟 dom 结构,然后通过 Babel 等工具将其转换为 JavaScript 代码。
- jsx其实是 createElement()方法的语法糖,jsx 语法会被@babel/preset-react 插件编译为 createElement()方法,而 createElement()方法会返回 react 元素,也就是虚拟dom对象(react 元素就是一个 js 对象,用来描述真实 dom 的内容,其本质也就是虚拟 dom,只是在不同应用场景叫法不同)。
vue与react的虚拟dom及其应用形式的对比
本质上vue与react虚拟dom并没有什么区别。vue 和 react 本质都是用 js 对象来模拟真实 dom(二者仅仅是让程序员书写页面结构的形式以及将其编译为虚拟dom的方式不同。但无论是 vue 的模板写法还是 react 的 class 或 function 写法, 最后都是生成 render 函数,而 render 函数会执行返回一个描述页面信息结构的对象,也就是虚拟 dom,本质上是棵树),然后都是通过对虚拟 dom 进行 diff 算法来最小化更新真实 dom,从而减少不必要的性能损耗(其中 diff 算法的优化思路基本相同:tag 不同则视为不同节点;只比较同一层级,不跨级比较;同一层级节点用 key 唯一标识,tag 和 key 都相同则视为同一节点[所以无论是 react 还是 vue,在写动态列表时都需要设置一个唯一值 key,这样 diff 算法处理时才能性能最大化])。