渲染真实DOM的开销很大,dom操作会引起浏览器的重排和重绘,也就是浏览器重新渲染。浏览器重新渲染是非常耗费性能的,因为要重新绘制整个页面。当数据变化后,尤其是大量的数据变化后,例如列表中的数据,如果直接操作dom的话,会让浏览器重新渲染整个列表。虚拟DOM中diff的核心是当数据变化后不直接操作dom,而是用javascript对象来描述真实DOM。当数据变化后,会先比较javascript对象是否发生变化,找到所有变化后的位置,最后只是最小化的更新变化的位置,从而提升性能。
Virtual DOM
Virtual DOM(虚拟 DOM),是由普通的 JS 对象来描述 DOM 对象。
- 虚拟 DOM 可以维护程序的状态,跟踪上一次的状态
- 通过比较前后两次状态差异更新真实 DOM
虚拟 DOM 的作用
- 维护视图和状态的关系,可以保存视图的状态
- 复杂视图情况下提升渲染性能
- 跨平台
- 浏览器平台渲染DOM
- 服务端渲染 SSR(Nuxt.js/Next.js)
- 原生应用(Weex/React Native)
- 小程序(mpvue/uni-app)等
虚拟 DOM 库
Snabbdom
- Vue.js 2.x 内部使用的虚拟 DOM 就是改造的 Snabbdom
- 大约 200 SLOC (single line of code)
- 通过模块可扩展
- 源码使用 TypeScript 开发
- 最快的 Virtual DOM 之一
Snabbdom
Snabbdom基本使用
- 创建项目
- 安装 parcel
- 配置 scripts
- 创建index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>snabbdom-demo</title>
</head>
<body>
<div id='app'></div>
<script src="./src/basicusage.js"></script>
</body>
</html>
- 导入 Snabbdom
-
安装 Snabbdom
npm intall snabbdom -
导入 Snabbdom
Snabbdom 的两个核心函数
- init 和 h()
- init() 是一个高阶函数,返回 patch()
- h() 返回虚拟节点 VNode 案例1:
- init 和 h()
// basicusage.js
// parcel/webpack 4 不支持 package.json 中的 exports 字段
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
const patch = init([])
// 第一个参数:标签+选择器
// 第二个参数:如果是字符串就是标签中的文本内容
let vnode = h('div#container.cls', 'Hello World')
let app = document.querySelector('#app')
// 第一个参数:旧的 VNode,可以是 DOM 元素
// 第二个参数:新的 VNode
// 返回新的 VNode
let oldVnode = patch(app, vnode)
patch(oldVnode, vnode)
案例2:
// basicusage.js
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
const patch = init([])
// 使用h()去创建一个div,可以创建div里面的子元素
let vnode = h('div#container', [
h('h1', 'Hello Snabbdom'),
h('p', '这是一个p')
])
let app = document.querySelector('#app')
let oldVnode = patch(app, vnode)
setTimeout(() => {
// vnode = h('div#container', [
// h('h1', 'Hello World'),
// h('p', 'Hello P')
// ])
// patch(oldVnode, vnode)
// 清除div中的内容
patch(oldVnode, h('!'))
}, 2000);
- Snabbdom 中的模块 模块的作用
- Snabbdom 的核心库并不能处理 DOM 元素的属性/样式/事件等,可以通过注册 Snabbdom 默认提供的模块来实现
- Snabbdom 中的模块可以用来扩展 Snabbdom的功能
- Snabbdom 中的模块的实现是通过注册全局的钩子函数来实现的 官方提供的模块
- attributes
- props
- dataset
- class
- style
- eventlisteners 模块使用步骤
- 导入需要的模块
- init() 中注册模块
- h() 函数的第二个参数处使用模块
// modules.js
import { init } from 'snabbdom/build/init'
import { h } from 'snabbdom/build/h'
// 1. 导入模块
import { styleModule } from 'snabbdom/build/modules/style'
import { eventListenersModule } from 'snabbdom/build/modules/eventlisteners'
// 2. 注册模块
const patch = init([
styleModule,
eventListenersModule
])
// 3. 使用h() 函数的第二个参数传入模块中使用的数据(对象)
let vnode = h('div', [
h('h1', { style: { backgroundColor: 'red' } }, 'Hello World'),
h('p', { on: { click: eventHandler } }, 'Hello P')
])
function eventHandler () {
console.log('别点我,疼')
}
let app = document.querySelector('#app')
patch(app, vnode)
Snabbdom 源码解析
Snabbdom 的核心
- init() 设置模块,创建 patch() 函数
- 使用 h() 函数创建 JavaScript 对象(VNode)描述真实 DOM
- patch() 比较新旧两个 Vnode
- 把变化的内容更新到真实 DOM 树
Snabbdom源码地址 github.com/snabbdom/sn…
diff算法
diff算法的本质就是查找出两个对象之间的差异,目的是尽可能多的复用节点。这个对象对应vue中的虚拟DOM,它是使用javascript对象来表示页面上的dom结构。虚拟DOM是将真实DOM数据抽离出来,用对象的形式模拟树形结构,diff算法比较的就是虚拟DOM。
diff算法是对操作前后的dom树的同一节点进行比较,一层一层的对比,然后插入真实的dom中进行渲染。它会给循环的列表中添加唯一标识,因为vue组件是高度复用的,增加了key可以识别组件的唯一性,这样diff算法就可以正确的识别此节点,并找到正确的位置插入新的节点。
Diff 算法的执行过程
diff 是找同级别的子节点依次比较,然后再找下一级别的节点比较。
- 在进行同级别节点比较的时候,首先会对新旧节点数组的 开始 和 结尾 节点设置标记索引,遍历的过程中移动索引;
索引标记为:
oldStartIdx/ newStartIdx (旧开始节点索引 / 新开始节点索引)
oldEndIdx/ newEndIdx (旧结束节点索引 / 新结束节点索引)
对应的节点为:
oldStartVnode / newStartVnode (旧开始节点 / 新开始节点)
oldEndVnode / newEndVnode (旧结束节点 / 新结束节点)
-
开始 和 结尾 点的比较依次按下面步骤进行
- 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同),调用
patchVnode()对比和更新节点,把旧开始和新开始索引往后移动,oldStartIdx++ / newStartIdx++,进入下一个循环。若不同,则进入下一个判断。 - 如果 oldEndVnode 和 newEndVnode 是 sameVnode (key 和 sel 相同),调用
patchVnode()对比和更新节点,把旧结尾和新结尾索引往前移动oldEndIdx-- / newEndIdx--,进入下一个循环;若不同,则进入下一个判断。 - 如果 oldStartVnode 和 newEndVnode 是 sameVnode (key 和 sel 相同),即 旧开始节点 / 新结尾节点 相同,调用
patchVnode()对比和更新节点,把 oldStartVnode 对应的 DOM 元素移动到当前标记的 oldEndVnode 对应的 DOM 元素的后面,然后更新索引oldStartIdx++ / newEndIdx--,进入下一个循环;不同,则进入下一个判断。 - 如果 oldEndVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同),即 旧结束节点 / 新开始节点 相同,调用
patchVnode()对比和更新节点,把 oldEndVnode 对应的 DOM 元素移动到当前标记的 oldStartVnode 对应的 DOM 元素的前面,然后更新索引oldEndIdx-- / newStartIdx++,进入下一个循环;不同,则进入下一步。
- 如果 oldStartVnode 和 newStartVnode 是 sameVnode (key 和 sel 相同),调用
-
如果首尾标记节点对比都不通过,则进入如下步骤:
- 使用当前标记的 newStartVnode 的 key 在 旧节点 数组中找相同节点。
- 如果没有找到,说明 newStartVnode 是新增节点,则用 newStartVnode 创建新的 DOM 元素,插入到当前标记的 oldStartVnode 对应的 DOM 元素之前,newStartIdx++ ,进入下一个循环。
- 如果找到了,则判断 新节点 和找到的 旧节点 的 sel 选择器是否相同。
- 如果相同,调用 patchVnode() 对比和更新节点,把找到的 旧节点 对应的 DOM 元素,移动到当前标记的 oldStartVnode 对应的 DOM 元素的前面, newStartIdx++ ,进入下一个循环。
- 如果不相同,说明节点被修改了,则用 newStartVnode 创建新的 DOM 元素,插入到当前标记的 oldStartVnode 对应的 DOM 元素之前,newStartIdx++ ,进入下一个循环。
-
同级对比循环结束时会有两种情况:旧节点的所有子节点先遍历完(
oldStartIdx > oldEndIdx)、新节点的所有子节点先遍历完 (newStartIdx > newEndIdx),此时需要对新旧节点数组进行后续处理:- 如果旧节点的数组先遍历完(
oldStartIdx > oldEndIdx),说明新节点有剩余且是新创建的 Vnode,则用这些剩余节点创建新的 DOM元素,并批量插入到当前所标记的 newEndVnode 之后的 Vnode(即标识索引为newEndIdx+1)所对应的 DOM 元素之前,若不存在该 Vnode,则相当于插入到末尾。 - 如果新节点的数组先遍历完(
newStartIdx > newEndIdx),说明旧节点中有多余,这直接把多余节点批量删除。
- 如果旧节点的数组先遍历完(