第九章-简单diff算法

54 阅读4分钟
  • 通过记录移动的旧vnode中最大排序值,小于排序值的进行移动, 大于vnode则更新最大排序值

  • 新vnode的顺序就是想要更新的顺序

  • 移动DOM、加载DOM、删除DOM

  • 遍历过程, 只有新vnode当前节点上个节点是已经排好序,是移动和删除的基线

  • key的作用, 进行diff比较的时候, key相当于虚拟节点的身份证, 是新旧节点之间的映射关系, 减少真实dom的操作

 if(Array.isArray(n2.children)) {   // 2、新节点是数组节点
      if(Array.isArray(n1.children)) {       // 2.1、节点是数组节点   // 这里是diff算法, 暂时全部卸载后再更新
        const oldChildren = n1.children;
        const newChildren = n2.children;
        let lastIndex = 0;
        for(let i = 0 ; i < newChildren.length; i++) {
          let newVnode = newChildren[i];
          let j = 0
          let find = false;
          for(j; j < oldChildren.length; j++) {
            let oldVnode = oldChildren[j];
            // 移动DOM
            if (newVnode.key === oldVnode.key) {
              find = true;
              patch(oldVnode, newVnode, container);
              if (j < lastIndex) {
                let prevVnode = newChildren[i - 1];
                if(prevVnode) {
                  let anchor = prevVnode.el.nextSibling();
                  insert(newVnode.el, container, anchor);
                }
              } else {
                lastIndex = j;
              }
              break;
            }
          }
          if(!find) {
            // 新增DOM
            let anchor = null;
            let prevVnode =  newChildren[i - 1];
            if(prevVnode) {
              anchor = prevVnode.el.nextSibling();
            } else {
              anchor = container.firstChild;
            }
            patch(null, newVnode, container, anchor )
          }
        }
        // 删除DOM
        for(let i = 0; i < oldChildren.length; i++) {
          let oldVnode = oldChildren[i];
          let find = newChildren.find(c => c.key === oldVnode.key);
          if(!find) {
            unmount(oldVnode)
          }
        }
      } else  {                              // 2.2、节点为空 不处理后挂载,2.3、文本节点 设置为空后挂载
        setElementText(container, "");
        n2.children.forEach(c => patch(null, c, container))
      }
    }

总结代码

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、节点是数组节点   
        const oldChildren = n1.children;
        const newChildren = n2.children;
        let lastIndex = 0;
        for(let i = 0 ; i < newChildren.length; i++) {
          let newVnode = newChildren[i];
          let j = 0
          let find = false;
          for(j; j < oldChildren.length; j++) {
            let oldVnode = oldChildren[j];
            // 移动DOM
            if (newVnode.key === oldVnode.key) {
              find = true;
              patch(oldVnode, newVnode, container);
              if (j < lastIndex) {
                let prevVnode = newChildren[i - 1];
                if(prevVnode) {
                  let anchor = prevVnode.el.nextSibling;
                  insert(newVnode.el, container, anchor);
                }
              } else {
                lastIndex = j;
              }
              break;
            }
          }
          if(!find) {
            // 新增DOM
            let anchor = null;
            let prevVnode =  newChildren[i - 1];
            if(prevVnode) {
              anchor = prevVnode.el.nextSibling();
            } else {
              anchor = container.firstChild;
            }
            patch(null, newVnode, container, anchor )
          }
        }
        // 删除DOM
        for(let i = 0; i < oldChildren.length; i++) {
          let oldVnode = oldChildren[i];
          let find = newChildren.find(c => c.key === oldVnode.key);
          if(!find) {
            unmount(oldVnode)
          }
        }
      } 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
});


类比算法题目: 移动数组1变成数组2, 在尽可能删除不数据的情况下

arr1: [6,7, 8, 10, 5]

arr2: [10, 5,8, 9]

let arr1 = [6,7, 8, 10, 5]

let arr2 =  [10, 5,8, 9]

function move(n1, n2) {
  let lastIndex = 0;
  for(let i = 0; i < n2.length; i ++) {
    let has = false;
    let j = 0;
    let length = n1.length;
    for(j; j < length; j ++) {
       if(n2[i] === n1[j]) {
         has = true;
         if(j < lastIndex) {
           let newValue = n1.slice(j, j + 1)[0]
           n1 = [...n1.slice(0, j) , ...n1.slice(j + 1), newValue]
           console.log("移动元素"+ n1[j], n1)
         } else {
           console.log("不移动元素" + n1[j], n1)
           lastIndex = j;
         }
       }
    }
    if(!has) {
      n1.splice(j, 0, n2[i])
      console.log("新增元素"+ n2[i], n1)
    }
  }
  let resultArr = [];
  for(let j = 0 ; j < n1.length; j++) {
    let has = n2.findIndex(newValue => newValue === n1[j]) !== -1;
    if(!has) {
      console.log("删除元素", n1[j])
    } else {
      resultArr.push(n1[j]);
    }
  }
  return resultArr;
}
move(arr1, arr2)