Vue 3.x 笔记

简介

目前为止,Vue 3.x正式版还没有发布。对应的文档已经有了。官方文档地址:Vue.js。中文版地址:Vue.js中文版

看了下官方文档,整理了些Vue 3.x的新特性。与Vue 2.x写法做了些对比

目前对应的脚手架还没有出。
这里直接克隆一个项目vue-next-webpack-preview练习。
该项目提供了一个包括Vue 3在内的最小的Webpack设置。

创建新App

Vue 2.x是使用new Vue()Vue 3.x需要使用新的 createApp()方法创建一个Vue对象的实例。

在旧的API下,我们添加的任何全局配置(pluginsmixins,原型属性等)都将永久更改全局状态。
在新的API下,调用 createApp 将返回一个新的app实例,该实例不会被应用于其他实例的任何全局配置污染。

// vue 2.x import Vue from 'vue' import App from './App.vue' new Vue({ render: h => h(App) }).$mount('#app') // vue 3.x import { createApp } from 'vue' import App from './App.vue' createApp(App).mount('#app')
  • Vue 3.x全局的方法是挂载到新的app实例上面。这样就不会被其他实例的全局方法污染
// vue 2.x import Vue from 'vue' import App from './App.vue' Vue.config.ignoredElements = [/^app-/] Vue.use(/* ... */) Vue.mixin(/* ... */) Vue.component(/* ... */) Vue.directive(/* ... */) Vue.prototype.customProperty = () => {} new Vue({ render: h => h(App) }).$mount('#app') // vue3.x import { createApp } from 'vue' import App from './App.vue' const app = createApp(App) app.config.isCustomElement = tag => tag.startsWith('app-') app.use(/* ... */) app.mixin(/* ... */) app.component(/* ... */) app.directive(/* ... */) app.config.globalProperties.customProperty = () => {} app.mount('#app')
2.x全局API 3.x实例API(app)
Vue.config app.config
Vue.config.productionTip removed
Vue.config.ignoreElements app.config.isCustomElement
Vue.component app.component
Vue.directive app.directive
Vue.mixin app.mixin
Vue.use app.use
Vue.prototype app.config.globalProperties

Composition API

这是 Vue 3.x 的一个核心变更了。
除了改了我们定义状态的书写方式外,也为我们提供体验更棒的逻辑复用和代码组织,新的方式可以让你把同一个业务逻辑的代码(状态,计算属性,方法等)都放到一块。

允许使用setup功能来定义组件功能。

setup是在组件创建之前调用的,setup执行时,还没有组件的this

setup的参数有两个propscontext
context可以访问的属性:propsattrsslotsemit

优点:

  • 可与现有选项 API 一起使用
  • 灵活的逻辑组成和重用
  • Reactivity模块可以作为独立的库使用

备注Composition API并不是更改,因为它纯粹是可选的。主要动机是允许更好的代码组织和组件之间的代码重用(因为mixin本质上是一种反模式)。

state

使用refreactive来生成响应式变量,返回一个带有value属性的对象。
setup中的响应式变量值都在value属性中。

  • ref:返回一个带有value属性的对象,也是响应式的
  • reactive:通过proxy返回一个对象的拷贝。是响应式的。修改该对象不会影响到原对象
// 原写法 export default { data() { return { count: 0, users: [], list: [1, 2, 3], book: { title: 'Vuejs' } } }, created() { console.log(this.count) console.log(this.users) console.log(this.list) console.log(this.book) } } // setup import { ref, reactive } from 'vue' export default { setup() { const counter = ref(0) const users = ref([]) const list = reactive([1, 2, 3]) const book = reactive({ title: 'Vuejs' }) console.log(counter.value) console.log(users.value) console.log(list) console.log(book) return { // 模板或者其他需要用到,就在setup中 return 出去 users, list } } }

props

使用toRefsprops转成响应式变量

// 原写法 export default { // props: ['user'] // props: { // user: Object // } props: { user: { type: Object, default: () => ({}) } } } // setup中如果要解构props(比如用来计算某个计算属性),可以使用toRefs(props) import { toRefs } from 'vue' export default { props: { user: { type: Object, default: () => ({}) } }, setup(props) { const { user } = toRefs(props) console.log(user.value) } }

computed

setup中使用computed方法生成一个计算属性。

// 原写法 export default { data() { return { counter: 0, c: 1 } }, computed: { twiceTheCounter() { return this.counter * 2 }, plusOne: { get: function() { return this.c + 1 }, set: function(val) { this.c = val - 1 } } } } // setup import { ref, computed } from 'vue' export default { setup() { const counter = ref(0) const twiceTheCounter = computed(() => counter.value * 2) // 可以使用get set const c = ref(1) const plusOne = computed({ get: () => c.value + 1, set: val => { c.value = val - 1 } }) } }

watch

watchEffect立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数

watchEffect 在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止

在一些情况下,也可以显式调用返回值以停止侦听

const count = ref(0) const stop = watchEffect(() => console.log(count.value)) // -> logs 0 setTimeout(() => { count.value++ // -> logs 1 }, 100) // later 也可以显式调用返回值以停止侦听 stop()

setup中使用watch方法监听响应式变量。

watch方法接收三个参数:

  • 要监听的响应式变量或者计算属性
  • 回调方法
  • options
// 原写法 export default { data() { return { counter: 0, books: [ { title: 'Vuejs' } ] } }, watch: { counter(newVal, oldVal) { console.log(newVal) console.log(counter.value) }, books: { handler: function(newList, oldList) { console.log(newList) }, deep: true } } } // setup import { ref, watch } from 'vue' export default { setup() { const counter = ref(0) watch(counter, (newVal, oldVal) => { console.log(newVal) console.log(counter.value) }) watch(books, (newList, oldList) => { console.log(newList[0].title) console.log(books.value[0].title) }, { deep: true }) } }

watchEffect 比较,watch 允许我们

  • 懒执行副作用;
  • 更具体地说明什么状态应该触发侦听器重新运行;
  • 访问侦听状态变化前后的值

生命周期

setup中使用生命周期,是在前面添加on
beforeCreatecreated生命周期的代码,可以直接写在setup方法中。
Vue2.x中的beforeDestorydestoryed,在Vue3.x中已经修改为beforeUnmountunmounted

组件中生命周期 setup中生命周期
beforeCreate
created
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeUnmount onBeforeUnmount
unmounted onUnmounted
errorCaptured onErrorCaptured
renderTracked onRenderTracked
renderTtiggered onRenderTriggered
// 原写法 export default { mounted() { console.log('mounted') }, destoryed() { // 注意:Vue3.x中已经改为 unmounted console.log('destoryed') } } // setup import { onMounted, onUnmounted } from 'vue' export default { setup() { onMounted(() => { console.log('mounted') }) onUnmounted(() => { console.log('unmounted') }) } }

methods

直接在setup中定义即可,外部需要使用的话,return出去

// 原方法 export default { methods: { demoFun() { // ... } } } // setup export default { setup() { const demoFun = () => { // ... } return { demoFun } } }

Template Refs

Template refsreactive refs是统一的。

可以在setup中像平常一样声明一个ref,然后在setup()中返回

<template> <div ref="root">This is a root element</div> </template> <script> import { ref, onMounted } from 'vue' export default { setup() { // 这里的 root, 要跟模板中的(ref="root")一样 const root = ref(null) onMounted(() => { // DOM元素会在渲染完成后赋值 console.log(root.value) }) return { // 要返回出去才可以 root } } } </script>

v-for中的Ref数组

Vue 2.x中,在v-for里面使用ref会用ref数组填充相应的$refs

<template> <div v-for="item in list" :ref="itemRefs"></div> </template> <script> export default { mounted() { console.log(this.$refs.itemRefs) } } </script>

Vue3.x中,将不再自动创建数组,要从单个绑定获取多个ref。需要将ref绑定到一个更灵活的函数上

<template> <div v-for="item in list" :ref="setItemRef"></div> </template> <!-- 普通写法 --> <script> export default { data() { return { itemRefs: [] } }, methods: { setItemRef(el) { this.itemRefs.push(el) } }, beforeUpdate() { this.itemRefs = [] }, updated() { console.log(this.itemRefs) } } </script> <!-- 组合式API写法 --> <script> import { ref, onBeforeUpdate, onUpdated } from 'vue' export default { setup() { // 如果需要,itemRefs 也可以是响应式的且可以被监听 let itemRefs = [] const setItemRef = el => { itemRefs.push(el) } onBeforeUpdate(() => { itemRefs = [] }) onUpdated(() => { console.log(itemRefs) }) return { itemRefs, setItemRef } } } </script>

代码分离

Vue 2.x中,页面的某个功能会分散在datacomputed生命周期methodswatch等地方。代码比较分散。功能较多时比较混乱,不利于维护

Composition API可以将相同功能的代码集中管理。甚至可以分离到不同的js文件中

  • src/composables/useUserRepositories.js
import { fetchUserRepositories } from '@/api/repositories' import { ref, onMounted, watch } from 'vue' export default function useUserRepositories(user) { const repositories = ref([]) const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(user.value) } onMounted(getUserRepositories) watch(user, getUserRepositories) return { repositories, getUserRepositories } }
  • 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 } }
  • src/components/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories' import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch' import { toRefs } from 'vue' export default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String } }, setup (props) { const { user } = toRefs(props) const { repositories, getUserRepositories } = useUserRepositories(user) const { searchQuery, repositoriesMatchingSearchQuery } = useRepositoryNameSearch(repositories) return { // Since we don’t really care about the unfiltered repositories // we can expose the filtered results under the `repositories` name repositories: repositoriesMatchingSearchQuery, getUserRepositories, searchQuery, } }, data () { return { filters: { ... }, // 3 } }, computed: { filteredRepositories () { ... }, // 3 }, methods: { updateFilters () { ... }, // 3 } }

Provide / Inject

setup中使用provideinject

  • 原写法如下:
<!-- src/components/MyMap.vue --> <template> <MyMarker /> </template> <script> import MyMarker from './MyMarker.vue' export default { components: { MyMarker }, provide: { location: 'North Pole', geolocation: { longitude: 90, latitude: 135 } } } </script>
<!-- src/components/MyMarker.vue --> <script> export default { inject: ['location', 'longitude', 'latitude'] } </script>
  • setup写法
<!-- src/components/MyMap.vue --> <template> <MyMarker /> </template> <script> import { provide, reactive, ref, readonly } from 'vue' import MyMarker from './MyMarker.vue' export default { components: { MyMarker }, setup() { // 不会变化 // provide('location', 'North Pole') // provide('geolocation', { // longitude: 90, // latitude: 135 // }) // 改成响应式写法 const location = ref('North Pole') const geolocation = reactive({ longitude: 90, latitude: 135 }) // 写一个方法更新location const updateLocation = () => { location.value = 'South Pole' } // provide('location', location) // provide('geolocation', geolocation) // 可以将 location 和 geolocation 设置成只读的 provide('location', readonly(location)) provide('geolocation', readonly(geolocation)) provide('updateLocation', updateLocation) } } </script>
<!-- src/components/MyMarker.vue --> <script> import { inject } from 'vue' export default { setup() { const userLocation = inject('location', 'The Universe') const userGeolocation = inject('geolocation') const updateUserLocation = inject('updateLocation') return { userLocation, userGeolocation, updateUserLocation } } } </script>

CSS Module

CSS Module相关使用参考:Vue项目常见问题 - 使用CSS Module

Composition API中使用CSS Module可以通过useCssModule

import { useCssModule } from 'vue' export default { // ... setup() { const style = useCssModule() console.log(style.home) return { // 这里也可以输出出去,template中就可以通过 style.xxx 使用了 // 没有下面这句的话,template中是通过 $style.xxx 使用的 style } }, // ... }

组件

Fragment

Vue 3.x 后组件不再限制 template 中根元素的个数(旧的版本之前是只能有一个根元素)。

<template> 中不再局限于单一的根节点

render 函数也可以返回数组

<template> <div>.....</div> <p>.....</p> <div>.....</div> </template>

Teleport

单页面应用,都是将DOM挂载到某个节点上面的。

如果需要在那个节点外面挂载一些DOMVue 2.x的做法都是直接操作DOM
Vue的原则就是尽量不让开发者直接操作DOM,这些事都统一由 vue 来完成

Vue 3.x提供了一个组件Teleport
该组件只是单纯的把定义在其内部的内容转移到目标元素中,在元素结构上不会产生多余的元素,当然也不会影响到组件树。

组件Teleport有个to属性。该属性是要挂载到的节点(为该属性分配一个查询选择器,以标识目标元素),这个节点需要在模板文件index.html中存在

<teleport to="body"> <div v-if="modalOpen" class="modal"> <div> I'm a teleported modal! (My parent is "body") <button @click="modalOpen = false">Close</button> </div> </div> </teleport>

比如我们要将一个组件挂载idendofbody的节点下面

  • 修改public/index.html,添加目标节点
<body> <!-- ... --> <div id="app"></div> <div id="endofbody"></div> <!-- built files will be auto injected --> <!-- ... --> </body>
  • 代码中添加Teleport
<teleport to="#endofbody"> <child-component name="John" /> </teleport>

函数组件

// vue 2.x const FunctionalComp = { functional: true, render(h) { return h('div', `Hello! ${props.name}`) } } // vue 3.x import { h } from 'vue' const FunctionalComp = (props, { slots, attrs, emit }) => { return h('div', `Hello! ${props.name}`) }

异步组件

用于异步加载的组件

// 原写法 const asyncComp = () => import('./Foo.vue') // or const asyncPage = { component: () => import('./Foo.vue'), delay: 200, timeout: 3000, error: ErrorComponent, loading: LoadingComponent } // 新写法 import { defineAsyncComponent } from 'vue' const asyncComp = defineAsyncComponent(() => import('./Foo.vue')) // or 注意是loader 不是 component const asyncPage = defineAsyncComponent({ loader: () => import('./Foo.vue'), delay: 200, timeout: 3000, error: ErrorComponent, loading: LoadingComponent })

Suspense

加载异步组件,在异步组件加载完成成并完全渲染之前 suspense 会先显示 #fallback 插槽的内容 。

#fallback 其实是插件 v-solt 的简写,而第一个 template 没有给,则为默认插槽。

<Suspense> <template> <Suspended-component /> </template> <template #fallback> Loading... </template> </Suspense>

过滤器

vue 3.x中过滤器filters已删除,不再受支持。

使用方法调用或者计算属性代替

全局过滤器可以通过全局属性在所有组件中使用
注意:这种方式只能用于方法中,不可以在计算属性中使用,因为后者只有在单个组件的上下文中定义时才有意义。

// main.js const app = createApp(app) app.config.globalProperties.$filters = { currencyUSD(value) { return '$' + value } }
<!-- 组件中使用 --> <p>{{ $filters.currencyUSD(accountBalance) }}</p>

自定义指令

自定义指令的生命周期修改了,沿用了vue组件的那一套

// vue 2.x const MyDirective = { bind(el, binding, vnode, prevVnode) {}, inserted() {}, update() {}, componentUpdated() {}, unbind() {} } // vue 3.x const MyDirective = { beforeMount(el, binding, vnode, prevVnode) {}, mounted() {}, beforeUpdate() {}, updated() {}, beforeUnmount() {}, // new unmounted() {} }
Vue2.x Vue 3.x
bind beforeMount
inserted mounted
beforeUpdate 新增
update 移除
componentUpdated updated
beforeUnmount 新增
unbind unmounted

$emit

$emit()方法在用法上没变,但需要额外多定义emits

emits可以是字符串数组或者对象

// 原写法 export default { methods: { handleTest() { this.$emit('check') } } } // emits 字符串数组 export default { emits: ['check'], // 放在methods中的写法 // methods: { // handleTest() { // this.$emit('check') // } // }, // 放在setup中的写法 setup(props, context) { const handleTest = () => { context.emit('check') } return { handleTest } }, methods: { // 这么使用。在setup里面的话,用context.emit onSubmit() { this.$emit('submit', { email: 'foo@bar.com', password: 123 }) this.$emit('non-declared-event') } } } // emits 对象 export default { emits: { // 没有参数 cancel: null, // 有参数 submit: payload => { // perform runtime validation // if (payload.email && payload.password) { // return true // } else { // console.warn(`Invalid submit event payload!`) // return false // } } // 这是ts写法 // submit: (payload: { email: string; password: string }) => {} } }

.sync

vue 3.0又去掉了.sync,合并到了v-model里,而v-model的内部实现也有了小调整

  • vue 2.x写法 – .sync
<!-- 父组件 --> <template> <div> <button @click="handleChange">{{ model.show ? '隐藏' : '显示' }}</button> <com-model :show.sync="model.show" :content="model.content"></com-model> </div> </template> <script> import comModel from './model.vue' export default { data() { return { model: { show: false, content: 'this is content' } } }, methods: { handleChange() { this.model.show = !this.model.show } }, components: { comModel } } </script>
<!-- 子组件 --> <template> <div v-if="show"> <p>这是一个弹窗 {{ content }}</p> <button @click="visible=false">关闭</button> </div> </template> <script> export default { props: { show: { type: Boolean, default: false }, content: { type: String, default: '' } } computed: { visible: { get() { return this.show }, set(val) { this.$emit('update:show', val) } } } } </script>
  • vue 3.x写法 – v-model
<!-- 父组件 --> <template> <button @click="handleChange">{{ model.show ? '隐藏' : '显示' }}</button> <com-model v-model:show="model.show" :content="model.content"></com-model> </template> <script> import { reactive } from 'vue' import comModel from './model.vue' export default { setup() { const model = reactive({ show: false, content: 'this is content' }) const handleChange = () => { model.show = !model.show } return { model, handleChange } }, components: { comModel } } </script>
<!-- 子组件 --> <template> <div v-if="show"> <p>这是一个弹窗 {{ content }}</p> <button @click="handleClose">关闭</button> </div> </template> <script> export default { props: { show: { type: Boolean, default: false }, content: { type: String, default: '' } }, setup(props, context) { const handleClose = () => { context.emit('update:show', false) } return { handleClose } } } </script>

v-model

<!-- 元素:原来的方式保留 --> <input v-model="xxx"> <!-- would be shorthand for: --> <input :model-value="xxx" @update:model-value="newValue => { xxx = newValue }"> <!-- 组件 --> <MyComponent v-model:aaa="xxx"/> <!-- would be shorthand for: --> <MyComponent :aaa="xxx" @update:aaa="newValue => { xxx = newValue }"/> <!-- 可以绑定多个 v-model --> <InviteeForm v-model:name="inviteeName" v-model:email="inviteeEmail"/> <!-- 额外处理:可以给这个属性添加额外的处理 --> <Comp v-model:foo.trim="text" v-model:bar.number="number" />

插槽

要向具名插槽提供内容,需要在<template>上使用指令v-slot,缩写#

<base-layout> <template v-slot:header> <h1>Here might be a page title</h1> </template> <template v-slot:default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <!-- 默认插槽可以省略掉 template 标签 --> <template v-slot:footer> <p>Here's some contact info</p> </template> </base-layout> <!-- 缩写 --> <base-layout> <template #header> <h1>Here might be a page title</h1> </template> <p>A paragraph for the main content.</p> <p>And another one.</p> <template #footer> <p>Here's some contact info</p> </template> </base-layout>

有时让插槽内容能够访问子组件中才有的数据是很有用的。
为了让 user 在父级的插槽内容中可用,我们可以将 user 作为 <slot> 元素的一个 attribute 绑定上去
绑定在 <slot> 元素上的 attribute 被称为插槽 prop

<!-- 子组件 --> <span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> <!-- 父组件 --> <current-user> <template v-slot:default="slotProps"> {{ slotProps.user.firstName }} </template> </current-user> <!-- 默认插槽也可以省略插槽名称default --> <current-user> <template v-slot="slotProps"> {{ slotProps.user.firstName }} </template> </current-user>

Vue 3.x中,子组件中定义插槽内的样式可以使用::v-slotted()
允许你在提供插槽的组件中使用范围化规则来针对插槽内容。
具体可参考:Scoped Styles RFC

<style scoped> ::v-slotted(p) { font-style: italic; } </style>

TypeScript

Vue 3.x提供了更好的TypeScript支持。等出来再搞吧

其他

Tree-shaking

vue 3 中不会把所有的 api 都打包进来,只会 打包你用到的 api
我们在项目中用什么什么,就只会打包什么,不会像 vue 2.x 那样全部 api 都打包。

// vue 2.x import Vue from 'vue' Vue.nextTick(() => {}) const obj = Vue.observable({}) // vue 3.x import Vue, { nextTick, observable } from 'vue' console.log(Vue.nextTick) // undefined nextTick(() => {}) const obj = observable({})

动态参数

指令名,事件名,插槽名,都可以使用变量来定义了

<!-- v-bind with dynamic key --> <div v-bind:[key]="value"></div> <!-- v-bind shorthand with dynamic key --> <div :[key]="value"></div> <!-- v-on with dynamic event --> <div v-on:[event]="handler"></div> <!-- v-on shorthand with dynamic event --> <div @[event]="handler"></div> <!-- v-slot with dynamic name --> <foo> <template v-slot:[name]> Hello </template> </foo> <!-- v-slot shorthand with dynamic name --> <foo> <template #[name]> Default slot </template> </foo>

创作不易,若本文对你有帮助,欢迎打赏支持作者!

 分享给好友: