React diff算法的实现

444 阅读3分钟

因为虚拟dom是用babel进行转义的,所以这里我简单直接写入虚拟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);
      }

      // 将虚拟dom渲染成真实dom
      function render(dom) {
        const tag = document.createElement(dom.type);
        setAttr(tag, dom.props);
        // 遍历children
        dom.children &&
          dom.children.forEach(item => {
            if (item instanceof Element) {
              tag.appendChild(render(item));
            } else {
              tag.textContent = item;
            }
          });
        return tag;
      }
      // 设置属性
      function setAttr(tag, attr) {
        for (let i in attr) {
          // if else的判断是为了增添删除属性
          if (i === "className") {
            if (attr[i]) {
              tag.setAttribute("class", attr[i]);
            } else {
              tag.removeAttribute("class");
            }
          } else if (i === "style") {
            if (attr[i]) {
              Object.assign(tag.style, attr[i]);
            } else {
              Object.assign(tag.style, {});
            }
          } else {
            if (attr[i]) {
              tag.setAttribute(i, attr[i]);
            } else {
              tag.removeAttribute(i);
            }
          }
        }
      }
      let num = 0;
      function diff(dom1, dom2) {
        let patchs = {};  //用于记录那个属性被改变
        let index = 0;  //用于记录第几次diff
        compiner(dom1, dom2, patchs, index);
        return patchs;
      }
      // 判读字节点是否为文本的函数
      function isString(node) {
        return Object.prototype.toString.call(node) === "[object String]";
      }
      // 对props进行diff算法
      function diffProps(oldNode, newNode) {
        const ptach = {};
        /*
        先遍历旧的props 若旧props的第n项不同于新props第n项 取新的props第n项key value
        先遍历新的props 若旧props没有新props里的属性 取新的props增添的key value
        其实应该为props排个序,我这里一开始没太注意,所以忘了
        */
        for (let key in oldNode) {
          // 这里转化为json字符串是为了比较style,因为style的值为一个对象
          // {}!=={}永远成立,所以就算style相同也会判断为不同,需要转化一下
          if (JSON.stringify(newNode[key]) !== JSON.stringify(oldNode[key])) {
            
            ptach[key] = newNode[key];
          }
        }
        for (let key in newNode) {
          if (!oldNode.hasOwnProperty(key)) {
            ptach[key] = newNode[key];
          }
        }
        return ptach;
      }
      /*
      对children进行diff算法比较 这里利用递归对children的每一项进行初始type比较
      判断旧children与新children的长度 若旧dom长则对旧dom进行遍历 这里可以看compiner函数
      */
      function diffChildren(oldChildren, newChildren, patchs) {
        const oldLen = oldChildren && oldChildren.length;
        const newLen = newChildren && newChildren.length;
        if (oldLen > newLen || oldLen === newLen) {
          oldChildren.forEach((item, index) => {
            compiner(item, newChildren[index], patchs, ++num);
          });
        } else {
          newChildren.forEach((item, index) => {
            if(oldChildren[index]){
              compiner(oldChildren[index], item, patchs, ++num);
            }else{
              compiner(oldChildren[index], item, patchs, num);

            }
          });
        }
      }
      // diff算法
      function compiner(oldNode, newNode, patchs, index) {
        let current = [];
        // 旧dom不存在 说明是新添了dom
        if (!oldNode) {
          current.push({ type: "REPLACE", node: newNode });
        } else if (!newNode) {
          // 新dom不存在 说明删除了dom
          current.push({ type: "REMOVE", index });
        } else if (isString(oldNode) && isString(newNode)) {
          // 新旧dom为文本 比较文本值是否相同 不相同则更换文本
          if(oldNode!==newNode){
            current.push({ type: "TEXT", text: newNode });
          }
        } else if (oldNode.type === newNode.type) {
          const attr = diffProps(oldNode.props, newNode.props);
          Object.keys(attr).length && current.push({ type: "ATTR", attr });
          diffChildren(oldNode.children, newNode.children, patchs);
        }else{
          // 新旧dom的标签不同 则新dom完全替换旧dom
          current.push({type:'REPLACE',node:newNode})
        }
        if (current.length) {
          if (Object.keys(patchs).length === index) {
            for (let i of current) {
              patchs[index].push(i);
            }
          } else {
            patchs[index] = current;
          }
        }
      }
      // 创建两个虚拟dom并把dom1渲染成真实dom
      const dom1=createElement('div',{className:"div1"},['我是div1',createElement('p',{style:{color:'red'}},['我是p'])])
      const dom2=createElement('div',{className:"div2"},['我是div2'
      ])
      const truelyDom1=render(dom1)
      document.body.appendChild(truelyDom1)
      const diffrance=diff(dom1,dom2)
      let count=0;

      // 将diff算法后的结果与旧的真实dom进行比对
      walkPach(truelyDom1,diffrance)
      function walkPach(dom){
        const current=diffrance[count++];
        const childNodes=dom.childNodes
        childNodes && childNodes.forEach((item,index)=>{
          walkPach(item)
        })
        if(current){
          doPach(dom,current)
        }
      }
      function doPach(node,current){
        const parentNode=node.parentNode
        // 四种情况四种不同的操作
        current.forEach((item,index)=>{
          if(item.type==='TEXT'){
            node.textContent=item.text
          }
          else if(item.type==='ATTR'){
            setAttr(node,item.attr)
          }
          else if(item.type==='REPLACE'){
            index===0 && parentNode.replaceChild(item.node,node)
            index!==0 && parentNode.appendChild(render(item.node))
          }else if(item.type==='REMOVE'){
            parentNode.removeChild(node)
          }
        })

      }

目前为止并不完善,今后会一步一步将之修改完美

gitHub地址:github.com/mayu888/Rea…