test.html:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>虚拟dom</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
<script type="text/javascript" src="diff.js"></script>
<script type="text/javascript" src="patch.js"></script>
<script type="text/javascript">
//虚拟dom的类
class Element{
constructor(type, props, children){
this.type = type;
this.props = props;
this.children = children;
}
}
//返回虚拟节点
function createElement(type, props, children){
return new Element(type, props, children)
}
//设置属性
function setAttr(node, key, value){
switch(key){
case 'value': //node是一个输入框
if(node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === "TEXTAREA"){
node.value = value;
}else{
node.setAttribute(key, value);
}
break;
case 'style':
node.style.cssText = value;
break;
default:
node.setAttribute(key, value)
break;
}
}
//插入到页面
function renderDom(el, target){
target.appendChild(el)
}
//转为节点dom
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
}
//-----------------------------------------
//虚拟dom
let vertualDom = createElement('ul',{class:'list'},[
createElement('li',{class: 'item'}, ['a']),
createElement('li',{class: 'item'}, ['b'])
])
let vertualDom1 = createElement('ul',{class:'list11'},[
createElement('li',{class: 'item'}, ['a']),
createElement('p',{class: 'item11'}, ['b1'])
])
//将虚拟dom转为真实dom渲染到页面
let el = render(vertualDom)
renderDom(el, document.getElementById('root'))
//修改的补丁
let patches = diff(vertualDom, vertualDom1)
console.log(patches)
//重新更新视图
patch(el, patches)
</script>
difff.js:
const ATTRS = 'ATTRS';
const TEXT = 'TEXT';
const REMOVE = 'REMOVE';
const REPLACE = 'REPLACE';
let Index = 0;
function diff(oldTree, newTree){
let patches = {}; //补丁
let index = 0; //开始比较的节点
//递归树 比较后的结果放到补丁包中
walk(oldTree, newTree, index, patches)
return patches;
}
//属性比较
function diffAttr(oldAttrs, newAttrs){
let patch = {}
//新旧属性对比
for(let key in oldAttrs){
if(oldAttrs[key] !== newAttrs[key]){
patch[key] = newAttrs[key]
}
}
for(let key in newAttrs){
//老节点没有新节点的属性
if(!oldAttrs.hasOwnProperty(key)){
patch[key] = newAttrs[key]
}
}
return patch;
}
function walk(oldNode, newNode, index, patches){
//自己的补丁包
let currentPatch = []
//节点删除
if(!newNode){
currentPatch.push({
type: REMOVE,
index
})
}else if(isString(oldNode) && isString(newNode)){//字符串
//文本不一致修改为最新的
if(oldNode !== newNode){
currentPatch.push({
type: TEXT,
text: newNode
})
}
}else if(oldNode.type === newNode.type){//节点类型相同
let attrs = diffAttr(oldNode.props, newNode.props);
//判断属性是否有修改
if(Object.keys(attrs).length > 0){
currentPatch.push({
type: ATTRS,
attrs
})
}
//子节点 遍历
diffChildren(oldNode.children, newNode.children, patches);
}else{ //节点被替换
currentPatch.push({
type: REPLACE,
newNode
})
}
//有补丁
if(currentPatch.length > 0){
//将元素和补丁对应放到外面的大补丁中去
patches[index] = currentPatch
}
}
function diffChildren(oldChildren, newChildren, patches){
oldChildren.forEach((child, idx)=>{
//索引全局 index
walk(child, newChildren[idx], ++Index, patches)
})
}
function isString(node){
return Object.prototype.toString.call(node) === "[object String]"
}
patch.js:
let allPatches;
let index = 0; //需要补丁的索引
function patch(node, patches){
allPatches = patches
patchWalk(node)
}
function patchWalk(node){
let currentPatch = allPatches[index++]
let childNodes = node.childNodes;
childNodes.forEach(child=>{
patchWalk(child)
})
//有补丁
if(currentPatch){
doPatch(node, currentPatch)
}
}
//给对应节点对应补丁
function doPatch(node, patches){
patches.forEach(item => {
switch(item.type){
case 'ATTRS':
for(let key in item.attrs){
let val = item.attrs[key]
if(val){
setAttr(node, key, val)
}else{
node.removeAttribute(key)
}
}
break;
case 'TEXT':
console.log(item.text)
node.textContent = item.text
break;
case 'REPLACE':
let newNode = (item.newNode instanceof Element) ? render(item.newNode) : document.createTextNode(item.newNode);
node.parentNode.replaceChild(newNode, node)
break;
case 'REMOVE':
break;
default:
break;
}
})
}