以装修房子为例,如果我们仅需要在客厅新添一座沙发或者将卧室的床换个位置。那么将整个房子重新翻修显然是不切实际的,我们通常的做法是在原先装修的基础上做微小的改动即可。
对于 DOM 树来讲,也是同样的道理,如果仅仅是新增了一个标签或者修改了某一个标签的属性或内容。那么引起整个 DOM 树的重新渲染显然是对性能和资源的极大浪费,虽然我们的计算机每秒能进行上亿次的计算。实际上,我们只需要找出新旧 DOM 树存在差异的地方,只针对这一块区域进行重新渲染就可以了。
所以 Diff 算法应运而生,diff 取自 different (不同的),Diff算法的作用,总结来说,就是:精细化对比,最小量更新。
用 JavaScript 对象描述 DOM 的层次结构。DOM 中的一切属性都在虚拟 DOM 中有对应的属性。
diff 是发生在虚拟 DOM 上的:新虚拟 DOM 和老虚拟 DOM 进行 diff (精细化比较),算出应该如何最小量更新,最后反映到真实的 DOM 上。
真实DOM:
<div class="box">
<h3>我是一个标题h3>
<ul>
<li>HTMLli>
<li>CSSli>
<li>JavaScriptli>
ul>
div>
虚拟DOM:
{
sel: div,
data: {
class: box
},
children: [
{ sel: h3, data: {}, text: 我是一个标题 },
{ sel: ul, data: {}, children: [
{ sel: li, data: {}, text: HTML }
{ sel: li, data: {}, text: CSS }
{ sel: li, data: {}, text: JavaScript }
]
]
}
作用:生成虚拟节点(Vnode)
示例:
h('ul', {}, [
h('li', {}, 'HTML'),
h('li', {}, 'CSS'),
h('li', {}, 'JavaScript')
])
// 会得到
{
"sel": "ul",
"data": {},
"children": [
{ "sel": "li", "data": {}, "text": "HTML" },
{ "sel": "li", "data": {}, "text": "CSS" },
{ "sel": "li", "data": {}, "text": "JavaScript" }
]
}
手写简单版h函数
/**
* @name: 简单版h函数
* @param {*} sel 选择器
* @param {*} data 属性
* @param {*} c 子节点或文字
* @return {*} vnode虚拟节点
*/
export default function(sel, data, c) {
if (arguments.length !== 3) throw new Error('h函数要求3个参数')
if (typeof c === 'string' || typeof c === 'number') {
// 判断参数c是否为文字
return vnode(sel, data, undefined, c, undefined)
} else if (typeof c === 'object' && c.sel) {
// 判断参数c是否为单个h函数
return vnode(sel, data, c, undefined, undefined)
} else if (Array.isArray(c)) {
// 判断参数c是否为数组
let children = []
// 循环遍历参数c收集children
for (let i = 0; i < c.length; i++) {
if (typeof c[i] === 'object' && c[i].sel) {
children.push(c[i])
} else if (typeof c[i] === 'string' || typeof c[i] === 'number') {
children.push(
vnode(undefined, undefined, undefined, c[i], undefined)
)
} else {
console.log(c[i]);
throw new Error('children中有子项类型错误')
}
}
return vnode(sel, data, children, undefined, undefined)
} else {
throw new Error('第三个参数类型错误')
}
}
/**
* @param {*} sel 选择器
* @param {*} data 属性
* @param {*} children 子节点
* @param {*} text 文字节点
* @param {*} el 真实dom
* @return {*}
*/
function vnode(sel, data, children, text, el) {
const key = data.key
return {
sel, data, children, text, el, key
}
}
Git
:https://github.com/snabbdom/snabbdomimport {
init,
classModule,
propsModule,
styleModule,
eventListenersModule,
h,
} from "snabbdom";
const patch = init([
// Init patch function with chosen modules
classModule, // makes it easy to toggle classes
propsModule, // for setting properties on DOM elements
styleModule, // handles styling on elements with support for animations
eventListenersModule, // attaches event listeners
]);
const container = document.getElementById("container");
const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
h("span", { style: { fontWeight: "bold" } }, "This is bold"),
" and this is just normal text",
h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);
console.log(vnode);
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode);
const newVnode = h(
"div#container.two.classes",
{ on: { click: anotherEventHandler } },
[
h(
"span",
{ style: { fontWeight: "normal", fontStyle: "italic" } },
"This is now italic type"
),
" and this is still just normal text",
h("a", { props: { href: "/bar" } }, "I'll take you places!"),
]
);
// Second `patch` invocation
patch(vnode, newVnode); // Snabbdom efficiently updates the old view to the new state
function someFn() {
console.log(111);
}
function anotherEventHandler() {
console.log(222);
}
diff算法比较流程图:
patch函数手写代码:
import vnode from './vnode'
import createElement from './createElement'
import patchVnode from './patchVnode'
export default function patch(oldVnode, newVnode) {
if (!oldVnode.sel) {
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
}
if (oldVnode.sel !== newVnode.sel || oldVnode.key !== newVnode.key) {
let newDom = createElement(newVnode)
if (oldVnode.el && newDom) {
oldVnode.el.parentNode.insertBefore(newDom, oldVnode.el)
}
oldVnode.el.parentNode.removeChild(oldVnode.el)
} else {
patchVnode(oldVnode, newVnode)
}
}
patchVnode函数手写代码:
import updateChildren from "./updateChildren"
export default function patchVnode(oldVnode, newVnode) {
// 判断新旧节点内存是否指向统一对象
if (oldVnode === newVnode) return
// 新节点有文字且没有children
if (newVnode.text && (newVnode.children == undefined || newVnode.children.length < 1)) {
// 直接将oldVnode的innerText改为newVnode.text
if (newVnode.text !== oldVnode.text) {
oldVnode.el.innerText = newVnode.text
}
} else {
if (oldVnode.children === undefined || oldVnode.children.length < 1) {
// newVnode有children oldVnode没有children有text
oldVnode.el.innerText = ''
for (let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i])
oldVnode.el.appendChild(dom)
}
} else {
console.log('比较子节点');
updateChildren(oldVnode.el, oldVnode.children, newVnode.children)
}
}
}
updateChildren函数手写就代码:
import createElement from "./createElement"
import patchVnode from "./patchVnode"
export default function updateChildren(parentElm, oldCh, newCh) {
let newStartIdx = 0
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[newStartIdx]
let newEndVnode = newCh[newEndIdx]
let oldStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[oldStartIdx]
let oldEndVnode = oldCh[oldEndIdx]
let before = null
let keyMap = null
while (newEndIdx >= newStartIdx && oldEndIdx >= oldStartIdx) {
if (!oldEndVnode) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (!oldStartVnode) {
oldStartVnode = oldCh[++oldStartIdx]
} else if (!newEndVnode) {
newEndVnode = newCh[--newEndIdx]
} else if (!newStartVnode) {
newStartVnode = newCh[++newStartIdx]
} else if (isSameNode(newStartVnode, oldStartVnode)) { // 新前与旧前比较
console.log('1、新前与旧前相同');
patchVnode(oldStartVnode, newStartVnode)
newStartVnode = newCh[++newStartIdx]
oldStartVnode = oldCh[++oldStartIdx]
} else if (isSameNode(newEndVnode, oldEndVnode)) { // 新后与旧后比较
console.log('2、新后与旧后相同');
patchVnode(oldEndVnode, newEndVnode)
newEndVnode = newCh[--newEndIdx]
oldEndVnode = oldCh[--oldEndIdx]
} else if (isSameNode(newEndVnode, oldStartVnode)) { // 新后与旧前比较
console.log('3、新后与旧前相同');
patchVnode(oldStartVnode, newEndVnode)
parentElm.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
newEndVnode = newCh[--newEndIdx]
oldStartVnode = undefined
oldStartVnode = oldCh[++oldStartIdx]
} else if (isSameNode(oldEndVnode, newStartVnode)) { // 新前与旧后相同
console.log('4、新前与旧后相同');
patchVnode(oldEndVnode, newStartVnode)
before = oldStartVnode.el
parentElm.insertBefore(oldEndVnode.el, before)
oldEndVnode = undefined
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
console.log('都不命中');
if (!keyMap) {
keyMap = {}
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
if (oldCh[i].key) {
keyMap[oldCh[i].key] = i
}
}
}
console.log(keyMap);
let moveIdx = keyMap[newStartVnode.key]
if (moveIdx) {
let nodeToMove = oldCh[moveIdx]
patchVnode(nodeToMove, newStartVnode)
oldCh[moveIdx] = undefined
parentElm.insertBefore(nodeToMove.el, oldStartVnode.el)
} else {
let newDom = createElement(newStartVnode)
parentElm.insertBefore(newDom, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
if (newEndIdx < newStartIdx) {
console.log('新节点循环结束');
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
parentElm.removeChild(oldCh[i].el)
}
} else if (oldEndIdx < oldStartIdx) {
console.log('旧节点循环结束');
before = oldCh[oldEndIdx + 1] == null ? null : oldCh[oldEndIdx + 1].el
for (let i = newStartIdx; i <= newEndIdx; i++) {
let newDom = createElement(newCh[i])
parentElm.insertBefore(newDom, before)
}
}
}
function isSameNode(node1, node2) {
return node1.sel === node2.sel && node1.key === node2.key
}