细致分析,尤雨溪直播中提到 vue3.0 diff 算法优化细节

2020/4/24 11:21:55

本文主要是介绍细致分析,尤雨溪直播中提到 vue3.0 diff 算法优化细节,对大家解决编程问题具有一定的参考价值,需要的程序猿们随着小编来一起学习吧!

守着b站看完直播
感慨良多,为什么存在头发多,能力还这么强的男人呢

整场下来,讲了很多
比如 重写了virtual dom,然后编译模板有了很大的优化
组件初始化会更加有效率, 对比 vue2 有1.3到2倍的性能提升
然后服务器渲染来说更是有2到3倍的的提升,等等

其中我对重写的 virtual dom 和 模板编译 的优化觉得很好奇
于是细究了一下

1.静态node优化

首先是对于静态节点的优化
举个例子

<div>
  <span>hello</span>
  <span>{{msg}}</span>
</div>

复制代码

vue2生成的render函数

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", [
    _c("span", [_vm._v("hello")]),
    _c("span", [_vm._v(_vm._s(_vm.msg))])
  ])
}
复制代码

vue3生成的render函数

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", null, "hello"),
    _createVNode("span", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
  ]))
}

// Check the console for the AST
复制代码

vue2 生成的 vnode

vue3 生成的 vnode
注意我标记的,dynamicChildren

很明显,vue3 标记了dynamicChildren 动态节点
接下来patch阶段,只会比较动态节点,静态的直接略过了
而vue2中,还是会patch所有子节点去比对变更

2.节点变更类型细分

再继续接着上面的例子
如果我们模板再改一下,加上 id props

<div>
  <span>hello</span>
  <span :id="hello" attr="test">{{msg}}</span>
</div>
复制代码

vue3生成的render函数如下

import { createVNode as _createVNode, toDisplayString as _toDisplayString, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", null, "hello"),
    _createVNode("span", {
      id: _ctx.hello,
      attr: "test"
    }, _toDisplayString(_ctx.msg), 9 /* TEXT, PROPS */, ["id"])
  ]))
}

// Check the console for the AST
复制代码

vue3生成的vnode如下

注意这几个属性
dynamicProps patchFlag 这里直接标记出来了动态的属性名称,只有 id 是需要动态对比的
并且patchFlag是 9 ,我们来看看

export const enum PatchFlags {
  TEXT = 1,
  CLASS = 1 << 1,
  STYLE = 1 << 2,
  PROPS = 1 << 3,
  FULL_PROPS = 1 << 4,
  HYDRATE_EVENTS = 1 << 5,
  STABLE_FRAGMENT = 1 << 6, 
  KEYED_FRAGMENT = 1 << 7, 
  UNKEYED_FRAGMENT = 1 << 8, 
  NEED_PATCH = 1 << 9, 
  DYNAMIC_SLOTS = 1 << 10, 
  HOISTED = -1,  
  BAIL = -2 
}
复制代码

9代表的 是 TEXT(1) | PROPS(8),就是文字和属性都有修改

这里的枚举用的位掩码,有疑惑的同学也可以搜下

然后我们去patch函数看看

// vue3 的patch
const patchElement = (n1, n2, parentComponent, parentSuspense, isSVG, optimized) => {
        const el = (n2.el = n1.el);
        let { patchFlag, dynamicChildren, dirs } = n2;
        const oldProps = (n1 && n1.props) || EMPTY_OBJ;
        const newProps = n2.props || EMPTY_OBJ;
        let vnodeHook;
        if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
            invokeVNodeHook(vnodeHook, parentComponent, n2, n1);
        }
        if (dirs) {
            invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate');
        }
        if (__HMR__ && parentComponent && parentComponent.renderUpdated) {
            // HMR updated, force full diff
            patchFlag = 0;
            optimized = false;
            dynamicChildren = null;
        }
        if (patchFlag > 0) {
            // the presence of a patchFlag means this element's render code was
            // generated by the compiler and can take the fast path.
            // in this path old node and new node are guaranteed to have the same shape
            // (i.e. at the exact same position in the source template)
            if (patchFlag & 16 /* FULL_PROPS */) {
                // element props contain dynamic keys, full diff needed
                patchProps(el, n2, oldProps, newProps, parentComponent, parentSuspense, isSVG);
            }
            else {
                // class
                // this flag is matched when the element has dynamic class bindings.
                if (patchFlag & 2 /* CLASS */) {
                    if (oldProps.class !== newProps.class) {
                        hostPatchProp(el, 'class', null, newProps.class, isSVG);
                    }
                }
                // style
                // this flag is matched when the element has dynamic style bindings
                if (patchFlag & 4 /* STYLE */) {
                    hostPatchProp(el, 'style', oldProps.style, newProps.style, isSVG);
                }
                // props
                // This flag is matched when the element has dynamic prop/attr bindings
                // other than class and style. The keys of dynamic prop/attrs are saved for
                // faster iteration.
                // Note dynamic keys like :[foo]="bar" will cause this optimization to
                // bail out and go through a full diff because we need to unset the old key
                if (patchFlag & 8 /* PROPS */) {
                    // if the flag is present then dynamicProps must be non-null
                    const propsToUpdate = n2.dynamicProps;
                    for (let i = 0; i < propsToUpdate.length; i++) {
                        const key = propsToUpdate[i];
                        const prev = oldProps[key];
                        const next = newProps[key];
                        if (prev !== next) {
                            hostPatchProp(el, key, prev, next, isSVG, n1.children, parentComponent, parentSuspense, unmountChildren);
                        }
                    }
                }
            }
            // text
            // This flag is matched when the element has only dynamic text children.
            if (patchFlag & 1 /* TEXT */) {
                if (n1.children !== n2.children) {
                    hostSetElementText(el, n2.children);
                }
            }
        }
        ...
    };
复制代码

这里可以看到方法内有很多根据特定的 patchFlag 去执行特定的操作

对比下 vue2 中 patchVnode 阶段
如果是普通节点,会通过内置的update钩子全量进行新旧对比,然后更新

// vue2内置的钩子,这些module有update钩子的,都会全量执行
var baseModules = [
  ref,
  directives
];

var platformModules = [
  attrs,
  klass,
  events,
  domProps,
  style,
  transition
];
复制代码

如果是component,则会在prepatch阶段进行判断,有变化则会重新触发forceUpdate

显然 vue2 中有很多重复的无用对比

  // vue2 的 patch
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    ...
    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)) {
      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); }
    }
    ...
}
复制代码

3.diff优化

再来就是diff算法的优化,尤大佬已注释的很详细……
下面我们也来用一组数据做例子

    // old arr
    ["a", "b", "c", "d", "e", "f", "g", "h"]
    // new arr
    ["a", "b", "d", "f", "c", "e", "x", "y", "g", "h"]
复制代码

其实看注释前四步都好理解
第1步:从头到尾开始比较,[a,b]是sameVnode,进入patch,到 [c] 停止;
第2步:从尾到头开始比较,[h,g]是sameVnode,进入patch,到 [f] 停止;
第3步:判断旧数据是否已经比较完毕,多余的说明是新增的,需要mount(本例中没有)
第4步:判断新数据是否已经比较完毕,多余的说明是删除的,需要unmount(本例中没有)

第5步:进入到这里,说明是乱序了,这一步就开始稍显复杂
5.1 首先建一个还未比较的新数据index的Map,keyToNewIndexMap[d:2,f:3,c:4,e:5,x:6,y:7]
5.2

  • 根据未比较完的数据长度,建一个填充 0 的数组 [0,0,0,0,0]
  • 然后循环一遍剩余数据,找到未比较的数据的索引newIndexToOldIndexMap[4(d),6(f),3(c),5(e),0,0]
  • 如果没有在剩余数据里找到,说明是删除的,unmount掉
  • 找到了,和之前的patch一下

5.3 其实到这一步,已经很好办了,从尾到头循环一下newIndexToOldIndexMap
是 0 的,说明是新增的数据,就 mount 进去
非 0 的,说明在旧数据里,我们只要把它们移动到对应index的前面就行了

如下:

  • 把 c 移动到 e 之前
  • 把 f 移动到 c 之前
  • 把 d 移动到 f 之前

但是心细的小明同学已经发现了,c 移动到 e 之前是多余的
因为等 f 和 d 都移动之后,c 自然就到 e 之前了
所以,vue3中还做了一件事情,根据newIndexToOldIndexMap找到最长递增子序列
我们的 [4(d),6(f),3(c),5(e),0,0] 很明显能找到 [3,5] 是数组中的最长递增子序列
于是乎 [3,5] 都不需要移动

我来画个图加深下记忆(忽略我的字迹)

做完这步操作之后,我们的diff算法就结束了
当然还有很多细节,注意看源码如下注意看尤大标记的序号 1 ~ 5.3

    const patchKeyedChildren = (c1, c2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized) => {
        let i = 0;
        const l2 = c2.length;
        let e1 = c1.length - 1; // prev ending index
        let e2 = l2 - 1; // next ending index
        // 1. sync from start
        // (a b) c
        // (a b) d e
        while (i <= e1 && i <= e2) {
            const n1 = c1[i];
            const n2 = (c2[i] = optimized
                ? cloneIfMounted(c2[i])
                : normalizeVNode(c2[i]));
            if (isSameVNodeType(n1, n2)) {
                patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized);
            }
            else {
                break;
            }
            i++;
        }
        // 2. sync from end
        // a (b c)
        // d e (b c)
        while (i <= e1 && i <= e2) {
            const n1 = c1[e1];
            const n2 = (c2[e2] = optimized
                ? cloneIfMounted(c2[e2])
                : normalizeVNode(c2[e2]));
            if (isSameVNodeType(n1, n2)) {
                patch(n1, n2, container, parentAnchor, parentComponent, parentSuspense, isSVG, optimized);
            }
            else {
                break;
            }
            e1--;
            e2--;
        }
        // 3. common sequence + mount
        // (a b)
        // (a b) c
        // i = 2, e1 = 1, e2 = 2
        // (a b)
        // c (a b)
        // i = 0, e1 = -1, e2 = 0
        if (i > e1) {
            if (i <= e2) {
                const nextPos = e2 + 1;
                const anchor = nextPos < l2 ? c2[nextPos].el : parentAnchor;
                while (i <= e2) {
                    patch(null, (c2[i] = optimized
                        ? cloneIfMounted(c2[i])
                        : normalizeVNode(c2[i])), container, anchor, parentComponent, parentSuspense, isSVG);
                    i++;
                }
            }
        }
        // 4. common sequence + unmount
        // (a b) c
        // (a b)
        // i = 2, e1 = 2, e2 = 1
        // a (b c)
        // (b c)
        // i = 0, e1 = 0, e2 = -1
        else if (i > e2) {
            while (i <= e1) {
                unmount(c1[i], parentComponent, parentSuspense, true);
                i++;
            }
        }
        // 5. unknown sequence
        // [i ... e1 + 1]: a b [c d e] f g
        // [i ... e2 + 1]: a b [e d c h] f g
        // i = 2, e1 = 4, e2 = 5
        else {
            const s1 = i; // prev starting index
            const s2 = i; // next starting index
            // 5.1 build key:index map for newChildren
            const keyToNewIndexMap = new Map();
            for (i = s2; i <= e2; i++) {
                const nextChild = (c2[i] = optimized
                    ? cloneIfMounted(c2[i])
                    : normalizeVNode(c2[i]));
                if (nextChild.key != null) {
                    if ((process.env.NODE_ENV !== 'production') && keyToNewIndexMap.has(nextChild.key)) {
                        warn(`Duplicate keys found during update:`, JSON.stringify(nextChild.key), `Make sure keys are unique.`);
                    }
                    keyToNewIndexMap.set(nextChild.key, i);
                }
            }
            // 5.2 loop through old children left to be patched and try to patch
            // matching nodes & remove nodes that are no longer present
            let j;
            let patched = 0;
            const toBePatched = e2 - s2 + 1;
            let moved = false;
            // used to track whether any node has moved
            let maxNewIndexSoFar = 0;
            // works as Map<newIndex, oldIndex>
            // Note that oldIndex is offset by +1
            // and oldIndex = 0 is a special value indicating the new node has
            // no corresponding old node.
            // used for determining longest stable subsequence
            const newIndexToOldIndexMap = new Array(toBePatched);
            for (i = 0; i < toBePatched; i++)
                newIndexToOldIndexMap[i] = 0;
            for (i = s1; i <= e1; i++) {
                const prevChild = c1[i];
                if (patched >= toBePatched) {
                    // all new children have been patched so this can only be a removal
                    unmount(prevChild, parentComponent, parentSuspense, true);
                    continue;
                }
                let newIndex;
                if (prevChild.key != null) {
                    newIndex = keyToNewIndexMap.get(prevChild.key);
                }
                else {
                    // key-less node, try to locate a key-less node of the same type
                    for (j = s2; j <= e2; j++) {
                        if (newIndexToOldIndexMap[j - s2] === 0 &&
                            isSameVNodeType(prevChild, c2[j])) {
                            newIndex = j;
                            break;
                        }
                    }
                }
                if (newIndex === undefined) {
                    unmount(prevChild, parentComponent, parentSuspense, true);
                }
                else {
                    newIndexToOldIndexMap[newIndex - s2] = i + 1;
                    if (newIndex >= maxNewIndexSoFar) {
                        maxNewIndexSoFar = newIndex;
                    }
                    else {
                        moved = true;
                    }
                    patch(prevChild, c2[newIndex], container, null, parentComponent, parentSuspense, isSVG, optimized);
                    patched++;
                }
            }
            // 5.3 move and mount
            // generate longest stable subsequence only when nodes have moved
            const increasingNewIndexSequence = moved
                ? getSequence(newIndexToOldIndexMap)
                : EMPTY_ARR;
            j = increasingNewIndexSequence.length - 1;
            // looping backwards so that we can use last patched node as anchor
            for (i = toBePatched - 1; i >= 0; i--) {
                const nextIndex = s2 + i;
                const nextChild = c2[nextIndex];
                const anchor = nextIndex + 1 < l2 ? c2[nextIndex + 1].el : parentAnchor;
                if (newIndexToOldIndexMap[i] === 0) {
                    // mount new
                    patch(null, nextChild, container, anchor, parentComponent, parentSuspense, isSVG);
                }
                else if (moved) {
                    // move if:
                    // There is no stable subsequence (e.g. a reverse)
                    // OR current node is not among the stable sequence
                    if (j < 0 || i !== increasingNewIndexSequence[j]) {
                        move(nextChild, container, anchor, 2 /* REORDER */);
                    }
                    else {
                        j--;
                    }
                }
            }
        }
    };

复制代码

对比 vue2.0 的diff算法

说到这里,我们可以来回顾下 vue2.0 的diff算法
还是这组数据

    // old arr
    ["a", "b", "c", "d", "e", "f", "g", "h"]
    // new arr
    ["a", "b", "d", "f", "c", "e", "x", "y", "g", "h"]
复制代码

vue2整体上也差不多,但是它只有一个双指针的循环
首先比较新旧的头,直到第一个非 sameVNOde
然后从尾开始比较,直到第一个非 sameVNOde
然后会做头尾,尾头的比较,这种是考虑到会左移和右移操作
上面的步骤做完,会发现和vue3一样,只剩下这些没有比较

["d", "f", "c", "e", "x", "y"]
复制代码

接着会尝试从 "d" 开始去旧数据里找到 index
然后移动到旧数据还未比较数据的头部
于是乎:

  • 把 d 移动到 c 之前
  • 把 f 移动到 c 之前
  • 下轮循环发现新旧都是 c 于是 patch 完之后继续
  • 下轮循环发现新旧都是 e 于是 patch 完之后继续
  • 发现 x 不在旧数据中,createElm(x)
  • 发现 x 不在旧数据中,createElm(y)

可以发现,vue2 在 diff 算法处理无序数据的判断是在最后
每次处理之前,会依次判断之前所有的 if
而vue3中,会找到所有需要移动的节点,直接移动
还有一点 vue3 中 对于首尾替换的额外判断似乎也取消了

3.事件优化

再来讲到了关于事件的优化
例子:

<div>
  <span  @click="onClick">hello</span>
</div>
复制代码

如果开启了cacheHandlers
则会缓存我们的事件,事件的变化不会引起重新渲染

import { createVNode as _createVNode, openBlock as _openBlock, createBlock as _createBlock } from "vue"

export function render(_ctx, _cache) {
  return (_openBlock(), _createBlock("div", null, [
    _createVNode("span", {
      onClick: _cache[1] || (_cache[1] = $event => (_ctx.onClick($event)))
    }, "hello")
  ]))
}

// Check the console for the AST
复制代码

这个很直观的,我们能在 vue2 中看到
其实每次更新,render函数跑完之后
vnode绑定的事件都是一个全新生成的function,就算它们内部的代码是一样的
所以events的update钩子,几乎每次都能命中,然后更新一下函数的引用

function updateListeners (
  on,
  oldOn,
  add,
  remove$$1,
  vm
) {
  var name, def, cur, old, event;
  for (name in on) {
    def = cur = on[name];
    old = oldOn[name];
    event = normalizeEvent(name);
    debugger
    /* istanbul ignore if */
    if (isUndef(cur)) {
      process.env.NODE_ENV !== 'production' && warn(
        "Invalid handler for event \"" + (event.name) + "\": got " + String(cur),
        vm
      );
    } else if (isUndef(old)) {
      if (isUndef(cur.fns)) {
        cur = on[name] = createFnInvoker(cur);
      }
      add(event.name, cur, event.once, event.capture, event.passive, event.params);
    } else if (cur !== old) {
      // 这里 这里 这里
      old.fns = cur;
      on[name] = old;
    }
  }
  for (name in oldOn) {
    if (isUndef(on[name])) {
      event = normalizeEvent(name);
      remove$$1(event.name, oldOn[name], event.capture);
    }
  }
}

复制代码

其他的..

还提到了SSR优化,我之前也发现了,vue3有一个新的ssr渲染函数createSSRApp
下次有时间仔细看看
当然老生常谈说了很多关于Composition API,这个是真的香
然后就是Teleport Suspense 这两个组件
在 react 中已经有类似的组件,不过在vue中的实现还未来得及看



这篇关于细致分析,尤雨溪直播中提到 vue3.0 diff 算法优化细节的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!


扫一扫关注最新编程教程