vue3性能优化关键点浅谈,让你面试侃侃而谈| 8月更文挑战

656 阅读7分钟

这是我参与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调用

image.png 这是mountComponent函数中的一小部分,如果有兴趣可以在vue2源码中查看。

源码中的地址:vue\src\core\instance\lifecycle.js

这个文件的在190行左右,因为我本地的文件有一些注释,所以不是很准确,不过大致是可以定位的,或者搜索这个函数。

image.png

watcher会在vue进行依赖收集时被dep收集,而dep内部有一个notify函数,这个函数的作用就是将收集到的watcher进行执行。

地址vue\src\core\observer\dep.js

image.png

而在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响应式。

image.png

在这个函数内部会调用initData函数,在这里你可以看到完整的过程。

在watcher中调用的update函数,会将watcher入一个队列,这个队列就是vue中异步更新最重要的地方,在这个队列中会将watcher去重存入一个queue的数组,每个watcher只会存在一次,所以就存在我们平常中给data中数据多次复制只会看到最后一次结果的原因。 image.png 这里的nexttick中会传入循环执行queue数组中watcher的函数,nexttick中也非常简单,进行异步处理,这里边不做多说感兴趣的可以去vue\src\core\util\next-tick.js中查看可以一目了然的看到如何做到异步处理. 传递给nexttick的函数就时执行传递给watcher更新的函数.在watcher中真正执行的是run函数.run函数中执行get函数。

image.png

而run函数中值得注意的只有一个地方,那就是执行getter函数,而getter函数保存的就是我们传递给watcher的第二个参数,可以在上边一张图片中看到watcher中传入的更新函数。 > 这里的watcher函数是不完整的,为了截图展示删除了部分代码

image.png 而在这个的执行中就是组件的更新机制了,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构建依赖关系,在刷新时调用.

image.png

在组件编译的时候会看到两个奇怪的数字.这个数字在vue2中是没有的.

image.png

vue3会利用这两个数字来达到很高的性能优化,在源码中,有两个文件,导出了两个枚举。

image.png

感兴趣可以在源码中查看,通过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函数

image.png 在有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号前端切图师,感谢大家的阅读。此文纯属各方面学习之后个人理解,如果有错误和纰漏,感谢能给予指正。有帮助的话请❤️关注+点赞+收藏+评论+转发❤️