你不知道的Virtual DOM
# 你不知道的Virtual DOM
[toc]
# 什么是Virtual DOM?
虚拟DOM,是目前主流MVVM前端框架的组成部分。
最初诞生的目的是前端工程化解决方案中的一个技术点。
主要的职责如同DOM元素一样,用于描述页面的结构。
# Virtual DOM解决了什么问题?
解决了目前阶段Web开发的工程化问题。
这就不得不提MVVM框架,Vue和React等等都是MVVM框架的实现。
回顾一下,历史阶段是如何开发一个页面的。
- 获取数据:我们要从后端服务器拿到数据结构Model,然后保存到一个地方
- 创建视图:可能使用模板引擎去描述我们的页面结构。
- 渲染到视图:将Model结合模板引擎,获得新的页面结构片段.
- 监听视图变化更新数据:监听DOM元素的事件,用户操作等,等待新的Model变更。
- 将新的变化再一次渲染到视图:当用户操作页面后会得到新的Model,将Model根据相应的预设逻辑进行更新,并同步到视图中。
- 提交到服务器:将新的Model更新到数据库。
而现阶段,使用MVVM框架是如何开发一个页面的呢?
- 获取数据:我们要从后端服务器拿到数据结构Model,然后保存到一个地方
- 描述视图:描述页面的结构(VNode or VNode Template)。
- 绑定到视图:将Model丢给MVVM框架,由VM来维护视图和数据间的关系和变化。
- 提交到服务器:将新的Model更新到数据库。
原来需要手动维护的渲染到视图、监听视图变化更新数据、将新的变化再一次渲染到视图,这三层关系,由MVVM框架来统一托管。
其中MVVM里的vm,最核心的就是使用Virtual DOM来维护页面。
那么为什么数据同步需要用到Virtual DOM?
MVVM实现Model自动同步到View面临两个问题:
- 监听变化
- 数据变化,Observer(观察者模式)
- 视图变化,Tree Diff(树对比)
- 同步更新
- 更新数据
- 更新视图
而Virtual DOM在视图变化中有两个优势:
第一,它没有DOM Element那么重
Diff性能高,VNode相当于阉割版的DOM Element,属性少,相比DOM Element,需要Diff的东西少。
第二,增强了组件化的实现
- 提前尝鲜。VNode可以实现插槽(Slot),自定义标签(Custom Element) 等Web Component规范里才有的前沿技术,并把它们搬到大多数浏览器上。
- 提升代码可维护性
- 更佳直观了解组件之间的关系。
- 更容易的拆分逻辑。
# Virtual DOM由哪些组成部分?
这里仅仅讨论Vue里的Virtual DOM实现,Virtual DOM由一个个VNode组成,VNode由以下几个部分组成
入参:Props和DOM Props和DOM Attrs
- Props为自定义组件的入参
- DOM Props为不能被序列化至DOM标签上的属性
- DOM Attrs 为能被序列化至DOM标签上的属性
样式: class和style
- static class,静态的class
- class ,和状态绑定的class
- style,行内样式
事件: on和nativeOn
- on,处理自定义组件的自定义事件
- nativeOn,DOM原生的事件
插槽: slot和scopedSlots
- slot,纯插槽
- scopedSlots,作用域插槽,主要用于将组件内部数据的渲染职能开放给父级组件
有了以上信息,就足够描述我们的页面了。
# 如何利用Virtual DOM?
# 实际编码的使用
SFC(单文件组件)
移除template元素,使用render函数代替
<script>
// Vue组件
export default {
render(createElement) {
// <div id="el"></div>
return createElement(
'div',
{
attrs: {
id: 'el'
}
},
['I\'m a div']
)
}
}
</script>
<script>
// jsx版本
export default {
render() {
// <div id="el"></div>
return (
<div id="el"></div>
)
}
</script>
在js文件中
VNode的创建和jsx其实也可以在普通的js文件中使用,只需要引用了vue的createElement函数即可
/**
* 创建select元素
* @param {VueComponent.createElement} createElement
* @param {Array<{ label: string, value: string }>} options
* @example
* <select>
* <option value="a"> A </option>
* <option value="b"> B </option>
* </select>
*/
function genSelectVNode(createElement, options) {
const optionsVNodeList = options.map(function ({ label, value }) {
return createElement('option', { value }, label);
});
return createElement('select', optionsVNodeList);
}
//------------------------------------------------------------------------------------------------------------
/**
* 创建select元素
* @param {VueComponent.createElement} createElement
* @param {Array<{ label: string, value: string }>} options
* @example
* <select>
* <option value="a"> A </option>
* <option value="b"> B </option>
* </select>
*/
function genSelectVNodeForJSX(createElement, options) {
const optionsVNodeList = options.map(function ({ label, value }) {
return <option value={value}>{label}</option>
});
return <select>{optionsVNodeList}</select>
}
# 框架是如何将Virtual DOM转换成实际的DOM
- render,根据Model创建新的Virtual DOM
- diff, 找出两个tree之间的最小编辑距离
- patch,打开一个缓冲区,将最小编辑距离存入缓冲区,待下一帧绘制前同步至页面
# 控制diff算法
Vue采用的是双指针层级对比方法,是简单最小编辑距离的查找的贪心实现。具体的代码实现的updateChildren函数 (opens new window)
贪心的部分:
- 当VNode发生变化时,放弃对孩子节点的对比,视为发生变化。
- 当VNode指定key属性时,优先比对,若不相同时,则视为未变化
判断是否两个节点是否相等
以下是vue源码用于判断两个VNode节点是否相同的代码片段。
// 代码来自Vue virtual dom代码片段
// 完整代码:https://github.com/vuejs/vue/blob/dev/src/core/vdom/patch.js
// 判断两个VNode是否相同
function sameVnode (a, b) {
return (
a.key === b.key && // 优先使用key判断
a.asyncFactory === b.asyncFactory && (
(
a.tag === b.tag &&
a.isComment === b.isComment &&
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b)
) || (
isTrue(a.isAsyncPlaceholder) &&
isUndef(b.asyncFactory.error)
)
)
)
}
通过控制key值,可以实现
- 控制DOM元素的复用,实现静态渲染
- 强制刷新DOM元素
关于key的官方说明:https://cn.vuejs.org/v2/api/#key
# 适用场景及实战案例
# 场景1:函数式组件
函数式组件多适用于那些一次性业务场景,它属于临时性的执行,执行完即回收。
例如,创建一个Alert,用于提示用户的信息
// MyAlert.js
import Vue from 'vue';
const ContainerStyles = {
width: '400px',
height: '300px',
position: 'fixed',
left: '50%',
top: '50%',
transform: 'translate(-50%, -50%)'
}
function MyAlert(messageGenerator) {
const MyAlertComponent = Vue.extend({
name: 'MyAlert',
render(createElement) {
const content = messageGenerator(createElement);
return (
<div style={ContainerStyles}>{content}</div>
)
}
})
const alert = new MyAlertComponent();
alert.$mount();
document.body.append(alert.$el);
}
export default MyAlert;
调用方
import MyAlert from './MyAlert.js';
MyAlert(function (createElement) {
return createElement('p', {
style: {
color: 'red'
}
}, '测试')
});
# 场景2:设计灵活度更高的组件
- 高度配置化的组件
- 改良原有的代码设计,提升可维护性,可读性,高度模块化。
- 实现一些只有javascript才能实现特殊渲染逻辑。
# 场景3:组件的单元测试
- 脱离浏览器环境进行组件本身的逻辑测试,分支测试,语句测试等单元级别的测试。
- 代码少,速度快,在大量测试用例下,能够节省CI资源的消耗。
# 场景4:服务器端渲染
- 需要良好的SEO
- 降低维护成本,实现前端代码和服务端渲染技术的同构