Vue3 diff 算法

代码仓库

diff 算法

内容更新

const { createApp, h, reactive } = Vue

const App = {
    setup() {
        let state = reactive({flag: true})
        setTimeout(() => {
        state.flag = !state.flag;
        }, 1000);
        
        return ()=>(state.flag ? h("div",{style:{color:"#f00"}},"hello") : h("p",{style:{background:"#0f0"}},"world"))
    },
}

let app = createApp(App);
app.mount("#app")
  function setupRenderEffect(instance, container) {
    effect(() => {
      if (!instance.isMounted) {
        let subTree = instance.subTree = instance.render();
        patch(null, subTree, container)
        instance.isMounted = true
      } else {
        // 更新操作
        let prevTree = instance.subTree
        let nextTree = instance.render()
        patch(prevTree, nextTree, container)
      }
    })
  }

同级对比条件

  • 类型不一样 key 不一样不复用
  • 复用节点后 比较属性
  • 对比孩子 1方 有儿子 2方都有儿子
  • 都有儿子的时候此时正真的dom-diff

类型和key都不一样

const isSameVnodeType = (n1, n2) => {
    return n1.type == n2.type && n1.key == n2.key
}
  const patch = (n1, n2, container) => {
    
    // 类型不一样 key 不一样不复用
    if (n1 && !isSameVnodeType(n1, n2)) {      
      // 删除旧节点
      hostRemove(n1.el)
      // 旧节点重置为 null
      n1 = null
    }

    // 开始渲染
    let { shapeFlag } = n2
    if (shapeFlag & ShapeFlags.ELEMENT) {
      // 1
      processElement(n1, n2, container)
    } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
      processComponent(n1, n2, container)
    }
  }

属性对比

复用节点后 比较属性

return ()=>{
    if(state.flag){
        return h("div",{style:{color:"#f00"}},"hello")
    }else{
        return h("div",{style:{background:"#0f0"}},"world")
    }
}
const patchElement = (n1, n2, container) => {
    // 比较两个虚拟节点,并且复用老节点
    let el = n2.el = n1.el;

    // 对比属性
    const oldProps = n1.props || {};
    const newProps = n2.props || {};
    patchProps(oldProps,newProps,el);
}
const patchProps = (oldProps, newProps, el) => {
  if (oldProps !== newProps) {
    for (let key in newProps) {
      const prev = oldProps[key]
      const next = newProps[key]
      if (prev !== next) {
        hostPatchProp(el, key, prev, next)
      }
    }
    // 旧属性新的props中没有,需要移除
    for (let key in oldProps) {
      if (!(key in newProps)) {
        hostPatchProp(el, key, oldProps[key], null)
      }
    }
  }
}

子节点对比

子节点对比条件

  • 旧的是文本,新的是文本
  • 旧的是数组,新的是文本
  • 旧的是文本,新的是数组
  • 新的是数组,旧的是数组
return ()=>{
  if(state.flag){
    return h("div",{style:{color:"#f00"}},"hello")
  }else{
    return h("div",{style:{background:"#0f0"}},[
      h("div",{},"chen"),
      h("div",{},"wei"),
      h("div",{},"long"),
    ])
  }
}

交给 patchChildren 处理

const patchElement = (n1, n2, container) => {
  // 比较两个虚拟节点,并且复用老节点
  let el = n2.el = n1.el;
  const oldProps = n1.props || {};
  const newProps = n2.props || {};
  
  patchProps(oldProps,newProps,el);
  
  patchChildren(n1, n2, el);
}

const patchChildren = (n1,n2,el)=>{
  
  const c1 = n1.children
  const c2 = n2.children

  const prevShapeFlag = n1.shapeFlag
  const nextShapeFlag = n2.shapeFlag

  // 旧的是文本,新的是文本
  if(nextShapeFlag & ShapeFlags.TEXT_CHILDREN){
    // 新的是文本
    if(c2 !== c1){
      hostSetElementText(el,c2);
    }
  } else {
    // 新的是数组 旧的是数组
    if(prevShapeFlag & ShapeFlags.ARRAY_CHILDREN){
      patchKeyedChildren(c1,c2,el)
    }else{
      // 老的是文本 新的是数组
      hostSetElementText(el,""); // 删掉旧内容
      mountChildren(c2,el)
    }
  }
}

数组子节点的对比

尽可能复用 children 节点

新旧值前面节点相同:

  • [a, b, c]
  • [a, b, c, d]
let i=0;
let e1 = c1.length-1;
let e2 = c2.length-1;
while(i<=e1 && i<=e2){// 谁先比对完毕就结束
  const n1 = c1[i];
  const n2 = c2[i];
  if(isSameVnodeType(n1,n2)){
    patch(n1,n2,el); // 递归比较子节点
  }else{
    break;
  }
  i++;
}

新旧值后面面节点相同:

  • [a, b, c]
  • [d, a, b, c]
while(i<= e1 && i<=e2){
  const n1 = c1[e1];
  const n2 = c2[e2];
  if(isSameVnodeType(n1,n2)){
    patch(n1,n2,el)
  }else{
    break;
  }
  e1--;
  e2--;
}

前后都不一样的情况,插入和删除操作

// 新增的节点,在i和e2之间或i=e2
if(i>e1){
  
  if(i<=e2){
    // 获取插入参照物
    // 前面值都一样,e2值不变:e2+1 大于 数组长度(c2.length)
    // 后面值都一样,e2向前取:e2+1 小于 数组长度(c2.length)
    const nextPos = e2+1;
    const anchor = nextPos < c2.length ? c2[nextPos].el : null;
    console.log(i, e1, e2, anchor)
    while(i<=e2){
      patch(null, c2[i], el, anchor);
      i++
    }
  }
  
  // 新值小于旧值 删除情况, 判断条件:i>e2
  // abcd abc (i=3 e1=3 e2=2)
} else if(i>e2){
    while(i<=e1){
      hostRemove(c1[i].el);
      i++;
    }
}

patchKeyedChildren 方法如下:

const patchKeyedChildren = (c1,c2,el)=>{
  // 新旧都有children元素
  
  // 1) 尽可能复用两个 children
  // abc
  // abcd 
  // i = 3
  let i=0;
  let e1 = c1.length-1;
  let e2 = c2.length-1;
  while(i<=e1 && i<=e2){// 谁先比对完毕就结束
    const n1 = c1[i];
    const n2 = c2[i];
    if(isSameVnodeType(n1,n2)){
      patch(n1,n2,el); // 递归比较子节点
    }else{
      break;
    }
    i++;
  }
  
  // 2) abc i=0 e1=2 e2=3
  //    dabc  i=0 e1=-1 e2=0
  
  while(i<= e1 && i<=e2){
    const n1 = c1[e1];
    const n2 = c2[e2];
    if(isSameVnodeType(n1,n2)){
      patch(n1,n2,el)
    }else{
      break;
    }
    e1--;
    e2--;
  }
  
  // 3) 前后都不一样的情况
  
  // 新值大于旧值 新增情况,判断条件:i大于e1
  // abc => abcd (i=3 e1=2 e2=3)
  // abc => dabc (i=0 e1=-1 e2=0)
  if(i>e1){
    // 新增的节点,在i和e2之间或i=e2
    if(i<=e2){
      // 获取插入参照物
      // 前面值都一样,e2值不变:e2+1 大于 数组长度(c2.length)
      // 后面值都一样,e2向前取:e2+1 小于 数组长度(c2.length)
      
      const nextPos = e2+1;
      const anchor = nextPos < c2.length ? c2[nextPos].el : null;
      console.log(i, e1, e2, anchor)
      while(i<=e2){
        patch(null, c2[i], el, anchor);
        i++
      }
    }
    
    // 新值小于旧值 删除情况, 判断条件:i>e2
    // abcd abc (i=3 e1=3 e2=2)
  } else if(i>e2){
      while(i<=e1){
        hostRemove(c1[i].el);
        i++;
      }
  }else{
    // 乱序比对
  }     
}

乱序比对

前后子节点值变化:

旧: a,b,c,d,e,f,g
新: a,b,e,d,c,h,f,g

const App = {
  setup() {
    let state = reactive({ flag: true })
    setTimeout(() => {
      state.flag = !state.flag
    }, 1000)
    return () => {
      if (state.flag) {
        return h('div', {}, [
          h('div', { key: 'A' }, 'A'),
          h('div', { key: 'B' }, 'B'),

          h('div', { key: 'C' }, 'C'),
          h('div', { key: 'D' }, 'D'),
          h('div', { key: 'E' }, 'E'),

          h('div', { key: 'F' }, 'F'),
          h('div', { key: 'G' }, 'G'),
        ])
      } else {
        return h('div', {}, [
          h('div', { key: 'A' }, 'A'),
          h('div', { key: 'B' }, 'B'),

          h('div', { key: 'E' }, 'E'),
          h('div', { key: 'D' }, 'D'),
          h('div', { key: 'C' }, 'C'),
          h('div', { key: 'H' }, 'H'),

          h('div', { key: 'F' }, 'F'),
          h('div', { key: 'G' }, 'G'),
        ])
      }
    }
  },
}
// 乱序比对
// ab [cde] fg   // s1=2 e1=4
// ab [edch] fg   // s2=2 e2=5
const s1 = i
const s2 = i
// 新的索引 和 key 做成一个映射表
const keyToNewIndexMap = new Map()
for (let i = s2; i <= e2; i++) {
  const nextChild = c2[i]
  if (nextChild.key != null) {
    keyToNewIndexMap.set(nextChild.key, i)
  }
}

const toBePatched = e2 - s2 + 1 // 需要处理的节点总数
const newIndexToOldMapIndex = new Array(toBePatched).fill(0)

// 遍历老节点
for (i = s1; i <= e1; i++) {
  const prevChild = c1[i]
  let newIndex = keyToNewIndexMap.get(prevChild.key)
  if (newIndex == undefined) {
    // 旧值在新值中不存在,直接删除
    hostRemove(prevChild.el)
  } else {
    newIndexToOldMapIndex[newIndex - s2] = i + 1
    patch(prevChild, c2[newIndex], el)
  }
}

let increasingIndexSequence = getSequence(newIndexToOldMapIndex)
let j = increasingIndexSequence.length - 1;

for (i = toBePatched - 1; i >= 0; i--) {
  const nextIndex = s2 + i // [edch] 找到h索引
  const nextChild = c2[nextIndex] // 找到h
  // 找到当前元素的下一个元素
  let anchor = nextIndex + 1 < c2.length ? c2[nextIndex + 1].el : null

  if (newIndexToOldMapIndex[i] == 0) {
    // 新元素,直接创建插入到当前元素的下一个
    patch(null, nextChild, el, anchor)
  } else {
    // 根据参照物直接将节点移动过去,所有节点都需要移动
    // 没有考虑不需要动的节点情况
    // hostInsert(nextChild.el, el, anchor)

    // 最长递增子序列优化
    if (j < 0 || i != increasingIndexSequence[j]) {
      hostInsert(nextChild.el, el, anchor)
    } else {
      j--;
    }
  }
}

最长递增子序列优化

function getSequence(arr):number[] {
  const result = [0]
  const p = arr.slice()
  let i, j, u, v, c
  const len = arr.length
  for (i = 0; i < len; i++) {
    const arrI = arr[i]
    if (arrI !== 0) {
      j = result[result.length - 1]
      // 拿最后一项的值 和 当前这项做比较
      if (arr[j] < arrI) {
        p[i] = j // 保存递增索引
        result.push(i)
        continue
      }
    }
    u = 0
    v = result.length - 1 // 二分查找 找索引
    while (u < v) {
      c = ((u + v) / 2) | 0
      if (arr[result[c]] < arrI) {
        u = c + 1
      } else {
        v = c
      }
    }
    // 确定索引, 用小的替换从、
    if (arrI < arr[result[u]]) {
      if (u > 0) {
        console.log(p, result[u - 1], result, u)
        p[i] = result[u - 1]
      }
      result[u] = i
    }
  }

  u = result.length
  v = result[u - 1]
  while (u-- > 0) {
    result[u] = v
    v = p[v]
  }
  return result
}

console.log(getSequence([1, 2, 6, 8, 12, 5, 7]))