你不知道的 Vue3 优化。

前言

使用 Vue2 的时候,碰到了一些性能瓶颈,然后在看 Vue3 源码的时候,发现 Vue3 的优化有点意思,大多数人写过的 Vue3 优化我就不说了,说一说比较细的几个方面。

方面

  1. 响应式初始化的优化。
  2. 编译上的优化。
  3. patch 上的优化。
  4. 插槽对组件更新的影响。

响应式初始化的优化

在 Vue2 中,对非数组数据的劫持是通过 Object.defineProperty,因为要确保所有数据的变化都能被监听到,不论数据数据多深,用到没,只能无脑递归。

// data.js
// 模拟大数据

const data = []

for (let i = 0; i < 150; i++) {
  data.push({ name: 1, sex: 1, age: 1 })
}

const children = JSON.stringify(data)
for (const item of data) {
  item.children = JSON.parse(children)
  for (const it of item.children) {
    it.children = JSON.parse(children)
  }
}

Vue2

<!-- html -->
<div id="app">
    我是 Vue2,<button @click="changeData">增加数据</button>
</div>
  
  
<!-- script -->
<script src="https://unpkg.com/vue@2.6.14"></script>
<script src="./data.js"></script>
<script>
    new Vue({
      el: '#app',
      data() {
        return {
          data: []
        }
      },
      methods: {
        changeData() {
          this.data = data
        }
      }
    })
</script>

Vue2 性能

页面加载之后,我们点击按钮,来看下性能。我们能发现响应式数据耗费了大概 8.7s,哪怕我们页面中并没有用到这个数据。

image.png

Vue3

<!-- html -->
<div id="app">
    我是 Vue2,<button @click="changeData">增加数据</button>
</div>
  
  
<!-- script -->
<script src="https://unpkg.com/vue@latest"></script>
<script src="./data.js"></script>
<script>
    Vue.createApp({
      data() {
        return {
          data: {}
        }
      },
      methods: {
        changeData() {
          this.data = data
        }
      }
    }).mount('#app')
</script>

Vue3 性能

可以看到耗费时间大约 0.1s,因为没有使用脚手架创建工程,所以这个 0.1s 中有很多时间用在了模板编译上,实际上的时间就更少了。

image.png

Vue3 和 Vue2 响应式数据最大的区别就是,Vue3 是用到哪个数据,才会去对这个数据做劫持,我下面贴一段 Vue3 响应式处理的代码。

export function track(target: object, type: TrackOpTypes, key: unknown) {
  if (!isTracking()) {
    return
  }
  
  // 判断数据是否响应式处理过。
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  
  // 判断数据的这个 key 是否做过响应式劫持。
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = createDep()))
  }

  const eventInfo = __DEV__
    ? { effect: activeEffect, target, type, key }
    : undefined
    
  // 收集依赖
  trackEffects(dep, eventInfo)
}

确实有点意思,我觉得可以叫做惰性劫持。

编译

Vue3 利用编译做了一些优化,而且对性能的提升是非常高的,现在贴一段代码,看看在 Vue2,Vue3 编译过后有什么不同。

<ul>
  <li>
    我是静态数据1
    <span>123</span>
  </li>
  <li>我是静态数据2</li>
  <li>我是静态数据3</li>
  <li>我是静态数据4</li>
  <li>我是静态数据5</li>
  <li>{{ name }}</li>
</ul>

Vue2

在 Vue2 编译中主要的优化是:找到静态节点 => 找到值得标记的静态根节点,静态节点就是不会变化的节点,而静态根节点就是静态节点最大的祖先,也不会变化。这样每次组件更新的时候就可以跳过这些节点,从而提升性能。

image.png

Vue3

Vue3 将所有可以提升的节点全部提升,从而每次 render 的时候不会重复创建静态节点。

image.png

patch

接着上面 Vue3 的编译继续讲,你可以看到 Vue3 编译的时候调用了 _openBlock 这个方法,下面来讲讲这个方法做了什么。

image.png

// 这个就是 _openBlock 函数
// 我们可以看到创建了一个数组,然后放到了 blockStack 中。
export function openBlock(disableTracking = false) {
  blockStack.push((currentBlock = disableTracking ? null : []))
}

现在我们回顾刚才 render 函数里面创建 VNode 的方法,创建 VNode 的方法都会调用 createBaseVNode 这个方法,我们在下面可以看到他把当前这个 VNode 放到了下面的 currentBlock 数组里面,Vue3 是把动态节点和所有能提升的节点隔离开来,然后把当前组件所创建的 VNode 放到了一个数组里面。

image.png

function createBaseVNode(
  type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
  props: (Data & VNodeProps) | null = null,
  children: unknown = null,
  patchFlag = 0,
  dynamicProps: string[] | null = null,
  shapeFlag = type === Fragment ? 0 : ShapeFlags.ELEMENT,
  isBlockNode = false,
  needFullChildrenNormalization = false
) {

  // 省略代码。。。
  
  // track vnode for block tree
  if (
    isBlockTreeEnabled > 0 &&
    // avoid a block node from tracking itself
    !isBlockNode &&
    // has current parent block
    currentBlock &&
    // presence of a patch flag indicates this node needs patching on updates.
    // component nodes also should always be patched, because even if the
    // component doesn't need to update, it needs to persist the instance on to
    // the next vnode so that it can be properly unmounted later.
    (vnode.patchFlag > 0 || shapeFlag & ShapeFlags.COMPONENT) &&
    // the EVENTS flag is only for hydration and if it is the only flag, the
    // vnode should not be considered dynamic due to handler caching.
    vnode.patchFlag !== PatchFlags.HYDRATE_EVENTS
  ) {
    // 我在这里
    currentBlock.push(vnode)
  }

  return vnode
}

现在我们继续接着 openBlock 挨着的这个方法,我们看他做了什么。

image.png

// 它调用了 setupBlock 方法
export function createElementBlock(
  type: string | typeof Fragment,
  props?: Record<string, any> | null,
  children?: any,
  patchFlag?: number,
  dynamicProps?: string[],
  shapeFlag?: number
) {
  return setupBlock(
    createBaseVNode(
      type,
      props,
      children,
      patchFlag,
      dynamicProps,
      shapeFlag,
      true /* isBlock */
    )
  )
}


function setupBlock(vnode: VNode) {
  // 给当前组件根节点挂载一个数组,存放所有创建的 VNode
  vnode.dynamicChildren = isBlockTreeEnabled > 0 ? currentBlock || (EMPTY_ARR as any) : null
  // 弹出
  closeBlock()
  // a block is always going to be patched, so track it as a child of its
  // parent block
  if (isBlockTreeEnabled > 0 && currentBlock) {
    currentBlock.push(vnode)
  }
  return vnode
}


// 弹出
export function closeBlock() {
  blockStack.pop()
  currentBlock = blockStack[blockStack.length - 1] || null
}

接下来我们去看看这个 dynamicChildren 到底是要做什么。

代码太多,下面以图片示例,组件 patch 的时候会调用一些 patch 的子方法,其中会对 dynamicChildren 做判断,如果有,直接用新旧两个 dynamicChildren 去对比,Vue2 中你不论怎么办,你都得从头比到脚,而在 Vue3 中,可以直接对会变化的节点进行对比,这个是之前不敢想像的。

image.png

静态插槽的优化

我们平常项目开发中经常会这样使用组件。

<!-- 组件内 -->
<div>

    <!-- 某个子组件,我们会直接插进来一些内容 -->
    <Child>
        {{ name }}
    </Child>

</div>

如上所示,在 Vue2 和 Vue3 中的效果是截然不同的。

Vue2

如图所示,子组件插槽的编译和 VNode 的生成都是在父组件,在 Vue2 中这种情况,只要父组件更新,子组件就会伴随着强制更新,如果我们这个子组件很大,那么就会带来不必要的性能开销。

image.png

Vue3

如图所示,静态插槽被归类成了作用域插槽,也就意味着子组件插槽的编译是在父组件,但是这个生成 VNode 的函数会在子组件内调用生成,也就意味着子组件会被所使用的响应式数据收集到依赖中,也就只有这些数据发生变化时,才会通知子组件更新,子组件也不会伴随父组件更新而强制更新。

image.png

插槽的影响可以看我之前的这篇文章:理解Vue内部渲染机制(避免页面性能瓶颈)

总结

Vue3 确实有点意思!!!