index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>dom diff算法</title>
</head>
<body>
<div id="root"></div>
<script src="js/element.js"></script>
<script src="js/diff.js"></script>
<script src="js/patch.js"></script>
<script>
/*
let virtualDom = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['a']),
createElement('li', {class: 'item'}, ['b']),
createElement('li', {class: 'item'}, ['c']),
]
)
renderDom(render(virtualDom), document.getElementById('root'))
*/
let virtualDom1 = createElement('ul', {class: 'list'}, [
createElement('li', {class: 'item'}, ['a1']),
createElement('li', {class: 'item'}, ['b']),
createElement('li', {class: 'item'}, ['c1']),
]
)
let virtualDom2 = createElement('ul', {class: 'list1'}, [
createElement('li', {class: 'item'}, ['1']),
createElement('li', {class: 'item'}, ['b']),
createElement('div', {class: 'item'}, ['3']),
]
)
let el = render(virtualDom1);
renderDom(el, document.getElementById('root'))
let patchs = diff(virtualDom1, virtualDom2);
// 将差异element放到页面中
patchRender(el, patchs)
</script>
</body>
</html>
element.js
/*
* @desc 创建标签
* @params
* {String} type 属性类型
* {Object} props 属性值
* {Array} children 子集
* */
class Element{
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
// eslint-disable-next-line no-unused-vars
/*
* @desc 创建标签
* @params
* {String} type 属性类型
* {Object} props 属性值
* {Array} children 子集
* */
function createElement(type, props, children) {
return new Element(type, props, children)
}
/*
* @desc 设置属性
* @params
* {Element} node 当前标签
* {String} key 属性
* {String, ...} value 属性值
* */
function setAttr(node, key, value) {
switch(key){
case 'value':
if(node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() === 'textarea'){
node.value = value
}
else{
node.setAttribute(key, value)
}
break;
case 'style':
node.style.cssText = value;
break;
default:
node.setAttribute(key, value);
break;
}
}
// eslint-disable-next-line no-unused-vars
/*
* @desc 创建标签,并且设置属性
* @params
* {Object} eleobj 虚拟树
* */
function render(eleobj) {
let el = document.createElement(eleobj.type);
for(let key in eleobj.props){
setAttr(el, key, eleobj.props[key])
}
eleobj.children.forEach(child => {
child = (child instanceof Element) ? render(child) : document.createTextNode(child);
el.appendChild(child);
})
return el;
}
// eslint-disable-next-line no-unused-vars
/*
* @desc 将渲染好的标签添加到页面中
* @params
* {Element} el 渲染好的树
* {Element} target 目标位置
* */
function renderDom(el, target) {
target.appendChild(el)
}
diff.js
/*
* @desc diff算法
* @params
* {Object} oldTree 旧树
* {Object} newTree 新树
* @return
* {Object} 补丁包
* @example
* {1: [], 2: []}
* */
function diff(oldTree, newTree) {
let patches = {};
let index = 0;
// 对比
walk(oldTree, newTree, index, patches)
return patches;
}
/*
* @desc 对比属性
* @params
* {Object} oldAttrs 旧属性
* {Object} newAttrs 新属性
* @return
* {Object} 返回带有差异的属性
* */
function diffAttr(oldAttrs, newAttrs) {
let obj = {};
for(let key in oldAttrs){
let newAttr = newAttrs[key];
let oldAttr = oldAttrs[key];
if(newAttr !== oldAttr){
obj[key] = newAttr; // 如果新属性上某一个属性被删除,则为undefined
}
}
// 以下为旧属性没有,新属性有值
for(let key in newAttrs){
let newAttr = newAttrs[key];
if(!oldAttrs.hasOwnProperty(key)){
obj[key] = newAttr;
}
}
return obj;
}
/*
* @desc 对比新旧树子节点
* @params
* {Array} oldChildren 旧子节点
* {Array} newChildren 新子节点
* {Object} patches 两棵树子元素的差异
* */
let Index = 0;
function diffChildren(oldChildren, newChildren, patches) {
oldChildren.forEach((child, idx) => {
walk(child, newChildren[idx], ++Index, patches)
})
}
/*
* @desc 判断是否为为文本节点
* @params
* {Element} node 要判断的元素
* */
function isString(node) {
return Object.prototype.toString.call(node) === '[object String]'
}
/*
* @desc 将两棵树差异返出来
* @params
* {Object} oldTree 旧树
* {Object} newTree 新树
* {Number} index 当前children的下标
* {Object} patches 两棵树的差异
* */
function walk(oldTree, newTree, index, patches) {
let currentPath = [];
// 节点被删除
if(!newTree){
currentPath.push({type: 'REMOVE', index})
}
// 判断是否是文本节点
else if(isString (oldTree) && isString(newTree)){
if(oldTree !== newTree){
currentPath.push({type: 'TEXT', text: newTree})
}
}
// 节点标签完全一样
else if(oldTree.type === newTree.type){
// 比较属性是否有更改
let attrs = diffAttr(oldTree.props, newTree.props);
if(Object.keys(attrs).length > 0){
currentPath.push({type: 'ATTRS', attrs})
}
// 如果有子节点,遍历子节点
diffChildren(oldTree.children, newTree.children, patches)
}
// 节点被替换
else{
currentPath.push({type: 'REPLACE', newTree})
}
// 当前元素有差异
if(currentPath.length > 0){
patches[index] = currentPath
}
}
patch.js
/*
* @desc 将两棵树差异性放到页面中
* @params
* {Element} el 将旧virtualDom转化为element
* {Object} patches 新旧两棵树差异
* @example
* patches: {0: [attrs: {class: "list1"},type: "ATTRS"], 2: Array(1), 6: [text: "3",type: "TEXT"]}
* */
let allPatches;
let index = 0; // 默认哪个属性需要打补丁
function patchRender(el, patches) {
console.log(patches)
allPatches = patches;
walkRender(el)
}
/*
* @desc 渲染子节点
* @params
* {Element} node 旧有的dom
* */
function walkRender(node) {
// 将patches里面的0,1,2...当做key值,取出对应数组
let currentPath = allPatches[index++];
let childNodes = node.childNodes;
childNodes.forEach(child => {
walkRender(child)
})
if(currentPath){
doPath(node, currentPath)
}
}
/*
* @desc 设置属性
* @params
* {Element} node 当前标签
* {String} key 属性
* {String, ...} value 属性值
* */
function setAttr(node, key, value) {
switch(key){
case 'value':
if(node.tagName.toLowerCase() === 'input' || node.tagName.toLowerCase() === 'textarea'){
node.value = value
}
else{
node.setAttribute(key, value)
}
break;
case 'style':
node.style.cssText = value;
break;
default:
node.setAttribute(key, value);
break;
}
}
/*
* @desc 将不定渲染到页面中
* @params
* {Element} node 旧有的dom
* {Array} patches 每一级对应不定数组
* */
function doPath(node, patches) {
console.log(patches)
patches.forEach(patch => {
switch (patch.type){
case 'ATTRS':
for(let key in patch.attrs){
let value = patch.attrs[key];
if(value){
setAttr(node, key, value)
}
else{
node.removeAttribute(key)
}
}
break;
case 'TEXT':
node.textContent = patch.text;
break;
case 'REPLACE':
let newNode = (patch.newTree instanceof Element) ? render(patch.newTree) : document.createTextNode(patch.newTree);
node.parentNode.replaceChild(newNode, node)
break;
case 'REMOVE':
node.parentNode.removeChild(node);
break;
default:
break;
}
})
}