第十章-双端diff算法

52 阅读3分钟
  • 比较头部

  • 比较尾部

  • 比较新尾旧头

  • 比较新头旧尾

  • 找不到遍历旧vnode寻找新头

    • 找到, 插入Dom头部, 原位置置为undefined;
    • 找不到, 挂载插入Dom头部
  • 旧节点比对完, 但是新节点还没有遍历完, 需要加载节点

  • 新节点比对完, 但是旧节点还没有遍历完, 需要删除节点

if(Array.isArray(n2.children)) {   // 2、新节点是数组节点
      if(Array.isArray(n1.children)) {       // 2.1、节点是数组节点   // 这里是diff算法, 暂时全部卸载后再更新
        let oldChildren = n1.children;
        let newChildren = n2.children;
        let oldStartIdx = 0;
        let oldEndIdx = oldChildren.length - 1;
        let newStartIdx = 0;
        let newEndIdx = newChildren.length - 1;
        let oldStartVnode = oldChildren[oldStartIdx];
        let oldEndVnode = oldChildren[oldEndIdx];
        let newStartVnode = newChildren[newStartIdx];
        let newEndVnode = newChildren[newEndIdx];
        while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {{
          if(!oldStartVnode) {
            oldStartVnode = oldChildren[++oldStartIdx];
          } else if(!oldEndVnode) {
            oldEndVnode = oldChildren[--oldEndIdx];
          } else if(oldStartVnode.key === newStartVnode.key) { // 如果旧始节点和新始节点相同
            patch(oldStartVnode, newStartVnode, container);
            oldStartVnode = oldChildren[++oldStartIdx];
            newStartVnode = newChildren[++newStartIdx]
          } else if(oldEndVnode.key === newEndVnode.key) { // 如果旧末节点和新末节点相同
            patch(oldEndVnode, oldEndVnode, container);
            oldEndVnode = oldChildren[--oldEndIdx];
            newEndVnode = newChildren[--newEndIdx]
          } else if(oldStartVnode.key === newEndVnode.key) { // 如果新末节点和旧始节点相同
            patch(oldStartVnode, newEndVnode, container);
            insert(oldStartVnode.el, container, oldEndVnode.el.nextSibling);
            oldStartVnode = oldChildren[++oldStartIdx];
            newEndVnode = newChildren[--newEndIdx];
          } else if(oldEndVnode.key === newStartVnode.key) {  // 如果旧末节点和新始节点相同
            patch(oldEndVnode, newStartVnode, container);
            insert(oldEndVnode.el, container, oldStartVnode.el);
            oldEndVnode = oldChildren[--oldEndIdx];
            newStartVnode = newChildren[++newStartIdx];
          } else {
            // 双端对比没有结果, 则寻找新始节点在旧DOM中的位置
             let idxInOld = oldChildren.findIndex(item => {
                return item.key === newStartVnode.key
             });
             // 如果能找到
             if(idxInOld !== -1) {
               let vnodeToMove = oldChildren[idxInOld];
               patch(vnodeToMove, newStartVnode, container);
               insert(vnodeToMove.el, container, oldStartVnode.el);
               oldChildren[idxInOld] = null;
             // 找不到则进行挂载
             } else {
               patch(null, newStartVnode, container, oldStartVnode.el);
            
             }
            newStartVnode = newChildren[++newStartIdx]
          }
        }}
        // 旧节点比对完, 但是新节点还没有遍历完, 需要加载节点
        if(oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
          for(let i = newStartIdx; i <= newEndIdx; i++) {
            patch(null, newChildren[i], container, oldStartVnode.el);
          }
        // 新节点比对完, 但是旧节点还没有遍历完, 需要删除节点
        } else if(newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
           for(let i = oldStartIdx; i < oldEndIdx; i++) {
             unmount(oldChildren[i])
           }
        }
      }

总结

const { effect, ref} =  VueReactivity;

const Text = Symbol();
const Comment = Symbol();
const Fragment = Symbol();
function createRenderer(options) {
  const{ createElement, setElementText, insert, patchProps, unmount, setText, createText } = options;
  function mountElement(vnode, container, anchor) {
    const el = vnode.el =  createElement(vnode.type);
    if(typeof vnode.children === "string") {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el, anchor)
      })
    }
    if(vnode.props) {
      for(let key in vnode.props) {
        patchProps(el, key, null, vnode.props[key])
      }
    }
    insert(el, container);
  }
  
  function patchElement(n1, n2) {
    const el = n2.el = n1.el;
    const oldProps = n1.props;
    const newProps = n1.props;
    for(const key in newProps) {
      if(newProps[key] !== oldProps[key]){
        patchProps(el, key, oldProps[key], newProps[key])
      }
    }
    for(const key in oldProps) {
      if(!(key in newProps)) {
        patchProps(el, key, oldProps[key], null)
      }
    }
    patchChildren(n1, n2, el)
  }
  function  patchChildren(n1, n2, container) {
    if(typeof n2.children === "string"){    // 1、新节点是文本节点, 1.1、没有节点 直接设置 1.2、文本节点 直接设置  1.3、数组节点 卸载新增
      if(Array.isArray(n1.children)){
        n1.children.forEach(child => unmount(child))
      }
      setElementText(container, n2.children);
    } else if(Array.isArray(n2.children)) {   // 2、新节点是数组节点
      if(Array.isArray(n1.children)) {       // 2.1、节点是数组节点   // 这里是diff算法, 暂时全部卸载后再更新
        let oldChildren = n1.children;
        let newChildren = n2.children;
        let oldStartIdx = 0;
        let oldEndIdx = oldChildren.length - 1;
        let newStartIdx = 0;
        let newEndIdx = newChildren.length - 1;
        let oldStartVnode = oldChildren[oldStartIdx];
        let oldEndVnode = oldChildren[oldEndIdx];
        let newStartVnode = newChildren[newStartIdx];
        let newEndVnode = newChildren[newEndIdx];
        while (newStartIdx <= newEndIdx && oldStartIdx <= oldEndIdx) {{
          if(!oldStartVnode) {
            oldStartVnode = oldChildren[++oldStartIdx];
          } else if(!oldEndVnode) {
            oldEndVnode = oldChildren[--oldEndIdx];
          } else if(oldStartVnode.key === newStartVnode.key) { // 如果旧始节点和新始节点相同
            patch(oldStartVnode, newStartVnode, container);
            oldStartVnode = oldChildren[++oldStartIdx];
            newStartVnode = newChildren[++newStartIdx]
          } else if(oldEndVnode.key === newEndVnode.key) { // 如果旧末节点和新末节点相同
            patch(oldEndVnode, oldEndVnode, container);
            oldEndVnode = oldChildren[--oldEndIdx];
            newEndVnode = newChildren[--newEndIdx]
          } else if(oldStartVnode.key === newEndVnode.key) { // 如果新末节点和旧始节点相同
            patch(oldStartVnode, newEndVnode, container);
            insert(oldStartVnode.el, container, oldEndVnode.el.nextSibling);
            oldStartVnode = oldChildren[++oldStartIdx];
            newEndVnode = newChildren[--newEndIdx];
          } else if(oldEndVnode.key === newStartVnode.key) {  // 如果旧末节点和新始节点相同
            patch(oldEndVnode, newStartVnode, container);
            insert(oldEndVnode.el, container, oldStartVnode.el);
            oldEndVnode = oldChildren[--oldEndIdx];
            newStartVnode = newChildren[++newStartIdx];
          } else {
            // 双端对比没有结果, 则寻找新始节点在旧DOM中的位置
             let idxInOld = oldChildren.findIndex(item => {
                return item.key === newStartVnode.key
             });
             // 如果能找到
             if(idxInOld !== -1) {
               let vnodeToMove = oldChildren[idxInOld];
               patch(vnodeToMove, newStartVnode, container);
               insert(vnodeToMove.el, container, oldStartVnode.el);
               oldChildren[idxInOld] = null;
             // 找不到则进行挂载
             } else {
               patch(null, newStartVnode, container, oldStartVnode.el);
            
             }
            newStartVnode = newChildren[++newStartIdx]
          }
        }}
        // 旧节点比对完, 但是新节点还没有遍历完, 需要加载节点
        if(oldEndIdx < oldStartIdx && newStartIdx <= newEndIdx) {
          for(let i = newStartIdx; i <= newEndIdx; i++) {
            patch(null, newChildren[i], container, oldStartVnode.el);
          }
        // 新节点比对完, 但是旧节点还没有遍历完, 需要删除节点
        } else if(newEndIdx < newStartIdx && oldStartIdx <= oldEndIdx) {
           for(let i = oldStartIdx; i < oldEndIdx; i++) {
             unmount(oldChildren[i])
           }
        }
      } else  {                              // 2.2、节点为空 不处理后挂载,2.3、文本节点 设置为空后挂载
        setElementText(container, "");
        n2.children.forEach(c => patch(null, c, container))
      }
    } else {  // 3、新节点为空, 3.1、没有节点 不用处理 3.2、文本节点 设置为空, 3.3、数组节点 直接卸载
      if(Array.isArray(n1.children)) {
        n1.children.forEach(child => unmount(child))
      } else if(typeof  n1.children === "string") {
        setElementText(container, "");
      }
    }
  }
  
  // n1旧vnode n2新vnode, container容器
  function patch(n1, n2, container, anchor) {
    if(n1 && n1.type !== n2.type) {
      unmount(n1);
      n1 = null;
    }
    let { type } = n2;
    if(typeof type === "string") {  // 节点是普通标签元素
      if(!n1) {
        mountElement(n2, container, anchor);  //挂载节点
      } else {
        patchElement(n1, n2); // 更新节点
      }
    } else if(typeof  type === "object"){  // 节点是组件
    
    } else if(type === Text){
      // 前面有判断,能进入到这个判断分支的, 要不节点为空, 要不同为Text节点
      if(!n1) {
        const el = n2.el = createText(n2.children);
        insert(el, container, null);
      } else {
        const el = n2.el = n1.el;
        if(n1.children !== n2.children) {
          setText(el, n2.children)
        }
      }
    } else if(type === Fragment) {
      if(!n1) {
        n2.children.forEach(c => patch(null, c, container))
      } else {
        patchChildren(n1, n2, container)
      }
    } else {
      // 省略了其他类型的vnode
    }
  }
  function render(vnode, container) {
    if(vnode){
      // 打补丁(挂载也是一种特殊的打补丁)
      patch(container._vnode, vnode, container)
    } else {
      if(container._vnode) {
        unmount(container._vnode);
      }
    }
    container._vnode = vnode;
  }
  return {
    render,
  }
}

function createElement(tag){
  return document.createElement(tag);
}
function setElementText(el, text){
  el.textContent = text;
}
function insert(el, parent, anchor = null) {
  parent.insertBefore(el, anchor)  // parent父节点 el需要插入的节点  anchor插入时需要插入在这个节点前面
}
function patchProps(el,key, oldValue, newValue) {
  // 暂时放在这里
  function  shouldSetAsProps(el, key, value) {
    if(el.tagName === "INPUT" && key === "form") return false;
    return key in el;
  }
  if(/^on/.test(key)) {
    const name = key.slice(2).toLowerCase();
    let invokers = el._vei || (el._vei = {});
    let invoker = invokers[name];
    if(newValue) {
      if(!invoker) {
        invoker = el._vei = function(e) {
          if(e.timeStamp < invoker.attached) return;
          if(Array.isArray(invoker.value)) {
            invoker.value.forEach(fn => fn(e))
          } else {
            invoker.value(e);
          }
        }
        invoker.value = newValue;
        invoker.attached = performance.now();
        el.addEventListener(name, invoker) ; // 在之前没有绑定过数据的情况下进行数据的监听
      } else {
        invoker.value  = newValue;
      }
      // 为什么不在这里添加addEventListener, 因为每监听一次, 就多一次事件触发
    } else if(invoker)  {
      el.removeEventListener(name, invoker)
    }
  } else if(key === "class") {
    el.className = newValue || ""
  } else if(shouldSetAsProps(el, key, newValue)){
    let type = typeof el[key];
    if(type === "boolean" && newValue === "") {
      el[key] = true;
    } else {
      el[key] = newValue;
    }
  } else {
    el.setAttribute(key, newValue)
  }
}
function unmount(vnode) {
  if(vnode.type === Fragment) {
    vnode.children.forEach(c => unmount(c))
  }
  const parent = vnode.el.parent;
  if(parent) parent.removeChild(vnode.el);
}
function createText(text) {
  return document.createTextNode(text)
}
function setText(el, text) {
  el.nodeValue = text;
}

const renderer = createRenderer({
  createElement,
  setElementText,
  insert,
  patchProps,
  createText,
  setText
});