Vue3带来了哪些更新和优化

1,929 阅读9分钟

文中部分图片来源于Vue Mastery

前言

笔者最近在学习Vue3,通过本文和各位掘友聊聊Vue3带来了哪些更新和优化。 本文主要内容包括以下两个部分:

  1. Vue 的简要发展历程
  2. Vue3 的更新及优化点

Vue 简要发展历程

我们都知道,与前端页面息息相关的一个东西叫做DOM 或者 Document Object Model。在没有Vue这些框架之前,我们开发HTML页面,会映射到一系列的DOM 节点,由浏览器解析,绘制,最终显示成网页。 image.png 当我们需要改变网页显示的内容,我们会使用 JS 通过浏览器提供的API进行 DOM 操作。如下图: image.png 但是在实际的网页上可能会存在成千上万的DOM 节点,如果需要我们前端开发同学需要通过JS去操作那么多DOM Node,显然是一个体力活,而且还会造成很多问题:

  1. 开发效率低
  2. 代码复杂,维护成本高
  3. 代码量大
  4. ......

Vue1

然后就出现了像 Vue 这样的前端框架帮助我们干这些体力活,我们只需要关心业务数据的变更,框架会自动帮我们更新DOM,当用户更改了DOM的数据,框架又会自动的更新业务数据,省去了很多重复繁琐的工作量。但是随着网页复杂度增加,尽管由框架帮我们完成大量的DOM 节点操作,但是还是无法避免网页性能变差的问题(Vue1)

Vue2 —— 引入Virtual DOM

Vue2为了减少DOM 节点操作引入了Virtual DOM(虚拟DOM),其本质就是使用JavaScript对象去描述DOM Node

Vue会把我们写的template编译成render functionVue执行render function,过程中会进行依赖收集,最终返回Vnode(虚拟DOM节点)。然后由Vue将虚拟的节点创建为真实的DOM并挂载。 image.pngrender函数依赖的状态变更,render函数重新执行得到一个新的Virtual DOM,此时新旧Virtual DOM对比进行差异化DOM修补更新。 image.png

框架引入Virtual DOM操作DOM比原生DOM操作快吗?

我们看下尤大的回答: image.png

Virtual DOM的好处

  1. 让组件的渲染逻辑完全从真实的DOM解耦
  2. 在其他的环境中重用框架的运行时(允许开发人员自定义渲染解决方案,比如IOS、Android等,不局限于浏览器)
  3. 可以在返回实际渲染引擎之前使用JS以编程的方式构造、检查、克隆、操作所需要的DOM Node

就是因为这些优点,Vue3依然使用Virtual DOM。接下来,看看vue3做了哪些优化。

Vue3 及其优化点

Vue3 中,有三个比较核心的模块:

  • Reactivity Module —— 数据响应性处理
  • Compiler Module —— 将单文件组件的 template 编译成 render 函数
  • Render Module —— 负责将Virtual DOM生成DOM Node并挂载网页;patch 新旧Virtual DOM等。Render Module主要在三个阶段工作:
    • render阶段 —— 执行render返回VNode
    • mount阶段 —— 通过VNode创建DOM Node并挂载
    • patch阶段 —— 对比新旧VNode并更新DOM image.png

三个模块协同工作,一个动画看下整个流程: 1.gif 接下来,列举一下优化点。

采用 TS 开发

加入了类型后,更有利于阅读源码和维护。

采用 monorepo 管理项目

image.png 采用 monorepo 管理项目的好处请参考文章为什么越来越多的项目选择 Monorepo?

Vue3 渲染函数的优化

属性对象参数的写法优化

先来看一张图,图中是Vue3Vue2 渲染函数的写法。 image.png 可以看到 Vue3Vue2 渲染函数的参数个数与各个参数的作用差不多,区别最大的就是属性对象参数,也就是第二个参数的写法。

  • Vue2中:需要用户区分事件、DOM属性、组件属性、attribute等,要将对应的属性写在对应的类别中。
  • Vue3中:无需用户区分,扁平结构,直接写即可,Vue会帮我们智能的进行绑定👍🏻
render函数的第一个参数不再是渲染函数
// 在 vue2 中,如果我们将 render 的逻辑进行拆分
// 那么在调用各个拆分出来的函数时,要手动把渲染函数h传递进去
// 写 jsx 的时候不需要
export default {
  data() {
    return {}
  },
  methods: {
    renderCondition(h) {
      //...
    },
    defaultRender(h) {
      //...
    }
  },
  render(h) {
    if (condition) {
      return this.renderCondition(h)
    } else {
      return this.defaultRender(h)
    }
  }
}

vue3中:

import { h } from 'vue'
// 直接使用即可

插槽变成了一个函数,而不是虚拟节点

比如:

<template>
  <Stack>
    <div>1</div>
    <div>2</div>
    <Stack>
      <div>3</div>
      <div>5</div>
      <Stack>
        <div>6</div>
        <div>7</div>
      </Stack>
    </Stack>
  </Stack>
</template>

我们想要这个Stack渲染成以下的样子,并可以无限嵌套使用:

  • 1
  • 2
    • 3

    • 5

      • 6
      • 7

使用渲染函数实现:

<script>
import { h } from 'vue'
export default {
  render() {
    return h('div',
      {
        class: 'margin-l-12'
      },
      // 在Vue2, this.$slots.default就是VNode,不需要执行
      this.$slots.default && this.$slots.default() || [])
  }
}
</script>
<style>
.margin-l-12 {
  margin-left: 12px;
}
</style>

效果:

image.png

template编译优化

静态DOM节点Hoisted

image.png 我们的template中有两个静态的DOM节点,意味着这俩节点跟我们的状态没有关系,所以编译时会被提升,这样的好处是啥?

  1. patch的时候无需关注这些静态节点,提升patch的速度。在Vue2的时候,我们会去对比新旧虚拟DOM,不论这个节点是不是包含动态的内容都会去遍历对比,显然静态的节点是非常没有必要对比的(它不会消失,也不会变化)。
  2. 静态节点只会被创建一次,后面都是重用,减少了重复创建、回收的开销。
动态节点及动态属性识别

image.png

compiler在将template转化渲染函数时会生成一些辅助性的标识用于提升组件更新的速度,比如上图中圈出的地方,文本为dym的节点 class 是动态的,所以编译过后生成了一个 /*CLASS*/,当组件更新处理该节点时,无需关注该节点的其他属性(innerText/style/...),只需要关心class。一个节点我们可能觉得这点提升微不足道,但是往往我们的应用页面会由很多节点构成,在Vue2中没有这些辅助的标识帮助我们识别哪些属性是可以跳过不处理的,所以会对整个节点的所有属性都去检查是否变更,当节点数量多了以后无疑会造成阻塞或者卡顿,Vue3做到了,只检查可能会变化的那些属性,极大的提升了性能。

Block Optimization

译为”块优化“,我们注意到编译生成的render函数中有openBlock,其作用是用来收集某个block内的动态子节点,最终这个block上会生成一个存着该block内动态节点的扁平数组。这样就更进一步的缩小了组件更新时的新旧VNode的对比范围,也进一步提升了性能。

Reactivity

什么是响应式? —— 自动更新,状态自动保持同步

Vue3是如何实现的?

Vue3使用Proxy来实现状态变更的拦截,相比Vue2使用Object.defineProperty实现,优点如下:

  1. Object.defineProperty是通过给对象新增属性/修改现有属性 来实现 getter/setter 的拦截。需要遍历对象的每一个key去实现,当遇到很大的对象或者嵌套层级很深的对象,性能问题会很明显。而Proxy则是通过在对象的访问前架设拦截,是完完全全的代理模式,性能远远优于Object.defineProperty这种方式。
  2. Object.defineProperty这种方式无法拦截到给对象新增属性这种操作,因为组件初始化不能预知会新增哪些属性,也就没法设置getter/setter,所以我们不得不使用Vue2提供的$setapi,再去Object.defineProperty给新增的属性加上getter/setter。而使用Proxy无需关心这个问题。
  3. Proxy天然支持Array,无需再去拦截会改变原数组的那些原型方法(shift、unshift、push、pop、sort、splice、reverse),甚至你支持修改数组的length也会被拦截。
  4. Proxy不支持IE,啥年代了,这应该也是个优点。

Composition API & setup hook

Composition API

组合式 API (Composition API) 是一系列 API 的集合,使我们可以使用函数而不是声明选项的方式书写 Vue 组件。它包含了这些API:

  • 响应式API —— refreactivecomputedwatch......
  • 生命周期钩子 —— onMountedonUnmounted......
  • 依赖注入 —— provideinject......

Composition Api是为了更好的 代码逻辑组织 以及更好的代码复用能力设计的。

setup hook

Vue3还新增了setup hook,其特点如下:

  1. setup 这个hook是最先执行的。
  2. setup 内不能访问this,因为就是故意这么设计的。
  3. setup 返回一个对象,对象内的所有属性都可在其他 option 以及 template 中使用。
  4. setup 返回的对象中属性值为 ref 的,在 template 中无需使用 .value
  5. 完全支持与 options api 同时使用,加一个 setup 选项即可。
更好的代码逻辑组织

Vue2中,我们为了实现一个功能,往往需要在多个option选项中进行开发 —— data 中写一点,watch中写一点,methods中写一点......,这种情况在复杂的组件中尤为常见,同一个功能代码也尤其分散,造成了不好的开发体验以及后续维护上的困难。因此 Vue3 设计了 Composition Api,我们现在能够将一个功能的代码实现在一起,更加聚合,更加容易维护。再次放上一张官网的图: image.png

更好的代码复用能力
  • Vue2,我们往往通过 mixin 来实现代码逻辑复用,它存在以下几个问题:
    1. 变量来源不清,当我们没有注意到当前组件的mixin选项,我们可能看到一些当前组件没有定义的属性、方法,很可能会懵。当存在多个mixin,变量和方法的来源变得更加不清
    2. 变量和方法命名冲突,当存在多个mixin,变量和方法冲突变得极其容易
  • setup中,我们可以像编写普通的 JavaScript 那样来编写组件代码,我们的逻辑都是一个一个的变量或者函数,所以我们在写Composition Api的代码时可运用上所有JavaScript代码组织的最佳实践,很轻松的对这些函数拆分,提取以达到复用的目的。

笔者对此也写的有一篇文章,感兴趣的掘友可以移步看下4种方案带你探索 Vue.js 代码复用的前世今生

更好的开发体验

Composition Api主要利用基本的变量和函数,它们本身就是类型友好的。用Composition Api重写的代码可以享受到完整的类型推导,不需要书写太多类型标注。大多数时候,用 TypeScript 书写的组合式 API 代码和用 JavaScript 写都差不太多!让许多纯 JavaScript 用户也能从 IDE 中享受到部分类型推导功能。

更小的生产代码体积
  • 一方面<script setup> 形式书写的组件模板被编译为了一个内联函数,和 <script setup> 中的代码位于同一作用域。不像options api需要依赖 this 上下文对象访问属性,被编译的模板可以直接访问 <script setup> 中定义的变量,无需从实例中代理。这对代码压缩更友好,因为本地变量的名字可以被压缩,但对象的属性名则不能
  • 另一方面 Vue3 本身对tree shaking的支持就比较好。

watchEffect

Vue3 新增了watchEffect API,笔者简要描述一下watchwatchEffect的区别:

先看一下他们的用法:

watch(() => state.count, (val, oldVal) => {
  // 与vue2的watch几乎完全一样,除了第一个参数,要不是个ref, 要不是个函数返回一个值
  // 回调是懒执行的,只有当监听的值变更才会执行
  // 回调函数内访问其他响应式值,这些响应式值的变更不会触发回调(与watchEffect的区别)
  // 回调可以获取到当前值 和 旧值
}, {
  immediate: true, // 立即执行
  deep: true // 如果监听一个对象,可以设置深度监听
})

watchEffect(() => {
  // 会马上执行这个函数,并且函数内的所有响应式值的dep会添加这个effect
  // 当函数内任意一个响应式值变更,都会重新执行这个effect
})

在大多数情况下,你可能使用watchEffect要多一些,比如一个初始化加载数据的例子: 使用watch

import { watch, reactive, ref, watchEffect, computed, mounted } from 'vue'
export default {
  props: ['id'],
  data() {
    return {
      busiData: null
    }
  },
  created() {
    this.fetchData(this.id)
  },
  methods: {
    fetchData(id) {
       fetch(url + id).then(res => res.json()).then(data => this.busiData = data)
    }
  },
  setup() {
    watch(() => this.id, (val, oldVal) => {
      this.fetchData(val)
    })
    return {}
  }

使用watchEffect

export default {
  props: ['id'],
  setup(props) {
    const busiData = reactive(null)
    watchEffect(() => {
      fetch(url + props.id).then(res => res.json).then(data => busiData.value = data)
    })
    return {}
  }
}

可以很明显的看到,使用watchEffect实现这个功能,代码量更少,更简洁!

Vue3还有很多小优化和更新,笔者不在此一一列举了,一些主要的点都在上面了。

总结

本文主要内容如下:

  • 由浅入深,简要描述了Vue的发展历程
  • Virtual DOM相关问题
  • Vue3更新点及优化点
    • TS开发
    • monorepo管理项目
    • 渲染函数优化
    • 模版编译优化
    • 响应式优化
    • Composition API & setup

我们能够明显感受到,Vue3在性能、逻辑复用、逻辑组织、开发体验等很多方面的提升是巨大的!👍🏻

结语

如果文中有些的不对的地方,还请指正一下,谢谢! 如果本文对你有一点点帮助,点个赞支持一下吧,你的每一个【】都是我创作的最大动力 ^_^