首先先来聊一下,什么是虚拟dom?
为什么要使用虚拟dom?
其实答案很简单,虚拟dom就是位于js层和html层之间的一个层,使用js模拟出一个dom树,然后通过diff算法,来侦测到dom发生更改的准确信息,然后再通知html层进行更改.
至于为什么要使用虚拟dom其实原因也很实在,无非就是为了节约性能.
同学们之前可能都或多或少的了解过,我们的应用运行时损耗性能较大的就是频繁的dom操作,而dom操作会触发重绘或者回流
而虚拟dom的出现则解决了这个问题,由于js运行速度较快,我们可以通过使用js模拟dom树,形成一个虚拟dom树,再进行更改的时候速度就会快很多,只需要把更改的结果反馈给html层即可,大大减少了重绘与回流的触发.
闲话扯完了,开始代码部分
首先我们需要编写一个类,实现使用js模拟dom树
class
(高能预警) 这里是本文的核心内容!!!
然后是使用diff算法检测差异,然后生成一个包含差异信息的对象
首先我们先了解一下,我们是按照什么样的顺序获取差异的?
而我们又是如何标记新树和老树之间的差异呢?
两个节点之间的差异有总结起来有下面4种
0 代表 直接替换原有节点
1 代表 调整子节点,包括移动、删除等
2 代表 修改节点属性
3 代表 修改节点文本内容
而我们生成的差异对象应该是这样的
{
"1": [
{
"type": 0,
"node": {
"tagName": "h3",
"props": {
"style": "color: green"
},
"children": [
"I am H1"
],
"count": 1
}
}
]
}
实现的代码则应该是这样的
import listDiff from 'list-diff2'
// 每个节点有四种变动
export const REPLACE = 0 // 替换原有节点
export const REORDER = 1 // 调整子节点,包括移动、删除等
export const PROPS = 2 // 修改节点属性
export const TEXT = 3 // 修改节点文本内容
export function diff (oldTree, newTree) {
// 节点的遍历顺序
let index = 0
// 在遍历过程中记录节点的差异
let patches = {}
// 深度优先遍历两棵树
deepTraversal(oldTree, newTree, index, patches)
// 得到的差异对象返回出去
return patches
}
function deepTraversal(oldNode, newNode, index, patches) {
let currentPatch = []
if (newNode === null) { // 如果新节点没有的话直接不用比较了
return
}
if (typeof oldNode === 'string' && typeof newNode === 'string') {
// 比较文本节点
if (oldNode !== newNode) {
currentPatch.push({
type: TEXT,
content: newNode
})
}
} else if (oldNode.tagName === newNode.tagName && oldNode.key === newNode.key) {
// 节点类型相同
// 比较节点的属性是否相同
let propasPatches = diffProps(oldNode, newNode)
if (propasPatches) {
currentPatch.push({
type: PROPS,
props: propsPatches
})
}
// 递归比较子节点是否相同
diffChildren(oldNode.children, newNode.children, index, patches, currentPatch)
} else {
// 节点不一样,直接替换
currentPatch.push({ type: REPLACE, node: newNode })
}
if (currentPatch.length) {
// 那个index节点的差异记录下来
patches[index] = currentPatch
}
}
// 子数的diff
function diffChildren (oldChildren, newChildren, index, patches, currentPatch) {
var diffs = listDiff(oldChildren, newChildren)
newChildren = diffs.children
// 如果调整子节点,包括移动、删除等的话
if (diffs.moves.length) {
var reorderPatch = {
type: REORDER,
moves: diffs.moves
}
currentPatch.push(reorderPatch)
}
var leftNode = null
var currentNodeIndex = index
oldChildren.forEach((child, i) => {
var newChild = newChildren[i]
// index相加
currentNodeIndex = (leftNode && leftNode.count) ? currentNodeIndex + leftNode.count + 1 : currentNodeIndex + 1
// 深度遍历,从左树开始
deepTraversal(child, newChild, currentNodeIndex, patches)
// 从左树开始
leftNode = child
})
}
// 记录属性的差异
function diffProps (oldNode, newNode) {
let count = 0 // 声明一个有没没有属性变更的标志
const oldProps = oldNode.props
const newProps = newNode.props
const propsPatches = {}
// 找出不同的属性
for (let [key, val] of Object.entries(oldProps)) {
// 新的不等于旧的
if (newProps[key] !== val) {
count++
propsPatches[key] = newProps[key]
}
}
// 找出新增的属性
for (let [key, val] of Object.entries(newProps)) {
if (!oldProps.hasOwnProperty(key)) {
count++
propsPatches[key] = val
}
}
// 没有新增 也没有不同的属性 直接返回null
if (count === 0) {
return null
}
return propsPatches
}
最后,就是我们将差异对象解析并修改dom树.
import {REPLACE, REORDER, PROPS, TEXT} from './diff';
export function patch (node, patches) {
// 也是从0开始
const step = {
index: 0
}
// 深度遍历
deepTraversal(node, step, patches)
}
// 深度优先遍历dom结构
function deepTraversal(node, step, patches) {
// 拿到当前差异对象
const currentPatches = patches[step.index]
const len = node.childNodes ? node.childNodes.length : 0
for (let i = 0; i < len; i++) {
const child = node.childNodes[i]
step.index++
deepTraversal(child, step, patches)
}
//如果当前节点存在差异
if (currentPatches) {
// 把差异对象应用到当前节点上
applyPatches(node, currentPatches)
}
}
// 把差异对象应用到当前节点上
function applyPatches(node, currentPatches) {
currentPatches.forEach(currentPatch => {
switch (currentPatch.type) {
// 0: 替换原有节点
case REPLACE:
var newNode = (typeof currentPatch.node === 'string') ? document.createTextNode(currentPatch.node) : currentPatch.node.render()
node.parentNode.replaceChild(newNode, node)
break
// 1: 调整子节点,包括移动、删除等
case REORDER:
moveChildren(node, currentPatch.moves)
break
// 2: 修改节点属性
case PROPS:
for (let [key, val] of Object.entries(currentPatch.props)) {
if (val === undefined) {
node.removeAttribute(key)
} else {
node.setAttribute(key, value);
}
}
break;
// 3:修改节点文本内容
case TEXT:
if (node.textContent) {
node.textContent = currentPatch.content
} else {
node.nodeValue = currentPatch.content
}
break;
default:
throw new Error('Unknow patch type ' + currentPatch.type);
}
})
}
// 调整子节点,包括移动、删除等
function moveChildren (node, moves) {
let staticNodelist = Array.from(node.childNodes)
const maps = {}
staticNodelist.forEach(node => {
if (node.nodeType === 1) {
const key = node.getAttribute('key')
if (key) {
maps[key] = node
}
}
})
moves.forEach(move => {
const index = move.index
if (move.type === 0) { // 变动类型为删除的节点
if (staticNodeList[index] === node.childNodes[index]) {
node.removeChild(node.childNodes[index]);
}
staticNodeList.splice(index, 1);
} else {
let insertNode = maps[move.item.key]
? maps[move.item.key] : (typeof move.item === 'object')
? move.item.render() : document.createTextNode(move.item)
staticNodelist.splice(index, 0, insertNode);
node.insertBefore(insertNode, node.childNodes[index] || null)
}
})
}