vue/react virtual dom

64 阅读2分钟

背景

粗略实现

大致思想

  • 创建虚拟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);
          }
          let virtualDomOld = createElement('ul', {
            class: 'list'
          }, [
            createElement('li', {
              class: 'item'
            }, ['a']),
            createElement('li', {
              class: 'item'
            }, ['b']),
            createElement('li', {
              class: 'item'
            }, ['c']),
            createElement('li', {
              class: 'item'
            }, ['d']),
          ]);
          let virtualDomONew = createElement('ul', {
            class: 'list-group'
          }, [
            createElement('li', {
              class: 'item'
            }, ['1']),
            createElement('li', {
              class: 'item'
            }, ['0']),
            createElement('li', {
              class: 'item'
            }, ['1']),
            createElement('li', {
              class: 'item'
            }, ['1']),
          ]);
          var ATTRS = 'ATTRS',
            TEXT = 'TEXT',
            REMOVE = 'REMOVE',
            REPLACE = 'REPLACE',
            INDEX = 0;
          let currentPatch = [];

          let allPatches;
          let index = 0;
          let el = render(virtualDomOld);
          renderDom(el, window.root)
          let patches = diff(virtualDomOld, virtualDomONew);
          console.log(patches, 'get')
          patch(el, patches)

          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;
              default: node.setAttribute(key, value);
                break;
            }
          }
          function render(eleObj) {
            let el = document.createElement(eleObj.type);
            for (let key in eleObj.props) {
              setAttr(el, key, eleObj.props[key])
            }
            eleObj.children.forEach(child => {
              child = (child instanceof Element) ? render(child) : document.createTextNode(child)
              el.appendChild(child)
            })
            return el;
          }
          function renderDom(el, target) {
            target.appendChild(el)
          }
          /**----------------------------------------------------------------**/
          // dom diff

          /**
     * 先序深度优先遍历
     * 
     * 当节点类型相同时=>查看属性是否相同=>产生属性的补丁包{type:'ATTRS',attrs:['class':'list-group'}}
     * 新的dom节点不存在{type:'REMOVE':index}
     * 节点类型不相同,直接采用替换模式{type:'REPLACE,newNode:newNode'}
     * 文本的变化{type:'TEXT',text:1}
     * 
     * 
     **/


          function diff(oldTree, newTree) {
            let patches = {},
              index = 0;
            walk(oldTree, newTree, index, patches);
            return patches;
          }
          function diffAttrs(oldAttrs, newAttrs) {
            let patch = {};
            for (let key in oldAttrs) {
              if (oldAttrs[key] !== newAttrs[key]) {
                patch[key] = newAttrs[key];
              }
            }
            for (let key in newAttrs) {
              if (! oldAttrs.hasOwnProperty(key)) {
                patch[key] = newAttrs[key];
              }
            }
            return patch;
          }

          function diffChildren(oldChildren, newChildren, patches) { // diff old first and new first
            oldChildren.forEach((child, index) => {
              walk(child, newChildren[index], INDEX++, patches)
            })
          }
          function isString(node) {
            return Object.prototype.toString.call(node) === '[object String]'
          }
          function walk(oldNode, newNode, index, patches) {

            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 = diffAttrs(oldNode.props, newNode.props)
              if (Object.keys(attrs).length > 0) {
                currentPatch.push({type: ATTRS, attrs})
              }
              // children
              diffChildren(oldNode.children, newNode.children, patches);
            } else {
              currentPatch.push({type: REPLACE, newNode})
            }
            if (currentPatch.length > 0) {
              patches[index] = currentPatch;
            }
          }

          function patch(node, patches) {
            allPatches = patches;
            walkPatch(node)
          }
          function walkPatch(node) {
            let currentPatch = allPatches[index++];
            let childNodes = node.childNodes;
            childNodes.forEach(child => {
              console.log(child, 'time')
              walkPatch(child)
            })
            if (currentPatch) {
              doPatch(node, currentPatch)
            }
          }
          function doPatch(node, patches) {
            patches.forEach(patch => {
              switch (patch.type) {
                case 'ATTRS':
                  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 'REPLACE':
                  let newNode = (patch.newNode instanceof Element) ? render(patch.newNode) : document.createTextNode(patch.newNode)
                  node.parentNode.replaceChild(newNode, node);
                  break;
                case 'REMOVE': node.parentNode.removeChild(node)
                  break;
                default:
                  break;
              }
            })
          }

思考

  • 虚拟dom真的比直接操作dom快?
  • 虚拟dom的优点?