vue-diff算法

虚拟DOM(Virtual Dom),也就是我们常说的虚拟节点,是用JS对象来模拟真实DOM中的节点,该对象包含了真实DOM的结构及其属性,用于对比虚拟DOM和真实DOM的差异,从而进行局部渲染来达到优化性能的目的,避免频繁对真实DOM操作造成布局重排、回流重绘等性能开销。

1、diff算法:

渲染真实DOM的开销是很大的,轻微的操作都可能导致页面重新排版,非常耗性能。 相对于DOM对象,js对象处理起来更快,而且更简单。 通过diff算法对比新旧vdom之间的差异,可以批量的、最小化的执行 dom操作,从而提高性能。

2、diff时间复杂度分析:

常规:O(n^3) 遍历树1; 遍历树2; 排序; 1000个节点,十亿的量级。

1.png

vue diff:O(n) 只比较同一层级 ;tag不相同,直接删掉重建; 通过key来标识区分相同节点。

2.png

3、vue diff key优化特点

<div id="demo">
    <p v-for="item in arr" :key="item">
        {{a}}
    </p>
</div>
<script>
const app = new Vue({
    el: '#demo',
    data: { arr: ['A', 'B', 'C', 'D'] },
    mounted() {
        setTimeout(() => {
            this.arr.splice(1, 0, 'E');
        }, 1000);
    }
});
</script>

旧节点:A、B、C、D
新节点:A、E、B、C、D

节点没设置key:如下,将会对4个节点做更新操作
nokey.gif
节点设置了key:如下,只对1个节点做更新操作
key.gif

diff算法核心原理:

diff.png

1、sameVnode:判断两个节点是否为同一个节点

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}
判断条件:

1.1 两个节点key是否相同(两个key都没有,即 undefined === undefined)

1.2 两个节点tag标签名是否一样

1.3 两个节点是否都为注释节点

1.4 两个节点的data isDef是否都相等(isDef:data !== undefined && data !== null)

1.5 两个节点的input类型是否相同

1.6 节点a是否为异步占位

1.7 两个节点的异步函数是否相等

1.8节点b异步函数的error是否为空(isUndef:data === undefined && data === null)

2、patchVnode:更新节点

function patchVnode (
  oldVnode,
  vnode,
  insertedVnodeQueue,
  ownerArray,
  index,
  removeOnly
) {
  // 新旧node相同,return   
  if (oldVnode === vnode) {
    return
  }
 
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    // clone reused vnode
    vnode = ownerArray[index] = cloneVNode(vnode);
  }
 
  var elm = vnode.elm = oldVnode.elm;
 
  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue);
    } else {
      vnode.isAsyncPlaceholder = true;
    }
    return
  }
 
  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance;
    return
  }
 
  var i;
  var data = vnode.data;
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode);
  }
 
  var oldCh = oldVnode.children;
  var ch = vnode.children;
  if (isDef(data) && isPatchable(vnode)) {
    // 调用update回调以及update钩子
    for (i = 0; i < cbs.update.length; ++i) { cbs.update[i](oldVnode, vnode); }
    if (isDef(i = data.hook) && isDef(i = i.update)) { i(oldVnode, vnode); }
  }
  if (isUndef(vnode.text)) {
    // 新节点text文本属性不存在
    if (isDef(oldCh) && isDef(ch)) {
      // 新旧节点都有子节点,调用updateChildren重排子节点
      if (oldCh !== ch) { updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly); }
    } else if (isDef(ch)) {
      // 只有新节点有子节点,调用addVnodes添加子节点
      {
        checkDuplicateKeys(ch);
      }
      if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, ''); }
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
    } else if (isDef(oldCh)) {
      // 只有旧节点有子节点,调用removeVnodes添加子节点
      removeVnodes(oldCh, 0, oldCh.length - 1);
    } else if (isDef(oldVnode.text)) {
      // 只有旧节点有文本,调用setTextContent清空文本
      nodeOps.setTextContent(elm, '');
    }
  } else if (oldVnode.text !== vnode.text) {
    // 新旧节为文本节点并且文本不一致,调用setTextContent清空文本
    nodeOps.setTextContent(elm, vnode.text);
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) { i(oldVnode, vnode); }
  }
}
更新条件:

2.1 新旧节点都有子节点,调用updateChildren重排子节点

2.2 只有新节点有子节点,调用addVnodes添加子节点

2.3 只有旧节点有子节点,调用removeVnodes移除子节点

2.4 如果是文本节点,调用setTextContent更新节点文本内容

3、updateChildren:更新子节点

function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  var oldStartIdx = 0; // 旧子节点start下标(游标)
  var newStartIdx = 0; // 新子节点start下标(游标)
  var oldEndIdx = oldCh.length - 1; // 旧子节点end下标(游标)
  var newEndIdx = newCh.length - 1; // 新子节点end下标(游标)
  var oldStartVnode = oldCh[0]; // 旧start节点(旧头)
  var newStartVnode = newCh[0]; // 新start节点(新头)
  var oldEndVnode = oldCh[oldEndIdx]; // 旧end节点(旧尾)
  var newEndVnode = newCh[newEndIdx]; // 新end节点(新尾)
  var oldKeyToIdx, idxInOld, vnodeToMove, refElm;
 
  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  var canMove = !removeOnly;
 
  {
    checkDuplicateKeys(newCh);
  }
 
  // oldStartIdx大于oldEndIdx,或者newStartIdx大于newEndIdx时候跳出循环
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      // 旧头不存在,oldStartIdx++
      oldStartVnode = oldCh[++oldStartIdx]; // Vnode has been moved left
 
    } else if (isUndef(oldEndVnode)) {
      // 旧尾不存在,oldEndIdx--
      oldEndVnode = oldCh[--oldEndIdx];
 
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 旧头、新头相同,更新节点,oldStartVnode++、newStartVnode++
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
      oldStartVnode = oldCh[++oldStartIdx];
      newStartVnode = newCh[++newStartIdx];
 
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 旧尾、新尾相同,更新节点,oldEndIdx--、newEndIdx--
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
      oldEndVnode = oldCh[--oldEndIdx];
      newEndVnode = newCh[--newEndIdx];
 
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // 旧头、新尾相同,更新节点,并且将旧头移到最后,oldStartIdx++、newEndIdx--
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx);
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm));
      oldStartVnode = oldCh[++oldStartIdx];
      newEndVnode = newCh[--newEndIdx];
 
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      // 旧尾、新头相同,更新节点,并且将旧尾移到最前面,newStartIdx++、oldEndIdx--
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm);
      oldEndVnode = oldCh[--oldEndIdx];
      newStartVnode = newCh[++newStartIdx];
 
    } else {
      if (isUndef(oldKeyToIdx)) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx); }
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx);
      if (isUndef(idxInOld)) { // New element
        // 遍历发现旧子节点中没有和新节点相同的节点,创建新节点
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
      } else {
        vnodeToMove = oldCh[idxInOld];
        if (sameVnode(vnodeToMove, newStartVnode)) {
          // 遍历节点和新头相同,更新节点,移动节点替换
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx);
          oldCh[idxInOld] = undefined;
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm);
        } else {
          // 节点不同,新建节点
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx);
        }
      }
      newStartVnode = newCh[++newStartIdx];
    }
  }
 
  if (oldStartIdx > oldEndIdx) {
    // 旧子节点先遍历完,新子节点还有剩,调用addVnodes添加节点
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm;
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue);
  } else if (newStartIdx > newEndIdx) {
    // 新子节点先遍历完,旧子节点还有剩,调用removeVnodes添加节点
    removeVnodes(oldCh, oldStartIdx, oldEndIdx);
  }
}
3.1 新旧子节点未遍历完毕

3.1.1 旧头不存在,将旧头游标往后移一位

3.1.2 旧尾不存在,将旧尾游标往前移一位

3.1.3 旧头、新头相同,更新节点,并将头部游标往后移一位

3.1.4 旧尾、新尾相同,更新节点,并将尾部游标往前移一位

3.1.5 旧头、新尾相同,更新节点,并且将旧头移到尾部,旧头游标往后移一位,新尾游标往前移一位

3.1.6 旧尾、新头相同,更新节点,并且将旧尾移到头部,新头游标往后移一位,旧尾游标往前移一位

3.1.7 拿新头遍历旧子节点,找不到则新建一个节点;找到判断节点是否相同,相同则更新节点,移动老节点,不同则新建一个节点

3.2 新旧子节点遍历完毕

3.2.1 旧子节点先遍历完毕,说明有新增节点,批量增加

3.2.2 新子节点先遍历完毕,说明有节点删除,批量移除

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351