简介
目前为止,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
下,我们添加的任何全局配置(plugins
,mixins
,原型属性等)都将永久更改全局状态。
在新的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
的参数有两个props
和context
context
可以访问的属性:props
、attrs
、slots
、emit
优点:
- 可与现有选项
API
一起使用- 灵活的逻辑组成和重用
Reactivity
模块可以作为独立的库使用
备注:Composition API
并不是更改,因为它纯粹是可选的。主要动机是允许更好的代码组织和组件之间的代码重用(因为mixin
本质上是一种反模式)。
state
使用ref
、reactive
来生成响应式变量,返回一个带有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
使用toRefs
将props
转成响应式变量
// 原写法
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
。
beforeCreate
和created
生命周期的代码,可以直接写在setup
方法中。
Vue2.x
中的beforeDestory
和destoryed
,在Vue3.x
中已经修改为beforeUnmount
和unmounted
组件中生命周期 | 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 refs
和 reactive 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
中,页面的某个功能会分散在data
、computed
、生命周期
、methods
、watch
等地方。代码比较分散。功能较多时比较混乱,不利于维护
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
中使用provide
、inject
- 原写法如下:
<!-- 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
挂载到某个节点上面的。
如果需要在那个节点外面挂载一些DOM
,Vue 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>
比如我们要将一个组件挂载id
为endofbody
的节点下面
- 修改
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>
发表评论