手写dom diff

250 阅读3分钟

element.js 生成虚拟dom

import utils from './utils.js'

class Element {
    constructor(tagName,attrs,children) {
        this.tagName = tagName;
        this.attrs = attrs;
        this.children = children || [];
    }

    //  把一个虚拟的dom节点渲染成一个真实dom节点
    render(){
        //  创建一个真实的dom的节点
        let element =  document.createElement(this.tagName);
        //  给真实dom元素节点增加属性
        for(let attr in this.attrs){
            utils.setAttr(element, attr,this.attrs[attr]);
        }

        //  先序深度遍历
        this.children.forEach(child => {
            //  如果子节点是一个元素的话,就调用它的render方法创建子节点的真实dom,如果是一个字符串的话,创建一个文件节点就可以了
            let childElement = (child instanceof Element) ? child.render() : document.createTextNode(child);
            element.appendChild(childElement)
        })

        return element;
    }
}

function createElement(tagName,attrs,children) {
    return new Element(tagName,attrs,children);
}

export {
    createElement
}

diff.js 进行dom diff比较生成补丁包

import utils from './utils.js';

let keyIndex = 0;
function diff(oldTree, newTree) {
    //记录差异的空对象。key就是老节点在原来虚拟DOM树中的序号,值就是一个差异对象数组
    let patches = {};
    keyIndex = 0;
    let index = 0;
    walk(oldTree, newTree, index, patches);
    return patches;
}
//遍历
//REMOVE: 'REMOVE',//此节点被移除
//ATTRS: "ATTRS",//属性被改变
//TEXT: "TEXT",//文本内容被改变
//REPLACE: "REPLACE", //节点要被整个替换  
function walk(oldNode, newNode, index, patches) {
    let currentPatches = [];//这个数组里记录了所有的oldNode的变化
    if (!newNode) {//如果新节点没有了,则认为此节点被删除了
        currentPatches.push({ type: utils.REMOVE, index });
        //如果说老节点的新的节点都是文本节点的话
    } else if (utils.isString(oldNode) && utils.isString(newNode)) {
        //如果新的字符符值和旧的不一样
        if (oldNode != newNode) {
            ///文本改变 
            currentPatches.push({ type: utils.TEXT, content: newNode });
        }
    } else if (oldNode.tagName == newNode.tagName) {
        //比较新旧元素的属性对象
        let attrsPatch = diffAttr(oldNode.attrs, newNode.attrs);
        //如果新旧元素有差异 的属性的话
        if (Object.keys(attrsPatch).length > 0) {
            //添加到差异数组中去
            currentPatches.push({ type: utils.ATTRS, attrs: attrsPatch });
        }
        //自己比完后再比自己的儿子们
        diffChildren(oldNode.children, newNode.children, index, patches, currentPatches);
    } else {
        currentPatches.push({ type: utils.REPLACE, node: newNode });
    }
    if (currentPatches.length > 0) {
      patches[index] = currentPatches;
    }
}
//老的节点的儿子们 新节点的儿子们 父节点的序号 完整补丁对象 当前旧节点的补丁对象
function diffChildren(oldChildren, newChildren, index, patches, currentPatches) {
    oldChildren.forEach((child, idx) => {
        walk(child, newChildren[idx], ++keyIndex, patches);
    });
}
function diffAttr(oldAttrs, newAttrs) {
    let attrsPatch = {};
    for (let attr in oldAttrs) {
        //如果说老的属性和新属性不一样。一种是值改变 ,一种是属性被删除 了
        if (oldAttrs[attr] != newAttrs[attr]) {
            console.log(1)
            attrsPatch[attr] = newAttrs[attr];
        }
    }
    for (let attr in newAttrs) {
        if (!oldAttrs.hasOwnProperty(attr)) {
            attrsPatch[attr] = newAttrs[attr];
        }
    }
    return attrsPatch;
}
export{diff} ;

patch.js(补丁包,将虚拟dom转成真实dom)

import utils from "./utils.js";

let keyIndex = 0;
let allPatches; //  这里就是完整的补丁包

function patch(root, patches) {
  allPatches = patches;
  walk(root);
}

function walk(node) {
  let currentPatches = allPatches[keyIndex++];
  (node.childNodes || []).forEach((child) => walk(child));
  if (currentPatches) {
    doPatch(node, currentPatches);
  }
}

function doPatch(node, currentPatches) {
  currentPatches.forEach((patch) => {
    switch (patch.type) {
      case utils.ATTRS:
        for (let attr in patch.attrs) {
          let value = patch.attrs[attr];
          if (value) {
            utils.setAttr(node, attr, value);
          } else {
            node.removeAttribute(attr);
          }
        }
        break;
      case utils.TEXT:
        node.textContent = patch.content;
        break;
      case utils.REPLACE:
        let newNode =
          patch.node instanceof Element
            ? path.node.render()
            : document.createTextNode(path.node);
        node.parentNode.replaceChild(newNode, node);
        break;
      case utils.REMOVE:
        node.parentNode.removeChild(node);
        break;
    }
  });
}

export { patch };

utils.js工具函数

let utils = {
    //元素变化有哪些种类
    REMOVE: 'REMOVE',//此节点被移除
    ATTRS: "ATTRS",//属性被改变
    TEXT: "TEXT",//文本内容被改变
    REPLACE: "REPLACE", //节点要被整个替换  
    setAttr(element, attr, value) {
        switch (attr) {
            case 'style':
                element.style.cssText = value;
                break;
            case 'value':
                let tagName = element.tagName.toLowerCase();
                if (tagName == 'input' || tagName == 'textarea') {
                    element.value = value;
                } else {
                    element.setAttribute(attr, value);
                }
                break;
            default:
                element.setAttribute(attr, value);
                break;
        }

    },
    type(obj) {
        // [object String]
        return Object.prototype.toString.call(obj).replace(/\[object\s|\]/g, '');
    },
    isString(str) {
        return utils.type(str) == 'String';
    }
}
export default utils;

index.js 入口文件

import { createElement } from "./element.js";
import { diff } from "./diff.js";
import { patch} from './patch.js';

let dom1 = createElement("ul", { class: "list" }, [
  createElement("li", { class: "item" }, ["1"]),
  createElement("li", { class: "item" }, ["2"]),
  createElement("li", { class: "item" }, ["3"]),
  createElement("li", { class: "item" }, ["4"]),
]);
let root = dom1.render();
document.body.appendChild(root)

let dom2 = createElement("ul", { class: "list-new" }, [
  createElement("li", { class: "item" }, ["1"]),
  createElement("li", { class: "item" }, ["9"]),
  createElement("li", { class: "item" }, ["3"]),
]);

let patches = diff(dom1, dom2);

console.log('补丁包',patches)
patch(root,patches)

index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>dom diff</title>
</head>
<body>
    <script type="module" src="./index.js"></script>
</body>
</html>