一文搞懂虚拟DOM和DOM-diff
Keep Moving
神奇的虚拟DOM
实现一下虚拟DOM
    
    
      
      
    
     
       
       
     // 全局安装
     
       
       
     
    npm i create-react-app -g
     
       
       
     
    
     
       
       
     // 生成项目
     
       
       
     
    create-react-app dom-diff
     
       
       
     
    
     
       
       
     // 进入项目目录
     
       
       
     
    cd dom-diff
     
       
       
     
    
     
       
       
     // 编译
     
       
       
     
    npm run start
     
       
       
     
    
      
      
      
   创建虚拟DOM
    
    
      
      
    
     
       
       
     // element.js
     
       
       
     
    
     
       
       
     
    
     
       
       
     // 虚拟DOM元素的类,构建实例对象,用来描述DOM
     
       
       
     
    
     
       
       
     class Element {
     
       
       
     
        
     
       
       
     constructor(type, props, children) {
     
       
       
     
            
     
       
       
     this.type = type;
     
       
       
     
            
     
       
       
     this.props = props;
     
       
       
     
            
     
       
       
     this.children = children;
     
       
       
     
        }
     
       
       
     
    }
     
       
       
     
    
     
       
       
     // 创建虚拟DOM,返回虚拟节点(object)
     
       
       
     
    
     
       
       
     function createElement(type, props, children) {
     
       
       
     
        
     
       
       
     return 
     
       
       
     new Element(type, props, children);
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     export {
     
       
       
     
        Element,
     
       
       
     
        createElement
     
       
       
     
    }
     
       
       
     
    
      
      
      
   调用createElement方法
    
    
      
      
    
     
       
       
     // index.js
     
       
       
     
    
     
       
       
     
    
     
       
       
     // 首先引入对应的方法来创建虚拟DOM
     
       
       
     
    
     
       
       
     import { createElement } 
     
       
       
     from 
     
       
       
     './element';
     
       
       
     
    
     
       
       
     
    
     
       
       
     let virtualDom = createElement(
     
       
       
     'ul', {
     
       
       
     class: 
     
       
       
     'list'}, [
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '周杰伦']),
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '林俊杰']),
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '王力宏'])
     
       
       
     
    ]);
     
       
       
     
    
     
       
       
     
    
     
       
       
     console.log(virtualDom);
     
       
       
     
    
      
      
      
   -  
     参数分析 
-  
     type: 指定元素的标签类型,如'li', 'div', 'a'等 
-  
     props: 表示指定元素身上的属性,如class, style, 自定义属性等 
-  
     children: 表示指定元素是否有子节点,参数以数组的形式传入 
渲染虚拟DOM
    
    
      
      
    
     
       
       
     // element.js
     
       
       
     
    
     
       
       
     
    
     
       
       
     class Element {
     
       
       
     
        
     
       
       
     // 省略
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    functioncreateElement() {
     
       
       
     
        
     
       
       
     // 省略
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     // render方法可以将虚拟DOM转化成真实DOM
     
       
       
     
    
     
       
       
     function render(domObj) {
     
       
       
     
        
     
       
       
     // 根据type类型来创建对应的元素
     
       
       
     
        
     
       
       
     let el = 
     
       
       
     document.createElement(domObj.type);
     
       
       
     
        
     
       
       
     
        
     
       
       
     // 再去遍历props属性对象,然后给创建的元素el设置属性
     
       
       
     
        
     
       
       
     for (
     
       
       
     let key 
     
       
       
     in domObj.props) {
     
       
       
     
            
     
       
       
     // 设置属性的方法
     
       
       
     
            setAttr(el, key, domObj.props[key]);
     
       
       
     
        }
     
       
       
     
        
     
       
       
     
        
     
       
       
     // 遍历子节点
     
       
       
     
        
     
       
       
     // 如果是虚拟DOM,就继续递归渲染
     
       
       
     
        
     
       
       
     // 不是就代表是文本节点,直接创建
     
       
       
     
        domObj.children.forEach(
     
       
       
     child => {
     
       
       
     
            child = (child 
     
       
       
     instanceof Element) ? render(child) : 
     
       
       
     document.createTextNode(child);
     
       
       
     
            
     
       
       
     // 添加到对应元素内
     
       
       
     
            el.appendChild(child);
     
       
       
     
        });
     
       
       
     
    
     
       
       
     
        
     
       
       
     return el;
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     // 设置属性
     
       
       
     
    functionsetAttr(node, key, value) {
     
       
       
     
        
     
       
       
     switch(key) {
     
       
       
     
            
     
       
       
     case
     
       
       
     'value':
     
       
       
     
                
     
       
       
     // node是一个input或者textarea就直接设置其value即可
     
       
       
     
                
     
       
       
     if (node.tagName.toLowerCase() === 
     
       
       
     'input' ||
     
       
       
     
                    node.tagName.toLowerCase() === 
     
       
       
     '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);
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     export {
     
       
       
     
        Element,
     
       
       
     
        createElement,
     
       
       
     
        render,
     
       
       
     
        setAttr,
     
       
       
     
        renderDom
     
       
       
     
    };
     
       
       
     
    
      
      
      
   调用render方法
    
    
      
      
    
     
       
       
     // index.js
     
       
       
     
    
     
       
       
     
    
     
       
       
     // 引入createElement、render和renderDom方法
     
       
       
     
    
     
       
       
     import { createElement, render, renderDom } 
     
       
       
     from 
     
       
       
     './element';
     
       
       
     
    
     
       
       
     
    
     
       
       
     let virtualDom = createElement(
     
       
       
     'ul', {
     
       
       
     class: 
     
       
       
     'list'}, [
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '周杰伦']),
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '林俊杰']),
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '王力宏'])
     
       
       
     
    ]);
     
       
       
     
    
     
       
       
     
    
     
       
       
     console.log(virtualDom);
     
       
       
     
    
     
       
       
     
    
     
       
       
     // +++
     
       
       
     
    
     
       
       
     let el = render(virtualDom); 
     
       
       
     // 渲染虚拟DOM得到真实的DOM结构
     
       
       
     
    
     
       
       
     console.log(el);
     
       
       
     
    
     
       
       
     // 直接将DOM添加到页面内
     
       
       
     
    renderDom(el, 
     
       
       
     document.getElementById(
     
       
       
     'root'));
     
       
       
     
    
     
       
       
     // +++
     
       
       
     
    
      
      
      
   DOM-diff闪亮登场
    
    
      
      
    
     
       
       
     // diff.js
     
       
       
     
    
     
       
       
     
    
     
       
       
     function diff(oldTree, newTree) {
     
       
       
     
        
     
       
       
     // 声明变量patches用来存放补丁的对象
     
       
       
     
        
     
       
       
     let patches = {};
     
       
       
     
        
     
       
       
     // 第一次比较应该是树的第0个索引
     
       
       
     
        
     
       
       
     let index = 
     
       
       
     0;
     
       
       
     
        
     
       
       
     // 递归树 比较后的结果放到补丁里
     
       
       
     
        walk(oldTree, newTree, index, patches);
     
       
       
     
    
     
       
       
     
        
     
       
       
     return patches;
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     function walk(oldNode, newNode, index, patches) {
     
       
       
     
        
     
       
       
     // 每个元素都有一个补丁
     
       
       
     
        
     
       
       
     let current = [];
     
       
       
     
    
     
       
       
     
        
     
       
       
     if (!newNode) { 
     
       
       
     // rule1
     
       
       
     
            current.push({ 
     
       
       
     type: 
     
       
       
     'REMOVE', index });
     
       
       
     
        } elseif (isString(oldNode) && isString(newNode)) {
     
       
       
     
            
     
       
       
     // 判断文本是否一致
     
       
       
     
            
     
       
       
     if (oldNode !== newNode) {
     
       
       
     
                current.push({ 
     
       
       
     type: 
     
       
       
     'TEXT', 
     
       
       
     text: newNode });
     
       
       
     
            }
     
       
       
     
    
     
       
       
     
        } elseif (oldNode.type === newNode.type) {
     
       
       
     
            
     
       
       
     // 比较属性是否有更改
     
       
       
     
            
     
       
       
     let attr = diffAttr(oldNode.props, newNode.props);
     
       
       
     
            
     
       
       
     if (
     
       
       
     Object.keys(attr).length > 
     
       
       
     0) {
     
       
       
     
                current.push({ 
     
       
       
     type: 
     
       
       
     'ATTR', attr });
     
       
       
     
            }
     
       
       
     
            
     
       
       
     // 如果有子节点,遍历子节点
     
       
       
     
            diffChildren(oldNode.children, newNode.children, patches);
     
       
       
     
        } 
     
       
       
     else {    
     
       
       
     // 说明节点被替换了
     
       
       
     
            current.push({ 
     
       
       
     type: 
     
       
       
     'REPLACE', newNode});
     
       
       
     
        }
     
       
       
     
        
     
       
       
     
        
     
       
       
     // 当前元素确实有补丁存在
     
       
       
     
        
     
       
       
     if (current.length) {
     
       
       
     
            
     
       
       
     // 将元素和补丁对应起来,放到大补丁包中
     
       
       
     
            patches[index] = current;
     
       
       
     
        }
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     function isString(obj) {
     
       
       
     
        
     
       
       
     return 
     
       
       
     typeof obj === 
     
       
       
     'string';
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     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;
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     // 所有都基于一个序号来实现
     
       
       
     
    
     
       
       
     let num = 
     
       
       
     0;
     
       
       
     
    
     
       
       
     
    
     
       
       
     function diffChildren(oldChildren, newChildren, patches) {
     
       
       
     
        
     
       
       
     // 比较老的第一个和新的第一个
     
       
       
     
        oldChildren.forEach(
     
       
       
     (child, index) => {
     
       
       
     
            walk(child, newChildren[index], ++num, patches);
     
       
       
     
        });
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     // 默认导出
     
       
       
     
    
     
       
       
     export 
     
       
       
     default diff;
     
       
       
     
    
      
      
      
   比较规则
-  
     新的DOM节点不存在{type: 'REMOVE', index} 
-  
     文本的变化{type: 'TEXT', text: 1} 
-  
     当节点类型相同时,去看一下属性是否相同,产生一个属性的补丁包{type: 'ATTR', attr: {class: 'list-group'}} 
-  
     节点类型不相同,直接采用替换模式{type: 'REPLACE', newNode} 
-  
     每个元素都有一个补丁,所以需要创建一个放当前补丁的数组 
-  
     如果没有new节点的话,就直接将type为REMOVE的类型放到当前补丁里 
    
    
      
      
    
     
       
       
     if (!newNode) {
     
       
       
     
            current.push({ 
     
       
       
     type: 
     
       
       
     'REMOVE', index });
     
       
       
     
        }
     
       
       
     
    
      
      
      
   -  
     如果新老节点是文本的话,判断一下文本是否一致,再指定类型TEXT并把新节点放到当前补丁 
    
      
      
    
     
       
       
     elseif (isString(oldNode) && isString(newNode)) {
     
       
       
     
            
     
       
       
     if (oldNode !== newNode) {
     
       
       
     
                current.push({ 
     
       
       
     type: 
     
       
       
     'TEXT', 
     
       
       
     text: newNode });
     
       
       
     
            }
     
       
       
     
        }
     
       
       
     
    
      
      
      
   -  
     如果新老节点的类型相同,那么就来比较一下他们的属性props 
-  
     属性比较 
-  
     diffAttr 
-  
     去比较新老Attr是否相同 
-  
     把newAttr的键值对赋给patch对象上并返回此对象 
-  
     然后如果有子节点的话就再比较一下子节点的不同,再调一次walk 
-  
     diffChildren 
-  
     遍历oldChildren,然后递归调用walk再通过child和newChildren[index]去diff 
    
      
      
    
     
       
       
     elseif (oldNode.type === newNode.type) {
     
       
       
     
            
     
       
       
     // 比较属性是否有更改
     
       
       
     
            
     
       
       
     let attr = diffAttr(oldNode.props, newNode.props);
     
       
       
     
            
     
       
       
     if (
     
       
       
     Object.keys(attr).length > 
     
       
       
     0) {
     
       
       
     
                current.push({ 
     
       
       
     type: 
     
       
       
     'ATTR', attr });
     
       
       
     
            }
     
       
       
     
            
     
       
       
     
            
     
       
       
     // 如果有子节点,遍历子节点
     
       
       
     
            diffChildren(oldNode.children, newNode.children, patches);
     
       
       
     
        }
     
       
       
     
    
      
      
      
   -  
     上面三个如果都没有发生的话,那就表示节点单纯的被替换了,type为REPLACE,直接用newNode替换即可 
    
    
      
      
    
     
       
       
     else {
     
       
       
     
            current.push({ 
     
       
       
     type: 
     
       
       
     'REPLACE', newNode});
     
       
       
     
        }
     
       
       
     
    
      
      
      
   -  
     当前补丁里确实有值的情况,就将对应的补丁放进大补丁包里 
    
    
      
      
    
     
       
       
     if (current.length > 
     
       
       
     0) {
     
       
       
     
            
     
       
       
     // 将元素和补丁对应起来,放到大补丁包中
     
       
       
     
            patches[index] = current;
     
       
       
     
        }
     
       
       
     
    
      
      
      
   patch补丁更新
    
    
      
      
    
     
       
       
     import { Element, render, setAttr } 
     
       
       
     from 
     
       
       
     './element';
     
       
       
     
    
     
       
       
     
    
     
       
       
     let allPatches;
     
       
       
     
    
     
       
       
     let index = 
     
       
       
     0;  
     
       
       
     // 默认哪个需要打补丁
     
       
       
     
    
     
       
       
     
    
     
       
       
     function patch(node, patches) {
     
       
       
     
        allPatches = patches;
     
       
       
     
        
     
       
       
     
        
     
       
       
     // 给某个元素打补丁
     
       
       
     
        walk(node);
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     function walk(node) {
     
       
       
     
        
     
       
       
     let current = allPatches[index++];
     
       
       
     
        
     
       
       
     let childNodes = node.childNodes;
     
       
       
     
    
     
       
       
     
        
     
       
       
     // 先序深度,继续遍历递归子节点
     
       
       
     
        childNodes.forEach(
     
       
       
     child => walk(child));
     
       
       
     
    
     
       
       
     
        
     
       
       
     if (current) {
     
       
       
     
            doPatch(node, current); 
     
       
       
     // 打上补丁
     
       
       
     
        }
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    functiondoPatch(node, patches) {
     
       
       
     
        
     
       
       
     // 遍历所有打过的补丁
     
       
       
     
        patches.forEach(
     
       
       
     patch => {
     
       
       
     
            
     
       
       
     switch (patch.type) {
     
       
       
     
                
     
       
       
     case
     
       
       
     'ATTR':
     
       
       
     
                    
     
       
       
     for (
     
       
       
     let key 
     
       
       
     in patch.attr) {
     
       
       
     
                        
     
       
       
     let value = patch.attr[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;
     
       
       
     
                    newNode = (newNode 
     
       
       
     instanceof Element) ? render(newNode) : 
     
       
       
     document.createTextNode(newNode);
     
       
       
     
                    node.parentNode.replaceChild(newNode, node);
     
       
       
     
                    
     
       
       
     break;
     
       
       
     
                
     
       
       
     case
     
       
       
     'REMOVE':
     
       
       
     
                    node.parentNode.removeChild(node);
     
       
       
     
                    
     
       
       
     break;
     
       
       
     
                
     
       
       
     default:
     
       
       
     
                    
     
       
       
     break;
     
       
       
     
            }
     
       
       
     
        });
     
       
       
     
    }
     
       
       
     
    
     
       
       
     
    
     
       
       
     export 
     
       
       
     default patch;
     
       
       
     
    
      
      
      
   patch做了什么?
-  
     用一个变量来得到传递过来的所有补丁allPatches 
-  
     patch方法接收两个参数(node, patches) 
-  
     在方法内部调用walk方法,给某个元素打上补丁 
-  
     walk方法里获取所有的子节点 
-  
     给子节点也进行先序深度优先遍历,递归walk 
-  
     如果当前的补丁是存在的,那么就对其打补丁(doPatch) 
-  
     doPatch打补丁方法会根据传递的patches进行遍历 
-  
     判断补丁的类型来进行不同的操作 
-  
     属性ATTR for in去遍历attrs对象,当前的key值如果存在,就直接设置属性setAttr;如果不存在对应的key值那就直接删除这个key键的属性 
-  
     文字TEXT 直接将补丁的text赋值给node节点的textContent即可 
-  
     替换REPLACE 新节点替换老节点,需要先判断新节点是不是Element的实例,是的话调用render方法渲染新节点; 
-  
     删除REMOVE 直接调用父级的removeChild方法删除该节点 
-  
     将patch方法默认导出方便调用 
回归
    
    
      
      
    
     
       
       
     // index.js
     
       
       
     
    
     
       
       
     
    
     
       
       
     import { createElement, render, renderDom } 
     
       
       
     from 
     
       
       
     './element';
     
       
       
     
    
     
       
       
     // +++ 引入diff和patch方法
     
       
       
     
    
     
       
       
     import diff 
     
       
       
     from 
     
       
       
     './diff';
     
       
       
     
    
     
       
       
     import patch 
     
       
       
     from 
     
       
       
     './patch';
     
       
       
     
    
     
       
       
     // +++
     
       
       
     
    
     
       
       
     
    
     
       
       
     let virtualDom = createElement(
     
       
       
     'ul', {
     
       
       
     class: 
     
       
       
     'list'}, [
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '周杰伦']),
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '林俊杰']),
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '王力宏'])
     
       
       
     
    ]);
     
       
       
     
    
     
       
       
     
    
     
       
       
     let el = render(virtualDom);
     
       
       
     
    renderDom(el, 
     
       
       
     window.root);
     
       
       
     
    
     
       
       
     
    
     
       
       
     // +++
     
       
       
     
    
     
       
       
     // 创建另一个新的虚拟DOM
     
       
       
     
    
     
       
       
     let virtualDom2 = createElement(
     
       
       
     'ul', {
     
       
       
     class: 
     
       
       
     'list-group'}, [
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item active'}, [
     
       
       
     '七里香']),
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '一千年以后']),
     
       
       
     
        createElement(
     
       
       
     'li', {
     
       
       
     class: 
     
       
       
     'item'}, [
     
       
       
     '需要人陪'])
     
       
       
     
    ]);
     
       
       
     
    
     
       
       
     // diff一下两个不同的虚拟DOM
     
       
       
     
    
     
       
       
     let patches = diff(virtualDom, virtualDom2);
     
       
       
     
    
     
       
       
     console.log(patches);
     
       
       
     
    
     
       
       
     // 将变化打补丁,更新到el
     
       
       
     
    patch(el, patches);
     
       
       
     
    
     
       
       
     // +++
     
       
       
     
    
      
      
      
   四句话
-  
     用JS对象模拟DOM(虚拟DOM) 
-  
     把此虚拟DOM转成真实DOM并插入页面中(render) 
-  
     如果有事件发生修改了虚拟DOM,比较两棵虚拟DOM树的差异,得到差异对象(diff) 
-  
     把差异对象应用到真正的DOM树上(patch) 
