细致分析,尤雨溪直播中提到 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 算法优化细节的文章就介绍到这儿,希望我们推荐的文章对大家有所帮助,也希望大家多多支持为之网!
- 2024-10-04package.json 文件位置在哪?-icode9专业技术文章分享
- 2024-10-01Craco.js学习:从入门到实践指南
- 2024-10-01Create-React-App学习:入门与实践指南
- 2024-10-01CSS-in-JS学习:从入门到实践指南
- 2024-09-30JSX语法学习:从入门到初步掌握
- 2024-09-30Mock.js学习:入门教程与实战演练
- 2024-09-30React Hooks学习:从入门到实践
- 2024-09-30受控组件学习:React中的基础入门教程
- 2024-09-29JS定时器教程:初学者必看指南
- 2024-09-29JS对象教程:初学者的全面指南