声明文件
- index.js ----手动创建了两个虚拟dom树
- element.js ----实现了虚拟dom的渲染
- dom-diff.js ----实现了两个dom树之间的比较,并且生成了补丁patch对象
- patch.js ----实现了通过比较dom树的不同,将这个补丁包patch对象重新渲染出新的dom树。
-- 本次代码为简易版,代码粗糙,部分优化功能没有实现,比如
- 同级的dom进行比较,如果只是调换位置,不是通过删除,和插入新节点,应通过换位(key的作用)
- 同级的dom进行操作,一个相邻dom进行预比较,或者当新dom树的某一个节点,在老的dom树中存在此节点的情况下,进行优化处理
index.js
import { createElement, render, renderDom } from './element.js';
import diff from './dom-diff.js';
import patch from './patch.js';
let vertualDom = createElement('ul', { class: 'list' }, [
createElement('li', { class: 'item' }, ['a']),
createElement('li', { class: 'item' }, ['b']),
createElement('li', { class: 'item' }, ['c']),
]);
let newVertualDom = createElement('ul', { class: 'list' }, [
createElement('li', { class: 'item' }, ['1']),
createElement('li', { class: 'item' }, ['b']),
createElement('li', { class: 'item' }, ['3']),
]);
let el = render(vertualDom);
renderDom(el, window.root);
console.log(vertualDom);
let patches = diff(vertualDom, newVertualDom)
patch(el, patches);
element.js
class Element {
constructor(type, props, children) {
this.type = type;
this.props = props;
this.children = children;
}
}
//设置属性
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;
break;
default:
node.setAttribute(key, value);
break;
}
}
//返回虚拟节点
function createElement(type, props, children) {
return new Element(type, props, children);
}
//将虚拟dom转换为真实dom
function render(eleObj) {
let el = document.createElement(eleObj.type);
for (let key in eleObj.props) {
setAttr(el, key, eleObj.props[key]);
}
//遍历儿子节点,如果屎虚拟dom继续渲染,不是的话就是文本节点
eleObj.children.forEach((children) => {
child = (child instanceof Element) ? render(child) : document.createTextNode(child);
el.appendChild(child);
})
return el;
}
//将元素插入到页面中
function renderDom(el, target) {
target.appendChild(el)
}
export { Element, createElement, render, renderDom }
dom-diff.js
//dom - diff是通过js层面的计算返回一个patch对象(补丁包),然后通过特定的操作解析patch对象,完成页面的重新渲染
//先序深度优先遍历
//dom树同级对比,(节点类型,属性是否相同,新dom节点不存在,则删除,节点类型不相同直接采用替换模式)
//如果同级dom改变,进行删除,然后插入patch对象,就是插入新的dom节点
//同级如果dom如果只是位置发生改变,交换位置(key的作用)
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];//有可能是undefined
}
}
//老节点没有新节点的属性
for (let key in newAttrs) {
if (!oldAttrs.hasOwnProperty(key)) {
patch[key] = newAttrs[key];
}
}
return patch;
}
function diffChildren(oldChildren, newChildren, index, patches) {
oldChildren.forEach((child, idx) => {
//索引不应该是index了
//index每次传递给walk时,index是递增得,所有得人都基于一个序号来实现
Walk(child, newChildren[idx], ++Index, patches)
});
}
function isString(node) {
return Object.prototype.toString.call(node) === '[object String]';
}
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, index, patches);
} else {
//说明节点被替换了
currentPatch.push({ type: REPLACE, newNode })
}
if (currentPatch.length > 0) {
//当前元素确实有补丁将元素和补丁对应起来,放到大补丁包中
patches[index] = currentPatch;
}
}
export default diff;
patch.js
import { Element, render } from './element.js';
let allPatches;
let index = 0;
function patch(node, patches) {
//node是真实的dom
allPatches = patches;
//给某个元素打补丁
walk(node);
}
function walk(node) {
let currentPatch = allPatches[index++];
let childNodes = node.childNodes;
childNodes.forEach(child => walk(child));
if (currentPatch) {
doPatch(node, currentPatch);
}
}
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;
break;
default:
node.setAttribute(key, value);
break;
}
}
function doPatch(node, Patches) {
patches.forEach(patch => {
switch (patch.type) {
case 'ATTR':
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 'REMOVE':
node.parentNode.removeChild(node);
break;
case 'REPLACE':
let newNode = (patch.newNode instanceof Element) ? render(patch.newNode) : document.createTextNode(patch.newNode);
node.parentNode.replaceChild(newNode, node);
break;
default:
break;
}
})
}
export default patch;