这是我参与8月更文挑战的第一天,活动详情查看:8月更文挑战
起始
vue3已经上线很长时间了,相信很多公司已经陆续开始使用vue3,未来相信也会越来越多.如果好比vue2是一家公司,vue3就是一家拥有后台管理的公司.在vue3中性能提升在源码中可以达到肉眼可见的程度. 因为我所在的公司主要技术栈是react,所以对vue的理解也不是很深入.这篇文章是我本人对于vue3的理解,属于浅谈.可能有地方说的不对,欢迎评论指出。
vue3
- vue中大家都知道vue3中将响应式处理从
defineProperty变成了proxy,这个属于大家都知道的东西,当然在响应式过程中也做了一些变化,这里就不做说明。 - vue3中删除了Filters,on,off,$once,其中on,off,once3个方法被认为不应该由vue提供,因此被移除了,可以使用其他三方库实现(mitt)。
- 这篇文章主要说明vue在数据发生改变时相对于vue2发生了什么样的改变。
- 对于vue3的其他改动可以看羊村长的一篇文章,会让你受益良多
vue2
在看vue3之前先了解vue2更新过程,会让我们更真实的感受到vue3优化的好处.顺便可以了解一下vue2的一些更新机制。
vue3之前以下所有vue代指vue2
在vue中,组件在创建时,在$mount这个函数中会调用mountComponent函数,而在mountComponent函数中,会创建一个watcher,传递进入一个更新函数,供dep调用
这是mountComponent函数中的一小部分,如果有兴趣可以在vue2源码中查看。
源码中的地址:vue\src\core\instance\lifecycle.js
这个文件的在190行左右,因为我本地的文件有一些注释,所以不是很准确,不过大致是可以定位的,或者搜索这个函数。
watcher会在vue进行依赖收集时被dep收集,而dep内部有一个notify函数,这个函数的作用就是将收集到的watcher进行执行。
地址vue\src\core\observer\dep.js
而在dep中调用的是watcher的update函数,这张图片也可以很好的说明,subs就是和这个dep相关的watcher的数组。
如果有人对dep和watcher的关系不太了解也没关系,一个组件会产生一个watcher,data中的数据会产生dep,dep会与watcher产生关系,在数据发生变化时,dep就会调用内部的notify函数,dep与watcher的关系源码中可以在vue\src\core\instance\init.js中调用initState完成data响应式。
在这个函数内部会调用initData函数,在这里你可以看到完整的过程。
在watcher中调用的update函数,会将watcher入一个队列,这个队列就是vue中异步更新最重要的地方,在这个队列中会将watcher去重存入一个queue的数组,每个watcher只会存在一次,所以就存在我们平常中给data中数据多次复制只会看到最后一次结果的原因。
这里的nexttick中会传入循环执行queue数组中watcher的函数,nexttick中也非常简单,进行异步处理,这里边不做多说感兴趣的可以去
vue\src\core\util\next-tick.js中查看可以一目了然的看到如何做到异步处理.
传递给nexttick的函数就时执行传递给watcher更新的函数.在watcher中真正执行的是run函数.run函数中执行get函数。
而run函数中值得注意的只有一个地方,那就是执行getter函数,而getter函数保存的就是我们传递给watcher的第二个参数,可以在上边一张图片中看到watcher中传入的更新函数。 > 这里的watcher函数是不完整的,为了截图展示删除了部分代码
而在这个的执行中就是组件的更新机制了,diff算法什么的,应该大部分人都是清楚的,而其中我比较关心的就是虚拟dom的比较了,下边我直接将代码加上注释复制在下方,感兴趣的可以前往源码查看。
// todo 比较两个虚拟dom
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
// todo 狗子调用
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
// todo 1.获取两个带比较节点的孩子
const oldCh = oldVnode.children
const ch = vnode.children
// todo 2.属性更新
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)
}
// todo 3.没有文本
if (isUndef(vnode.text)) {
// todo 双方均有孩子: 比较子节点 !!!重要的地方
if (isDef(oldCh) && isDef(ch)) {
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
// todo 新节点有孩子 老节点没有
} else if (isDef(ch)) {
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
// todo 新增 老的节点清楚文本之后添加新的孩子节点
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
// todo 老节点有孩子 新节点没有
} else if (isDef(oldCh)) {
// todo 删除老节点的孩子
removeVnodes(oldCh, 0, oldCh.length - 1)
// todo 老节点又文本的情况 因为在上边判断新节点没有文本的情况下进入此分支
} else if (isDef(oldVnode.text)) {
// todo 文本清空操作
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// todo 文本更新
nodeOps.setTextContent(elm, vnode.text)
}
// todo 钩子
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
上方双方均有孩子: 比较子节点的时候执行updateChildren也就是diff发生的地方。
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
// todo 设置收尾的4个游标以及相对应的节点
let oldStartIdx = 0
let oldStartVnode = oldCh[0]
let oldEndIdx = oldCh.length - 1
let oldEndVnode = oldCh[oldEndIdx]
let newStartIdx = 0
let newStartVnode = newCh[0]
let newEndIdx = newCh.length - 1
let newEndVnode = newCh[newEndIdx]
// todo 后面进行查找时所需的变量
let 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
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
// todo 开始循环, 结束条件:开始游标不能超过结束游标
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// todo 前两种情况时游标的调整(有时候移动或者删除会导致游标对应的节点空了 需要重新设置节点)
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
// todo 两个开头相同
} else if (sameVnode(oldStartVnode, newStartVnode)) {
//! patchVnode 为打补丁
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// todo 游标移动
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// todo 老的结束和新的结束相同
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// todo 老的开头与新的结束相同
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// todo 移动改节点到队尾
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// todo 老的结束与新的开始相同
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// todo 移动到队首
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// todo 首位没有找到相同节点,从新的开头拿出一个节点去老的数组查找
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
idxInOld = isDef(newStartVnode.key)
? oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// todo 如果再老数组中没有找到
if (isUndef(idxInOld)) { // New element //todo 新增
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
} else {
// todo 找到的情况 更新
vnodeToMove = oldCh[idxInOld]
if (sameVnode(vnodeToMove, newStartVnode)) {
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
// todo 移动到队首
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// same key but different element. treat as new element
// todo 发现相同的键但是元素不同 会删除老节点插入新节点
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
}
}
newStartVnode = newCh[++newStartIdx]
}
}
// todo 清理工作
// todo 如果老树结束 判断新树中是否有剩余元素 如果有则批量新增
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
// todo 如果新树结束 删除老树剩余的元素
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
vue2结束
以上就是vue的更新策略了,vue3相对于vue2有一些改变,这些改变使得vue3的性能得到很大的提升.接下来说一些我了解到的vue3对于性能提升的改变。
vue3
以下所有vue代指vue3
vue3中在编译阶段也做出了改变,使用setupRenderEffect函数在内部会使用effect构建依赖关系,在刷新时调用.
在组件编译的时候会看到两个奇怪的数字.这个数字在vue2中是没有的.
vue3会利用这两个数字来达到很高的性能优化,在源码中,有两个文件,导出了两个枚举。
感兴趣可以在源码中查看,通过vue原本的注释获得很多新的信息.
vue-next\packages\shared\src\patchFlags.ts
vue-next\packages\shared\src\shapeFlags.ts
vue会通过在编译时的标记,来判断这个元素的内容,是否需要修改,比如是一个静态节点,不会做任何操作,也不会对比。进行下一项操作。比如内容是一个插值语法只有文本,那么他只会对比两个文本进行比对,属性之类如果没有标记为动态则不会发生比对,他只会对比发生变化的地方。这样就达到了性能的大幅提升。而对于他的二进制标识,也起到了很大的作用,每一个表示不同的意义从而达到控制的效果。令人眼前一亮。对于这个数字有什么意义,可以查看源码通过命名和注释都可以得到答案。 而在v-for循环的数组的处理也发生了变化,在有key和无key时处理并不相同。在没有key时是典型的重排操作,使⽤patchChildren更新,再次过程中不再进行vue2中的diff之类,只会比较同级节点做出修改。
vue-next\packages\runtime-core\src\renderer.ts 1600行左右patchChildren函数
在有key时更新过程发生了变化,最后来了解一下有key时的更新策略。
在结构中如果有key,在数据中先找出首位相同的节点进行保存,没有找到则无,完成之后查看中间剩余再考虑新增或者删除,下边是一个模拟的效果。
['a', 'b', 'c', 'd']
['a', 'e', 'b', 'c', 'd']
1.寻找数组头部的相同节点保存
['b', 'c', 'd']
['e', 'b', 'c', 'd']
2.寻找数组尾部的相同节点保存
[]
['e']
3.新增
代码太多就不贴在这里,源码中的地址放在下边,建议观看,源码中这里的注解非常简单易懂。如果以前你对于key不是很看重,那么你就要开始重视起来了,看比较性能差别还是比较大的
源码中的地址vue*-next\packages\runtime-core\src\renderer.ts 1770行左右patchKeyedChildren函数
到这里暂告一段落,这是一些我个人对vue的理解,希望对你有所帮助。vue3的新特性远远不止这些,等待你的探索,加油,切图少年。
最后
我是007号前端切图师,感谢大家的阅读。此文纯属各方面学习之后个人理解,如果有错误和纰漏,感谢能给予指正。有帮助的话请❤️关注+点赞+收藏+评论+转发❤️