快速了解vue3.0优化点

246 阅读6分钟

vue3.0发布已经有一段时间了,那么你现在对它的了解有多少呢?

激动又忐忑的你是不是在各大网站上看到各种关于vue3.0的介绍分析呢?

“vue2.0我还一知半解,what? 3.0都有了?”

👌已经get到大家强烈的求知欲,结合尤大大的各种分享以及各位前辈们的分析总结,小仙会输出vue3.0相关文档~

首先我们来简单了解下vue的发展史:Vue.js 从 1.x 到 2.0 版本,最大的升级就是引入了虚拟 DOM 的概念,它为后续做服务端渲染以及跨端框架 Weex 提供了基础。

Vue.js 2.x 发展了很久,生态已经非常完善了,而且对于 Vue.js 用户而言,它几乎满足了我们日常开发的所有需求。在我们看来 Vue.js 2.x 已经足够优秀,但是在 Vue.js 作者眼中它还不够完美。在迭代 2.x 版本的过程中,尤大大发现了很多需要解决的痛点,比如源码自身的维护性,数据量大后带来的渲染和更新的性能问题,一些想舍弃但为了兼容一直保留的鸡肋 API 等;另外,尤大大还希望能给开发人员带来更好的编程体验,比如更好的 TypeScript 支持、更好的逻辑复用实践等。 那么接下来,我们就一起来看一下 Vue.js 3.0 具体做了哪些优化。

为了大家能够更直观的了解vue3.0的具体优化点,特制导图一份,后面也会针对部分内容进行demo讲解。

性能优化(源码体积优化之tree-shaking):

举个例子,一个 math 模块定义了 2 个方法 square(x) 和 cube(x) :

export function square(x) {
  return x * x
}
export function cube(x) {
  return x * x * x
}

我们在这个模块外面只引入了 plus 方法:

import { cube } from './math.js'
// do something with cube

最终 math 模块会被 webpack 打包生成如下代码:

/***/ (function(module, __webpack_exports__, __webpack_require__) {
  'use strict';
  /* unused harmony export square */
  /* harmony export (immutable) */ __webpack_exports__['a'] = cube;
  function square(x) {
    return x * x;
  }
  function plus(x) {    return x * x * x;
  }
});

可以看到,未被引入的 square 模块被标记了, 然后压缩阶段会利用例如 uglify-js、terser 等压缩工具真正地删除这些没有用到的代码。

性能优化(数据劫持优化):

Vue.js 1.x 和 Vue.js 2.x 内部都是通过 Object.defineProperty 这个 API 去劫持数据的 getter 和 setter,具体是这样的:

Object.defineProperty(data, 'a',{
  get(){
    // track
  },
  set(){
    // trigger
  }
})

但这个 API 有一些缺陷,它必须预先知道要拦截的 key 是什么,所以它并不能检测对象属性的添加和删除。尽管 Vue.js 为了解决这个问题提供了 setset 和 delete 实例方法,但是对于用户来说,还是增加了一定的操作成本。 另外 Object.defineProperty 的方式还有一个问题,举个例子,比如这个嵌套层级比较深的对象:

export default {
  data: {
    a: {
      b: {
        c: {
          d: 1
        }
      }
    }
  }
}

由于 Vue.js 无法判断你在运行时到底会访问到哪个属性,所以对于这样一个嵌套层级较深的对象,如果要劫持它内部深层次的对象变化,就需要递归遍历这个对象,执行 Object.defineProperty 把每一层对象数据都变成响应式的。毫无疑问,如果我们定义的响应式数据过于复杂,这就会有相当大的性能负担。

为了解决上述 2 个问题,Vue.js 3.0 使用了 Proxy API 做数据劫持,它的内部是这样的:

observed = new Proxy(data, {
  get() {
    // track
  },
  set() {
    // trigger
  }
})

由于它劫持的是整个对象,那么自然对于对象的属性的增加和删除都能检测到。

但要注意的是,Proxy API 并不能监听到内部深层次的对象变化,因此 Vue.js 3.0 的处理方式是在 getter 中去递归响应式,这样的好处是真正访问到的内部对象才会变成响应式,而不是无脑递归,这样无疑也在很大程度上提升了性能。

性能优化(编译优化):

 举个例子,比如我们要更新这个组件:

<template>
  <div id="content">
    <p class="text">static text</p>
    <p class="text">static text</p>
    <p class="text">{{message}}</p>
    <p class="text">static text</p>
    <p class="text">static text</p>
  </div>
</template>

整个 diff 过程如图所示:

可以看到,因为这段代码中只有一个动态节点,所以这里有很多 diff 和遍历其实都是不需要的,这就会导致 vnode 的性能跟模版大小正相关,跟动态节点的数量无关,当一些组件的整个模版内只有少量动态节点时,这些遍历都是性能的浪费。

而对于上述例子,理想状态只需要 diff 这个绑定 message 动态节点的 p 标签即可。

Vue.js 3.0 做到了,它通过编译阶段对静态模板的分析,编译生成了 Block tree。Block tree 是一个将模版基于动态节点指令切割的嵌套区块,每个区块内部的节点结构是固定的,而且每个区块只需要以一个 Array 来追踪自身包含的动态节点。借助 Block tree,Vue.js 将 vnode 更新性能由与模版整体大小相关提升为与动态内容的数量相关,这是一个非常大的性能突破。

除此之外,Vue.js 3.0 在编译阶段还包含了对 Slot 的编译优化、事件侦听函数的缓存优化,并且在运行时重写了 diff 算法

语法 API 优化(优化逻辑复用):

举一个鼠标位置侦听的例子,我们会编写如下函数 mousePositionMixin:

const mousePositionMixin = {

  data() {

    return {

      x: 0,

      y: 0

    }

  },

  mounted() {

    window.addEventListener('mousemove', this.update)

  },

  destroyed() {

    window.removeEventListener('mousemove', this.update)

  },

  methods: {

    update(e) {

      this.x = e.pageX

      this.y = e.pageY

    }

  }

}

export default mousePositionMixin

然后在组件中使用:

<template>

  <div>

    Mouse position: x {{ x }} / y {{ y }}

  </div>

</template>

<script>

import mousePositionMixin from './mouse'

export default {

  mixins: [mousePositionMixin]

}

</script>

使用单个 mixin 似乎问题不大,但是当我们一个组件混入大量不同的 mixins 的时候,会存在两个非常明显的问题:命名冲突和数据来源不清晰。

首先每个 mixin 都可以定义自己的 props、data,它们之间是无感的,所以很容易定义相同的变量,导致命名冲突。另外对组件而言,如果模板中使用不在当前组件中定义的变量,那么就会不太容易知道这些变量在哪里定义的,这就是数据来源不清晰。但是Vue.js 3.0 设计的 Composition API,就很好地帮助我们解决了 mixins 的这两个问题

我们来看一下在 Vue.js 3.0 中如何书写这个示例:

import { ref, onMounted, onUnmounted } from 'vue'
export default function useMousePosition() {
  const x = ref(0)
  const y = ref(0)
  const update = e => {
    x.value = e.pageX
    y.value = e.pageY
  }
  onMounted(() => {
    window.addEventListener('mousemove', update)
  })
  onUnmounted(() => {
    window.removeEventListener('mousemove', update)
  })
  return { x, y }
}

这里我们约定 useMousePosition 这个函数为 hook 函数,然后在组件中使用:

<template>
  <div>
    Mouse position: x {{ x }} / y {{ y }}
  </div>
</template>
<script>
  import useMousePosition from './mouse'
  export default {
    setup() {
      const { x, y } = useMousePosition()
      return { x, y }
    }
  }
</script>

可以看到,整个数据来源清晰了,即使去编写更多的 hook 函数,也不会出现命名冲突的问题。

Composition API 除了在逻辑复用方面有优势,也会有更好的类型支持,因为它们都是一些函数,在调用函数时,自然所有的类型就被推导出来了,不像 Options API 所有的东西使用 this。另外,Composition API 对 tree-shaking 友好,代码也更容易压缩。

Composition API 属于 API 的增强,它并不是 Vue.js 3.0 组件开发的范式,如果你的组件足够简单,你还是可以使用 Options API。