Vue3.0新特性

1,362 阅读7分钟

本文主要讲解Vue3常用的新特性,详细可以查看Vue3.0官网教程

Vue3 比 Vue2 到底强在哪里?

  • Vue3 具有更明显的性能的提升(具体体现在:打包大小更小,初次渲染更快,更新更快,内存使用减少等优点)
  • Vue3 的 composition API 解决了组件碎片化的问题,使组件更具逻辑化,方便后期维护。
  • Vue3 增加了一些好用的新特性,如片段(Fragment)、Tree-shaking 和 Teleport 等。

Fragment(片段)

在 Vue 2.x 中,由于不支持多根节点组件,当开发者意外创建一个时会发出警告 (The template root requires exactly one element.)。

在 Vue 3.x 中,组件可以包含多个根节点

<!-- Layout.vue -->
<template>
  <header>...</header>
  <main v-bind="$attrs">...</main>
  <footer>...</footer>
</template>

一个新的全局 API:createApp

在 Vue 2.x 中使用 new 的方式来创建一个Vue实例,而 Vue 3.x 则用 Vue.createApp 来初创建一个Vue实例。

const app = Vue.createApp({
  /* 选项 */
})

以下是 Vue2 全局 API 对应 Vue3 全局API的表

2.x 全局 API3.x 实例 API (app)
Vue.configapp.config
Vue.config.productionTip移除
Vue.config.ignoredElementsapp.config.isCustomElement
Vue.componentapp.component
Vue.directiveapp.directive
Vue.mixinapp.mixin
Vue.useapp.use
Vue.prototypeapp.config.globalProperties

Tree-shaking

Tree-shaking的本质是消除无用的js代码。无用代码消除在广泛存在于传统的编程语言编译器中,编译器可以判断出某些代码根本不影响输出,然后消除这些代码,这个称之为DCE(dead code elimination)。 Tree-shaking 是 DCE 的一种新的实现,Javascript同传统的编程语言不同的是,javascript绝大多数情况需要通过网络进行加载,然后执行,加载的文件大小越小,整体执行时间更短,所以去除无用代码以减少文件体积,对javascript来说更有意义。

在 Vue 3.x 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。例如:

// vue 2
import Vue from 'vue'

Vue.nextTick(() => {
  // 一些和DOM有关的东西
})

// vue 3
import { nextTick } from 'vue'

nextTick(() => {
  // 一些和DOM有关的东西
})
//vue 2
import { shallowMount } from '@vue/test-utils'
import { MyComponent } from './MyComponent.vue'

test('an async feature', async () => {
  const wrapper = shallowMount(MyComponent)

  // 执行一些DOM相关的任务

  await wrapper.vm.$nextTick()

  // 运行你的断言
})

//vue 3
import { shallowMount } from '@vue/test-utils'
import { MyComponent } from './MyComponent.vue'
import { nextTick } from 'vue'

test('an async feature', async () => {
  const wrapper = shallowMount(MyComponent)

  // 执行一些DOM相关的任务

  await nextTick()

  // 运行你的断言
})

通过这一更改,如果模块打包工具支持 tree-shake,则 Vue 应用程序中未使用的全局 API 将从最终的打包产物中排除,从而获得最佳的文件大小。

Vue 2.x 中的这些全局 API 受此更改的影响

  • Vue.nextTick
  • Vue.observable (用 Vue.reactive 替换)
  • Vue.version
  • Vue.compile (仅完整构建版本)
  • Vue.set (仅兼容构建版本)
  • Vue.delete (仅兼容构建版本)

生命周期

Vue2 和 Vue3 生命周期的对比 image.png

emits选项(新增)

和 prop 类似,组件可触发的事件可以通过 emits 选项被定义

<template>
  <div>
    <p>{{ text }}</p>
    <button v-on:click="$emit('accepted')">OK</button>
  </div>
</template>
<script>
  export default {
    props: ['text'],
    emits: ['accepted']
  }
</script>

该选项也可以接收一个对象,该对象允许开发者定义传入事件参数的验证器,和 props 定义里的验证器类似。

Teleport

Teleport 是一种能够将我们的模板移动到 DOM 中 Vue app 之外的其他位置的技术,就有点像哆啦A梦的“任意门”。下面举个🌰:

//index.html

  <div id="app"></div>
+  <div id="teleport-target"></div>
  <script type="module" src="/src/main.js"></script>
//src/components/HelloWorld.vue 中,添加如下,留意 to 属性跟上面的 id 选择器一致

  <button @click="showToast" class="btn">打开 toast</button>
  <!-- to 属性就是目标位置 -->
  <teleport to="#teleport-target">
    <div v-if="visible" class="toast-wrap">
      <div class="toast-msg">我是一个 Toast 文案</div>
    </div>
  </teleport>
  
<script>
  import { ref } from 'vue';
  export default {
  setup() {
    // toast 的封装
    const visible = ref(false);
    let timer;
    const showToast = () => {
      visible.value = true;
      clearTimeout(timer);
      timer = setTimeout(() => {
        visible.value = false;
      }, 2000);
    }
    return {
      visible,
      showToast
    }
  }
}
</script>

效果图以及更佳详细地理解可以看这篇文章(文章清晰易懂)Vue 3 任意传送门——Teleport

移除了 $children

在 Vue2.x 中,开发者可以使用 this.$children 直接访问当前实例的子组件:

<template>
  <div>
    <img alt="Vue logo" src="./assets/logo.png">
    <my-button>Change logo</my-button>
  </div>
</template>

<script>
import MyButton from './MyButton'

export default {
  components: {
    MyButton
  },
  mounted() {
    console.log(this.$children) // [VueComponent]
  }
}
</script>

在 Vue3.x 中,$childrens 用法已移除,不再支持。

移除了过滤器 filter

在 Vue2.x 中可以使用过滤器来处理通用文本格式。

<template>
  <h1>Bank Account Balance</h1>
  <p>{{ accountBalance | currencyUSD }}</p>
</template>

<script>
  export default {
    props: {
      accountBalance: {
        type: Number,
        required: true
      }
    },
    filters: {
      currencyUSD(value) {
        return '$' + value
      }
    }
  }
</script>

在 Vue3.x 中,过滤器已删除,不再支持。相反地,Vue官方建议用方法调用或计算属性来替换它们。

移除了 .sync 修饰符

在 Vue2.x 中,我们有时候需要对某一个 prop 进行 “双向绑定” ,可以进行以下操作。

<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
// 简写
<ChildComponent :title.sync="pageTitle" />

相当于 Vue3.x 中以v-model修饰符代替.sync修饰符。

<ChildComponent v-model:title="pageTitle" />

移除 v-on.native 修饰符

在 Vue2.x 中,要将原生 DOM 监听器添加到子组件的根元素中,可以使用 .native 修饰符

<my-component
  v-on:close="handleComponentEvent"
  v-on:click.native="handleNativeClickEvent"
/>

在 Vue3.x 中,v-on 的 .native 修饰符已被移除。同时,新增的 emits 选项允许子组件定义真正会被触发的事件。

<my-component
  v-on:close="handleComponentEvent"
  v-on:click="handleNativeClickEvent"
/>
// MyComponent.vue
<script>
  export default {
    emits: ['close']
  }
</script>

移除 $listeners

$listeners 对象在 Vue 3 中已被移除。具体可以看官方文档

移除 $(on、off、once)

在 Vue3 中$(on,off,once) 实例方法已被移除,应用实例不再实现事件触发接口。

<script>
	created() {
        console.log(this.$on, this.$once, this.$off) // undefined undefined undefined
	}
</script>

!!!重头戏来了,Composition API是 Vue3里最重要的特性之一!!!

Composition API

什么是Composition API?

为了更好地理解Composition API,我找到了 大帅老猿 关于Composition API的4张动画图,图出自 做了一夜动画,就为让大家更好的理解Vue3的Composition Api

我们先来看看 Vue 2 中的Options API是什么样子的: 1bd101840df446c78d52e9c14711aae7_tplv-k3u1fbpfcp-watermark.gif

当我们的项目有了新的需求时,往往是需要在Options API中添加如下功能代码:

568b0ced69f241d282cf2c512e4e5f33_tplv-k3u1fbpfcp-watermark.gif

当我们的组件开始变得更大时,逻辑关注点的列表也会增长。尤其对于那些一开始没有编写这些组件的人来说,这会导致组件难以阅读和理解。因此Vue3 Composition API就是为了解决这种问题。 当我们使用了Vue3的 Composition API后可以使同一个逻辑关注点相关代码收集在一起,这样就增加了可阅读性:

d05799744a6341fd908ec03e5916d7b6_tplv-k3u1fbpfcp-watermark.gif

4146605abc9c4b638863e9a3f2f1b001_tplv-k3u1fbpfcp-watermark.gif

既然我们知道了为什么,我们就可以知道怎么做。为了开始使用组合式 API,我们首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup

新的 setup 选项在组件创建之前执行,一旦 props 被解析,就将作为组合式 API 的入口。setup 选项是一个接收 propscontext 的函数。

(⚠️注意:在 setup 中你应该避免使用 this,因为它不会找到组件实例。setup 的调用发生在 data property、computed property 或 methods 被解析之前,所以它们无法在 setup 中被获取。)

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {
    console.log(props) // { user: '' }

    return {} // 这里返回的任何内容都可以用于组件的其余部分
  }
  // 组件的“其余部分”
}

带 ref 的响应式变量

在 Vue 3.0 中,我们可以通过一个新的 ref 函数使任何响应式变量在任何地方起作用,如下所示:

import { ref } from 'vue'

const counter = ref(0)

ref 接收参数并将其包裹在一个带有 value property 的对象中返回,然后可以使用该 property 访问或更改响应式变量的值:

import { ref } from 'vue'

const counter = ref(0)

console.log(counter) // { value: 0 }
console.log(counter.value) // 0

counter.value++
console.log(counter.value) // 1

将值封装在一个对象中,看似没有必要,但为了保持 JavaScript 中不同数据类型的行为统一,这是必须的。这是因为在 JavaScript 中,Number 或 String 等基本类型是通过值而非引用传递的,在任何值周围都有一个封装对象,这样我们就可以在整个应用中安全地传递它,而不必担心在某个地方失去它的响应性。

在 setup 内注册生命周期钩子

为了使组合式 API 的功能和选项式 API 一样完整,我们还需要一种在 setup 中注册生命周期钩子的方法。

// src/components/UserRepositories.vue `setup` function
import { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted } from 'vue'

// 在我们的组件中
setup (props) {
  const repositories = ref([])
  const getUserRepositories = async () => {
    repositories.value = await fetchUserRepositories(props.user)
  }

  onMounted(getUserRepositories) // 在 `mounted` 时调用 `getUserRepositories`

  return {
    repositories,
    getUserRepositories
  }
}

watch 响应式更改

就像我们在组件中使用 watch 选项并在 user property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch 函数执行相同的操作。它接受 3 个参数:

  • 一个想要侦听的响应式引用或 getter 函数
  • 一个回调
  • 可选的配置选项
import { ref, watch } from 'vue'

const counter = ref(0)
watch(counter, (newValue, oldValue) => {
  console.log('The new counter value is: ' + counter.value)
})

每当 counter 被修改时,例如 counter.value=5,侦听将触发并执行回调 (第二个参数),在本例中,它将把 'The new counter value is:5' 记录到控制台中。

独立的 computed 属性

与 ref 和 watch 类似,也可以使用从 Vue 导入的 computed 函数在 Vue 组件外部创建计算属性。让我们回到 counter 的例子:

import { ref, computed } from 'vue'

const counter = ref(0)
const twiceTheCounter = computed(() => counter.value * 2)

counter.value++
console.log(counter.value) // 1
console.log(twiceTheCounter.value) // 2

这里我们给 computed 函数传递了第一个参数,它是一个类似 getter 的回调函数,输出的是一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像 ref 一样使用 .value property。

接下来我们来看由Options API 转换成 Composition API的两段代码:

// Options API

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: { 
      type: String,
      required: true
    }
  },
  data () {
    return {
      repositories: [], // 1
      filters: { ... }, // 3
      searchQuery: '' // 2
    }
  },
  computed: {
    filteredRepositories () { ... }, // 3
    repositoriesMatchingSearchQuery () { ... }, // 2
  },
  watch: {
    user: 'getUserRepositories' // 1
  },
  methods: {
    getUserRepositories () {
      // 使用 `this.user` 获取用户仓库
    }, // 1
    updateFilters () { ... }, // 3
  },
  mounted () {
    this.getUserRepositories() // 1
  }
}
// src/composables/useRepositoryNameSearch.js

import { ref, computed } from 'vue'

export default function useRepositoryNameSearch(repositories) {
  const searchQuery = ref('')
  const repositoriesMatchingSearchQuery = computed(() => {
    return repositories.value.filter(repository => {
      return repository.name.includes(searchQuery.value)
    })
  })

  return {
    searchQuery,
    repositoriesMatchingSearchQuery
  }
}


// Composition API
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'

export default {
  components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },
  props: {
    user: {
      type: String,
      required: true
    }
  },
  setup(props) {
    const { user } = toRefs(props)

    const { repositories, getUserRepositories } = useUserRepositories(user)

    const {
      searchQuery,
      repositoriesMatchingSearchQuery
    } = useRepositoryNameSearch(repositories)

    const {
      filters,
      updateFilters,
      filteredRepositories
    } = useRepositoryFilters(repositoriesMatchingSearchQuery)

    return {
      // 因为我们并不关心未经过滤的仓库
      // 我们可以在 `repositories` 名称下暴露过滤后的结果
      repositories: filteredRepositories,
      getUserRepositories,
      searchQuery,
      filters,
      updateFilters
    }
  }
}

setup

使用 setup 函数时,它将接收两个参数:
1、props
2、context

Props

setup 函数中的第一个参数是 props。正如在一个标准组件中所期望的那样,setup 函数中的 props 是响应式的,当传入新的 prop 时,它将被更新。

// MyBook.vue

export default {
  props: {
    title: String
  },
  setup(props) {
    console.log(props.title)
  }
}

但是,因为 props 是响应式的,你不能使用 ES6 解构,它会消除 prop 的响应性。
如果需要解构 prop,可以在 setup 函数中使用 toRefs 函数来完成此操作:

// MyBook.vue

import { toRefs } from 'vue'

setup(props) {
  const { title } = toRefs(props)

  console.log(title.value)
}

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

// MyBook.vue
import { toRef } from 'vue'
setup(props) {
  const title = toRef(props, 'title')
  console.log(title.value)
}

Context

传递给 setup 函数的第二个参数是 context。context 是一个普通的 JavaScript 对象,它暴露组件的三个 property:

// MyBook.vue

export default {
  setup(props, context) {
    // Attribute (非响应式对象)
    console.log(context.attrs)

    // 插槽 (非响应式对象)
    console.log(context.slots)

    // 触发事件 (方法)
    console.log(context.emit)
  }
}

context 是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context 使用 ES6 解构。

// MyBook.vue
export default {
  setup(props, { attrs, slots, emit }) {
    ...
  }
}

WatchEffect

该方法接收一个函数并且立即执行,并当该函数里的变量变更时,重新执行该函数。该方法无法获取到原值,只能是改变之后的值。

watchEffect(() => {
	console.log(nameObj.name) 
})

取消监听

const stop = watchEffect(() => {
	console.log(nameObj.name) 
    setTimeout(() => {
    	stop()
    }, 5000)
})

总结一下 watch 和 watchEffect 的区别

watch:
1.具有一定的惰性lazy 第一次页面展示的时候不会执行,只有数据变化的时候才会执行
2.参数可以拿到当前值和原始值
3.可以侦听多个数据的变化,用一个侦听起承载

watchEffect:
1.立即执行,没有惰性,页面的首次加载就会执行。
2.不能获取之前数据的值 只能获取当前值

有关响应性API的可以查看 官方文档响应性API