上一篇文章,我说到了如何实现一个简单的虚拟DOM,这一篇文章是接着上一篇文章的知识点的。node
咱们都知道虚拟DOM其实就是JS对象,咱们用JS来操做对象比操做DOM性能要好得多。算法
let patches = {};
let index = 0;
复制代码
因此咱们须要创建四个标识符。bash
const ATTR = 0; // 属性
const TEXT = 1; // 文本
const REMOVE = 2; // 删除
const REPLACE = 3; // 替换
复制代码
let vDom1 = createElement("div", {class: "div"}, [
createElement("div", {class: "div"}, ["a"])
]);
let vDom2 = createElement("div", {class: "div1"}, [
createElement("div", {class: "div2"}, ["b"])
]);
复制代码
diff(vDom1, vDom2)
function diff(oldTree, newTree){
walk(oldTree, newTree, index); // 遍历两个虚拟DOM树
}
复制代码
// 我这里使用了不是很准确的比较(可使用toString.call)
// 若是都是文本
// patch是补丁对象
// 数据描述: {type: 不一样的类型,不一样的地方}
if((typeof oldNode === "string") && (typeof newNode === "string")){
// 若是文本内容不同
if(newNode !== oldNode){
patch.push({type: TEXT, text: newNode});
}
}
复制代码
// 若是类型相同就比较属性, 类型不相同默认换掉了整个元素
if(oldNode.type === newNode.type){
// 遍历新老节点属性的不一样
let attr = diffAttr(oldNode.props, newNode.props);
// 若是有不一样, 就加入patch中
if (Object.keys(attr).length > 0) {
patch.push({ type: ATTR, attr });
}
// 遍历子节点
diffChildren(oldNode.children, newNode.children);
}
复制代码
遍历属性数据结构
function diffAttr(oldAttr, newAttr){
let attr = {};
// 看两个属性是否不一样(修改)
for (key in oldAttr) {
if(oldAttr[key] != newAttr[key]){
attr[key] = newAttr[key];
}
}
// 是否新增
for (key in newAttr) {
if(!oldAttr.hasOwnProperty(key)){
attr[key] = newAttr[key];
}
}
return attr;
}
复制代码
遍历子节点的属性dom
function diffChildren(oldChildren, newChildren){
oldChildren.forEach(function(child, i){
// 子节点递归遍历属性
walk(child, newChildren[i], ++ index);
});
}
复制代码
// 若是没有新节点,说明删除了,标记处删除的索引
if(!newNode){
patch.push({type: REMOVE, index});
}
复制代码
// 其他状况为替换
patch.push({type: REPLACE, newNode});
复制代码
let patches = {};
let index = 0;
const ATTR = 0;
const TEXT = 1;
const REMOVE = 2;
const REPLACE = 3;
function diff(oldTree, newTree){
walk(oldTree, newTree, index);
}
function walk(oldNode, newNode, index){
let patch = [];
// 删除
if(!newNode){
patch.push({type: REMOVE, index});
// 文本
}else if((typeof oldNode === "string") && (typeof newNode === "string")){
if(newNode !== oldNode){
patch.push({type: TEXT, text: newNode});
}
}else if(oldNode.type === newNode.type){
// 属性
let attr = diffAttr(oldNode.props, newNode.props);
if (Object.keys(attr).length > 0) {
patch.push({ type: ATTR, attr });
}
diffChildren(oldNode.children, newNode.children);
}else {
// 替换
patch.push({type: REPLACE, newNode});
}
if(patch.length > 0){
patches[index] = patch;
}
}
// 比较属性的不一样
function diffAttr(oldAttr, newAttr){
let attr = {};
// 看两个属性是否不一样(修改)
for (key in oldAttr) {
if(oldAttr[key] != newAttr[key]){
attr[key] = newAttr[key];
}
}
// 是否新增
for (key in newAttr) {
if(!oldAttr.hasOwnProperty(key)){
attr[key] = newAttr[key];
}
}
return attr;
}
// 比较子节点的属性
function diffChildren(oldChildren, newChildren){
oldChildren.forEach(function(child, i){
walk(child, newChildren[i], ++ index);
});
}
复制代码
咱们查看一下补丁对象 post
首先创建一个索引对象let patchIndex = 0;
性能
patch(dom, patches)
function patch(dom, patches){
walkPatch(dom);
}
复制代码
遍历补丁的实现ui
function walkPatch(dom){
// 获取当前节点的补丁
let patch = patches[patchIndex ++];
// 获取子节点
let children = dom.childNodes;
// 遍历子节点
// 遍历到最后一个元素,从后往前打补丁
children.forEach((child)=>walkPatch(child));
// 若是有补丁,就打补丁
if(patch){
doPatch(dom, patch);
}
}
复制代码
// 遍历属性
// key 就是 class或者value(这个value是属性)
// value 就是 类名或者是值
for (key in p.attr) {
let value = p.attr[key];
// 若是有值(其实就是上一篇虚拟DOM中的设置属性)
if(value){
if(key === "value"){
if(node.type.toUpperCase() === "INPUT" || node.type.toUpperCase() === "TEXTAREA"){
node.value = value;
}
}else {
node.setAttribute(key, value);
}
// 没有值,就是删除属性
}else {
node.removeAttribute(key);
}
}
复制代码
// 替换文本节点
node.textContent = p.text;
复制代码
// 删除本身
node.parentNode.removeChild(node);
复制代码
let { newNode } = p;
// 若是是元素就建立元素不然就是文本
newNode = (newNode instanceof Element) ? createDom(newNode): document.createTextNode(newNode);
// 用新节点替换旧结点
newNode.parentNode.replaceChild(newNode, node);
复制代码
总体代码spa
function doPatch(node, patch){
patch.forEach((p)=>{
switch (p.type) {
case ATTR:
// 遍历属性
for (key in p.attr) {
let value = p.attr[key];
// 若是有值(其实就是上一篇虚拟DOM中的设置属性)
if(value){
if(key === "value"){
if(node.type.toUpperCase() === "INPUT" || node.type.toUpperCase() === "TEXTAREA"){
node.value = value;
}
}else {
node.setAttribute(key, value);
}
// 没有值,就是删除属性
}else {
node.removeAttribute(key);
}
}
break;
case TEXT:
// 替换文本节点
node.textContent = p.text;
break;
case REMOVE:
// 删除本身
node.parentNode.removeChild(node);
break;
case REPLACE:
let { newNode } = p;
// 若是是元素就建立元素不然就是文本
newNode = (newNode instanceof Element) ? createDom(newNode): document.createTextNode(newNode);
// 用新节点替换旧结点
newNode.parentNode.replaceChild(newNode, node);
break;
default:
break;
}
})
}
复制代码
未打补丁的DOM树 3d
let vDom1 = createElement("div", {class: "div"}, [
createElement("div", {class: "div"}, ["a"]),
createElement("div", {}, ["b"]),
createElement("div", {class: "div"}, [
createElement("div", {class: "div"}, ["c"]),
createElement("div", {class: "div"}, ["d"])
])
]);
let vDom2 = createElement("div", {class: "div1"}, [
createElement("div", {class: "div2"}, ["1"]),
createElement("div", {class: "div3"}, ["2"]),
createElement("div", {}, [
createElement("div", {class: "div5"}, ["3"]),
createElement("div", {class: "div6"}, ["4"])
])
]);
复制代码
打补丁之前的DOM树