背景
粗略实现
大致思想
- 创建虚拟dom
- 渲染至页面
- 新旧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);
}
let virtualDomOld = createElement('ul', {
class: 'list'
}, [
createElement('li', {
class: 'item'
}, ['a']),
createElement('li', {
class: 'item'
}, ['b']),
createElement('li', {
class: 'item'
}, ['c']),
createElement('li', {
class: 'item'
}, ['d']),
]);
let virtualDomONew = createElement('ul', {
class: 'list-group'
}, [
createElement('li', {
class: 'item'
}, ['1']),
createElement('li', {
class: 'item'
}, ['0']),
createElement('li', {
class: 'item'
}, ['1']),
createElement('li', {
class: 'item'
}, ['1']),
]);
var ATTRS = 'ATTRS',
TEXT = 'TEXT',
REMOVE = 'REMOVE',
REPLACE = 'REPLACE',
INDEX = 0;
let currentPatch = [];
let allPatches;
let index = 0;
let el = render(virtualDomOld);
renderDom(el, window.root)
let patches = diff(virtualDomOld, virtualDomONew);
console.log(patches, 'get')
patch(el, patches)
function setAttr(node, key, value) {
switch (key) {
case 'value':
if (node.tagName.toUpperCase() === 'INPUT' || node.tagName.toUpperCase() === 'TEXTAREA') {
node.value = value;
} else {
node.setAttribute(key, value);
}
break;
case 'style': node.style.cssText = value;
default: node.setAttribute(key, value);
break;
}
}
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;
}
function renderDom(el, target) {
target.appendChild(el)
}
/**----------------------------------------------------------------**/
// dom diff
/**
* 先序深度优先遍历
*
* 当节点类型相同时=>查看属性是否相同=>产生属性的补丁包{type:'ATTRS',attrs:['class':'list-group'}}
* 新的dom节点不存在{type:'REMOVE':index}
* 节点类型不相同,直接采用替换模式{type:'REPLACE,newNode:newNode'}
* 文本的变化{type:'TEXT',text:1}
*
*
**/
function diff(oldTree, newTree) {
let patches = {},
index = 0;
walk(oldTree, newTree, index, patches);
return patches;
}
function diffAttrs(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 diffChildren(oldChildren, newChildren, patches) { // diff old first and new first
oldChildren.forEach((child, index) => {
walk(child, newChildren[index], INDEX++, patches)
})
}
function isString(node) {
return Object.prototype.toString.call(node) === '[object String]'
}
function walk(oldNode, newNode, index, patches) {
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 = diffAttrs(oldNode.props, newNode.props)
if (Object.keys(attrs).length > 0) {
currentPatch.push({type: ATTRS, attrs})
}
// children
diffChildren(oldNode.children, newNode.children, patches);
} else {
currentPatch.push({type: REPLACE, newNode})
}
if (currentPatch.length > 0) {
patches[index] = currentPatch;
}
}
function patch(node, patches) {
allPatches = patches;
walkPatch(node)
}
function walkPatch(node) {
let currentPatch = allPatches[index++];
let childNodes = node.childNodes;
childNodes.forEach(child => {
console.log(child, 'time')
walkPatch(child)
})
if (currentPatch) {
doPatch(node, currentPatch)
}
}
function doPatch(node, 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.newNode instanceof Element) ? render(patch.newNode) : document.createTextNode(patch.newNode)
node.parentNode.replaceChild(newNode, node);
break;
case 'REMOVE': node.parentNode.removeChild(node)
break;
default:
break;
}
})
}
思考
- 虚拟dom真的比直接操作dom快?
- 虚拟dom的优点?