守着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所有子节点去比对变更
经评论提醒,发现vue2中对于纯静态节点也有优化,但是上面demo中由于只有一层纯文字静态节点,所以未判定为static,套一层div后就会标记为static,之后进入patch阶段会判定是静态节点直接return。 vue3中patch则只会比较dynamicChildren节点,静态节点不能进入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中的实现还未来得及看
