其他API
前面我们已经学习了 vue3 的一些基础知识,本篇将继续讲解一些常用的其他api
,以及较完整的分析vue2 和 vue3 的改变。
浅层响应式数据
shallowRef
shallow 中文:"浅层的"
shallowRef:浅的 ref()。
先用 ref 写个例子:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>a: {{ a }}</p>
<p>o: {{ o }}</p>
<p><button @click="change1">change1</button></p>
<p><button @click="change2">change2</button></p>
<p><button @click="change3">change3</button></p>
<p><button @click="change4">change4</button></p>
</template>
<script lang="ts" setup name="App">
import {ref, shallowRef} from 'vue'
let a = ref(0)
let o = ref({
name: 'p',
age: 18
})
function change1 (){
a.value = 1
}
function change2 (){
o.value.name = 'p2'
}
function change3 (){
o.value.age = 19
}
function change4 (){
o.value = {name: 'p3', age: 20}
}
</script>
这4个按钮都会触发页面数据的变化。
现在将 ref 改成 shallowRef
,其他都不变。你会发现只有 change1 和 change4 能触发页面数据的变化:
typescript
<!-- ChildA.vue -->
<template>
// 不变
</template>
<script lang="ts" setup name="App">
import {ref, shallowRef} from 'vue'
let a = shallowRef(0)
let o = shallowRef({
name: 'p',
age: 18
})
function change1 (){
a.value = 1
}
function change2 (){
o.value.name = 'p2'
}
function change3 (){
o.value.age = 19
}
function change4 (){
o.value = {name: 'p3', age: 20}
}
</script>
这是因为 change1 中的 a.value
是浅层,而 change2 中的 o.value.name
是深层。
对于大型数据结构,如果只关心整体是否被替换,就可以使用 shallowRef,避免使用 ref 将大型数据结构所有层级都转成响应式,这对底层是很大的开销。
shallowReactive
知晓了 shallowRef,shallowReactive也类似。
shallowReactive:浅的 reactive()。
请看示例:
现在3个按钮都能修改页面数据:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>o: {{ o }}</p>
<p><button @click="change2">change2</button></p>
<p><button @click="change3">change3</button></p>
<p><button @click="change4">change4</button></p>
</template>
<script lang="ts" setup name="App">
import {reactive} from 'vue'
let o = reactive({
name: 'p',
options: {
age: 18,
}
})
function change2 (){
o.name = 'p2'
}
function change3 (){
o.options.age = 19
}
function change4 (){
o = Object.assign(o, {name: 'p3', options: {age: 20}})
}
</script>
将 reactive 改为 shallowReactive:
typescript
import {shallowReactive} from 'vue'
let o = shallowReactive({
name: 'p',
options: {
age: 18,
}
})
现在只有 change2 和 change4 能修改页面数据,因为 change3 是多层的,所以失效。
只读数据
readonly
readonly : Takes an object (reactive or plain) or a ref and returns a readonly proxy to the original.
readonly 能传入响应式数据,并返回一个只读代理
请看示例:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>name: {{ name }}</p>
<p><button @click="change1">change name</button></p>
<p>copyName: {{ copyName }}</p>
<p><button @click="change2">change copyName</button></p>
</template>
<script lang="ts" setup name="App">
import {ref, readonly} from 'vue'
let name = ref('p')
// 传入一个响应式的数据,返回一个只读代理
// reactive 数据也可以
// name 数据的修改,也会同步到 copyName
let copyName = readonly(name)
// 类型"number"的参数不能赋给类型"object"的参数。ts
// let copyName = readonly(2)
function change1(){
name.value = 'p2'
}
function change2(){
// 通过代理修改数据
// vscode 报错:无法为"value"赋值,因为它是只读属性。ts
copyName.value = 'p3'
}
</script>
浏览器呈现:
typescript
# 组件A
name: p2
// 按钮1
change name
copyName: p2
// 按钮2
change copyName
点击第一个按钮,发现 copyName 的值也跟着变化了(说明不是一锤子买卖),但是点击第二个按钮,页面数据不会变化。浏览器控制台也会警告:
javascript
[Vue warn] Set operation on key "value" failed: target is readonly. RefImpl {__v_isShallow: false, dep: Map(1), __v_isRef: true, _rawValue: 'p2', _value: 'p2'}
readonly 只读代理是深的:任何嵌套的属性访问也将是只读的。对比 shallowReadonly 就知道了。
Tip:使用场景,比如同事A定义了一个很重要的数据,同事B需要读取该数据,但又担心误操作修改了该数据,就可以通过 readonly 包含数据。
shallowReadonly
readonly 只读代理是深层的,而 shallowReadonly 是浅层的。也就是深层的 shallowReadonly 数据不是只读的。
请看示例:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>obj: {{ obj }}</p>
<p><button @click="change1">change1</button></p>
<p><button @click="change2">change2</button></p>
</template>
<script lang="ts" setup name="App">
import {ref, reactive, shallowReadonly} from 'vue'
let obj = reactive({
name: 'p',
options: {
age: 18,
}
})
let copyObj = shallowReadonly(obj)
function change1(){
// vscode 会提示:无法为"name"赋值,因为它是只读属性。ts
copyObj.name = 'p2'
}
function change2(){
copyObj.options.age = 19
}
</script>
通过 shallowReadonly 创建一个备份数据,点击第一个按钮没反应,点击第二个按钮,页面变成:
javascript
# 组件A
obj: { "name": "p", "options": { "age": 19 } }
shallowReadonly 只处理浅层次的只读。深层次的不管,也就是可以修改。
疑惑
:笔者的开发者工具中, copyObj -> options 中的 age 属性没有表示能修改的铅笔图标。应该要有,这样就能保持和代码一致
原始数据
toRaw
toRaw() can return the original object from proxies created by reactive(), readonly(), shallowReactive() or shallowReadonly().
用于获取一个响应式对象的原始对象。修改原始对象,不会在触发视图。
javascript
const foo = {}
const reactiveFoo = reactive(foo)
console.log(toRaw(reactiveFoo) === foo) // true
比如这个使用场景:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>obj: {{ obj }}</p>
<p><button @click="handle1(toRaw(obj))">处理数据</button></p>
</template>
<script lang="ts" setup name="App">
import {reactive, toRaw} from 'vue'
let obj = reactive({
name: 'p',
age: 18,
})
// 不用担心修改了数据从而影响到使用 obj 的地方
function handle1(o: any){
// 修改数据
o.age += 1
// o: {name: 'p', age: 19}
console.log('o: ', o)
// 例如发送请求
}
</script>
markRaw
Marks an object so that it will never be converted to a proxy. Returns the object itself.
标记一个对象
,使其永远不会被转换为proxy。返回对象本身。
- 有些值不应该是响应式的,例如一个复杂的第三方类实例,或者一个Vue组件对象。
typescript
import {reactive} from 'vue'
let o = {
getAge() {
console.log(18)
}
}
// Proxy(Object) {getAge: ƒ}
let o2 = reactive(o)
- 当使用不可变数据源呈现大型列表时,跳过代理转换可以提高性能。
请问输出什么:
typescript
import {reactive} from 'vue'
let o = {
name: 'p',
age: 18,
}
let o2 = reactive(o)
console.log(o);
console.log(o2);
答案是:
javascript
{name: 'p', age: 18}
Proxy(Object) {name: 'p', age: 18}
通过 reactive 会将数据转为响应式。
请看 markRaw 示例:
typescript
import {reactive, markRaw} from 'vue'
// 标记 o 不能被转成响应式
let o = markRaw({
getAge() {
console.log(18)
}
})
let o2 = reactive(o)
// {__v_skip: true, getAge: ƒ}
console.log(o2);
比如中国的城市,数据是固定不变的,我不做成响应式的,别人也不许做成响应式的。我可以这么写:
typescript
// 中国就这些地方,不会变。我自己不做成响应式的,别人也不许做成响应式的
let citys = markRow([
{name: '北京'},
{name: '上海'},
{name: '深圳'},
...
])
customRef
自定义 ref 可用于解决内置 ref 不能解决的问题。
ref 用于创建响应式数据,数据一变,视图也会立刻更新。比如要1秒后更新视图,这个 ref 办不到。
先用ref写个例子:input 输入字符,msg 立刻更新:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>msg: {{ msg }}</p>
<input v-model="msg"/>
</template>
<script lang="ts" setup name="App">
import {ref} from 'vue'
let msg = ref('')
</script>
现在要求:input输入字符后,等待1秒msg才更新。
我们可以用 customRef
解决这个问题。
实现如下:
javascript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>msg: {{ msg }}</p>
<input v-model="msg"/>
</template>
<script lang="ts" setup name="App">
import {ref, customRef, } from 'vue'
let initValue = ''
// customRef 传入函数,里面又两个参数
let msg = customRef((track, trigger) => {
return {
get() {
// 告诉 vue 这个数据很重要,要持续关注,数据一旦变化,更新视图
track()
return initValue
},
set(newValue) {
setTimeout(() => {
initValue = newValue
// 告诉vue我更新数据了,你更新视图去吧
trigger()
}, 1000)
}
}
})
</script>
customRef() 接收一个工厂函数作为参数,这个工厂函数接受 track 和 trigger 两个函数作为参数,并返回一个带有 get 和 set 方法的对象。
track()
和 trigger()
缺一不可,需配合使用:
- 缺少 track,即使通知vue 更新了数据,但不会更新视图
- 缺少 trigger,track 则一直在等着数据变,快变,我要更新视图。但最终没人通知它数据变了
实际工作会将上述功能封装成一个 hooks
。使用起来非常方便。就像这样:
typescript
// hooks/useMsg.ts
import { customRef, } from 'vue'
export function useMsg(value: string, delay = 1000) {
// customRef 传入函数,里面又两个参数
let msg = customRef((track, trigger) => {
// 防抖
let timeout: number
return {
get() {
// 告诉 vue 这个数据很重要,要持续关注,数据一旦变化,更新视图
track()
return value
},
set(newValue) {
clearTimeout(timeout)
timeout = setTimeout(() => {
value = newValue
// 告诉vue我更新数据了,你更新视图去吧
trigger()
}, delay)
}
}
})
return msg
}
使用起来和 ref 一样方便。就像这样:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>msg: {{ msg }}</p>
<input v-model="msg"/>
</template>
<script lang="ts" setup name="App">
import {useMsg} from '@/hooks/useMsg'
let msg = useMsg('hello', 1000)
</script>
Teleport
Teleport 中文"传送"
Teleport 将其插槽内容渲染到 DOM 中的另一个位置。
比如 box 内的内容现在在 box 元素中:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<div class="box">
<p>我是组件A内的弹框</p>
</div>
</template>
我可以利用 Teleport 新增组件将其移到body下面。
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p><button @click="handle1">change msg</button></p>
<div class="box">
<Teleport to="body">
<p>{{ msg }}</p>
</Teleport>
</div>
</template>
<script lang="ts" setup name="App">
import {ref} from 'vue'
let msg = ref('我是组件A内的弹框')
function handle1(){
msg.value += '~'
}
</script>
现在这段ui内容就移到了 body 下,并且数据链还是之前的,也就是 msg 仍受 button 控制。
Tip:to 必填,语法是选择器或实际元素
javascript
<Teleport to="#some-id" />
<Teleport to=".some-class" />
<Teleport to="[data-teleport]" />
Suspense
suspense 官网说是一个实验性功能。用来在组件树中协调对异步依赖的处理。
我们首先在子组件中异步请求,请看示例:
typescript
<!-- Father.vue -->
<template>
<p># 父亲</p>
<hr>
<ChildA/>
</template>
<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
</script>
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
</template>
<script lang="ts" setup name="App">
import axios from 'axios';
// https://api.uomg.com/ 免费的 API 接口服务
let {data} = await axios.get('https://api.uomg.com/api/rand.music?sort=热歌榜&format=json')
console.log('data: ', data);
</script>
Tip:我们现在用了 setup 语法糖,没有机会写 async,之所以能这么写,是因为底层帮我们做了。
浏览器查看,发现子组件没有渲染出来。控制台输出:
javascript
// main.ts:14 [Vue 警告]: 组件 <App>: setup 函数返回了一个 Promise,但在父组件树中未找到 <Suspense> 边界。带有异步 setup() 的组件必须嵌套在 <Suspense> 中才能被渲染。
main.ts:14 [Vue warn]: Component <App>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered.
data: {code: 1, data: {...}}
vue 告诉我们需要使用 Suspense。
假如我们将 await 用 async 方法包裹,子组件能正常显示。
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>data: {{ data }}</p>
</template>
<script lang="ts" setup name="App">
import {ref} from 'vue'
import axios from 'axios';
let data = ref({})
async function handle1(){
// https://api.uomg.com/ 免费的 API 接口服务
// 先安装:npm install axios
let response = await axios.get('https://api.uomg.com/api/rand.music?sort=热歌榜&format=json')
data.value = response.data
console.log('data: ', data);
}
handle1()
</script>
继续讨论异步的 setup()
的解决方案。在父组件中使用 Suspense 组件即可。请看代码:
typescript
<!-- Father.vue -->
<template>
<p># 父亲</p>
<hr>
// <Suspense> 组件有两个插槽:#default 和 #fallback。两个插槽都只允许一个直接子节点。
<Suspense>
<template #fallback>
Loading...
</template>
<ChildA/>
</Suspense>
</template>
<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
</script>
子组件也稍微调整下:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<p>data: {{ data }}</p>
</template>
<script lang="ts" setup name="App">
import axios from 'axios';
// https://api.uomg.com/ 免费的 API 接口服务
let {data} = await axios.get('https://api.uomg.com/api/rand.music?sort=热歌榜&format=json')
console.log('data: ', data);
</script>
利用开发者工具将网速跳到 3G,再次刷新页面,发现先显示Loading...
,然后在显示
javascript
# 组件A
data: { "code": 1, "data": { "name": "阿普的思念", "url": "http://music.163.com/song/media/outer/url?id=2096764279", "picurl": "http://p1.music.126.net/Js1IO7cwfEe6G6yNPyv5FQ==/109951169021986117.jpg", "artistsname": "诺米么Lodmemo" } }
注
:数据是一次性出来的,不是先展示 {}
在展示 {...}
。所以我们再看官网,就能理解下面这段内容:
javascript
<Suspense>
└─ <Dashboard>
├─ <Profile>
│ └─ <FriendStatus>(组件有异步的 setup())
└─ <Content>
├─ <ActivityFeed> (异步组件)
└─ <Stats>(异步组件)
在这个组件树中有多个嵌套组件,要渲染出它们,首先得解析一些异步资源。如果没有 <Suspense>
,则它们每个都需要处理自己的加载、报错和完成状态。在最坏的情况下,我们可能会在页面上看到三个旋转的加载态,在不同的时间显示出内容。
有了 <Suspense>
组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。
Tip: 在 React 中可以使用 Suspense 组件和 React.lazy() 函数来实现组件的延迟加载。就像这样:
javascript
import React, {Suspense} from 'react'
// 有当 OtherComponent 被渲染时,才会动态加载 './math' 组件
const OtherComponent = React.lazy(() => import('./math'))
function TestCompoment(){
return <div>
<Suspense fallback={<div>loading</div>}>
<OtherComponent/>
</Suspense>
</div>
}
全局 api 转移到应用对象
在 Vue 3 中,一些全局 API 被转移到了应用对象(app)中。
app就是这个:
typescript
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
这些 API 以前在 Vue 2 中是全局可用的,但在 Vue 3 中,出于更好的模块化和灵活性考虑,许多 API 被转移到了应用对象中。
app.component
对应 vue2 中 Vue.component,用于注册和获取全局组件。
例如定义一个组件:
typescript
<template>
<p>我的Apple组件</p>
</template>
在 main.ts 中注册:
typescript
import Apple from '@/views/Apple.vue'
app.component('Apple', Apple)
现在在任何地方都能直接使用,例如在 ChildA.vue 中:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<Apple/>
</template>
<script lang="ts" setup name="App">
</script>
app.config
vue2 中有 Vue.prototype. 比如 Vue.prototype.x = 'hello'
,在任意模板中 {{x}}
都会输出 hello
这里有 app.config。
比如在 main.ts 中增加:app.config.globalProperties.x = 'hello'
,在任意组件中就可以获取:
typescript
<template>
<p># 组件A</p>
x: {{ x }}
<Apple/>
</template>
但是 ts 会报错,因为找不到 x。
解决方法在官网中有提供。创建一个 ts:
typescript
// test.ts
// 官网:https://cn.vuejs.org/api/application.html#app-config-globalproperties
// 正常工作。
export {}
declare module 'vue' {
interface ComponentCustomProperties {
x: string,
}
}
然后在 main.ts 中引入:
typescript
import '@/utils/test'
app.config.globalProperties.x = 'hello'
不要随便使用,否则你一下定义100个,以后出问题不好维护。
app.directive
Vue.directive() - 注册或获取全局指令。
我们用函数形式的指令,就像这样:
javascript
// https://v2.cn.vuejs.org/v2/guide/custom-directive.html#函数简写
Vue.directive('color-swatch', function (el, binding) {
el.style.backgroundColor = binding.value
})
比如我写一个这样的指令:
typescript
// main.ts 注册一个全局指令
app.directive('green', (element, {value}, vnode) => {
element.innerText += value
element.style.color = 'green'
})
接着使用指令:
typescript
<!-- ChildA.vue -->
<template>
<p># 组件A</p>
<h4 v-green="msg">你好</h4>
<Apple/>
</template>
<script lang="ts" setup name="App">
import {ref} from 'vue'
let msg = ref('兄弟')
</script>
页面呈现:
javascript
# 组件A
// 绿色文字
你好兄弟
其他
app.mount - 挂载
app.unmount - 卸载
app.use - 安装插件。例如路由、pinia
非兼容性改变
非兼容性改变是Vue 2 迁移
中的一章,列出了 Vue 2 对 Vue 3 的所有非兼容性改变
Tip:强烈建议详细阅读该篇。
全局 API 应用实例
Vue 2.x 有许多全局 API 和配置,它们可以全局改变 Vue 的行为。例如,要注册全局组件,可以使用 Vue.component API
虽然这种声明方式很方便,但它也会导致一些问题。从技术上讲,Vue 2 没有"app"的概念,我们定义的应用只是通过 new Vue() 创建的根 Vue 实例。从同一个 Vue 构造函数创建的每个根实例共享相同的全局配置
全局配置使得在同一页面上的多个"应用"在全局配置不同时共享同一个 Vue 副本非常困难
为了避免这些问题,在 Vue 3 中我们引入了...
一个新的全局 API:createApp
全局和内部 API 都经过了重构,现已支持 TreeShaking (摇树优化)
如果你曾经在 Vue 中手动操作过 DOM,你可能会用过这种方式:
typescript
import Vue from 'vue'
Vue.nextTick(() => {
// 一些和 DOM 有关的东西
})
但是,如果你从来都没有过手动操作 DOM 的必要,或者更喜欢使用老式的 window.setTimeout() 来代替它,那么 nextTick() 的代码就会变成死代码。
如 webpack 和 Rollup (Vite 基于它) 这样的模块打包工具支持 tree-shaking,遗憾的是,由于之前的 Vue 版本中的代码编写方式,如 Vue.nextTick() 这样的全局 API 是不支持 tree-shake 的,不管它们实际上是否被使用了,都会被包含在最终的打包产物中。
Tip:Vite 基于 Rollup
在 Vue 3 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,对于 ES 模块构建版本来说,全局 API 现在通过具名导出进行访问。例如,我们之前的代码片段现在应该如下所示:
typescript
import { nextTick } from 'vue'
nextTick(() => {
// 一些和 DOM 有关的东西
})
通过这一更改,如果模块打包工具支持 tree-shaking,则 Vue 应用中未使用的全局 API 将从最终的打包产物中排除,从而获得最佳的文件大小。
v-model 指令在组件上的使用已经被重新设计,替换掉了 v-bind.sync
- 非兼容 :用于自定义组件时,v-model prop 和事件默认名称已更改:
- prop:value -> modelValue;
- 事件:input -> update:modelValue;
- 非兼容:v-bind 的 .sync 修饰符和组件的 model 选项已移除,可在 v-model 上加一个参数代替;
- 新增:现在可以在同一个组件上使用多个 v-model 绑定;
- 新增:现在可以自定义 v-model 修饰符。
sync 和 model 选项已废除
在<template v-for> 和没有 v-for 的节点身上使用 key 发生了变化
- 新增:对于 v-if/v-else/v-else-if 的各分支项 key 将不再是必须的,因为现在 Vue 会自动生成唯一的 key。
- 非兼容:如果你手动提供 key,那么每个分支必须使用唯一的 key。你将不再能通过故意使用相同的 key 来强制重用分支。
- 非兼容 :
<template v-for>
的 key 应该设置在<template>
标签上 (而不是设置在它的子节点上)。
v-if 和 v-for 在同一个元素身上使用时的优先级发生了变化
- 非兼容:两者作用于同一个元素上时,v-if 会拥有比 v-for 更高的优先级。
2.x 版本中在一个元素上同时使用 v-if 和 v-for 时,v-for 会优先作用。
3.x 版本中 v-if 总是优先于 v-for 生效。
v-bind="object" 现在是顺序敏感的
- 不兼容:v-bind 的绑定顺序会影响渲染结果。
在 2.x 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么这个独立 attribute 总是会覆盖 object 中的绑定。
javascript
<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="red"></div>
在 3.x 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么绑定的声明顺序将决定它们如何被合并
javascript
<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="blue"></div>
<!-- 模板 -->
<div v-bind="{ id: 'blue' }" id="red"></div>
<!-- 结果 -->
<div id="red"></div>
移除 v-on.native 修饰符
v-on 的 .native 修饰符已被移除。
2.x 语法: 默认情况下,传递给带有 v-on 的组件的事件监听器只能通过 this.$emit 触发。要将原生 DOM 监听器添加到子组件的根元素中,可以使用 .native 修饰符
javascript
<my-component
v-on:close="handleComponentEvent"
v-on:click.native="handleNativeClickEvent"
/>
3.x 语法: 对于子组件中未被定义为组件触发的所有事件监听器,Vue 现在将把它们作为原生事件监听器添加到子组件的根元素中。强烈建议使用 emits 记录每个组件所触发的所有事件。
函数式组件只能通过纯函数进行创建
概览
对变化的总体概述:
- 2.x 中函数式组件带来的性能提升在 3.x 中已经可以忽略不计,因此我们
建议
只使用有状态的组件 - 函数式组件只能由接收 props 和 context (即:slots、attrs、emit) 的普通函数创建
- 非兼容 :functional attribute 已从单文件组件 (SFC) 的
<template>
中移除 - 非兼容:{ functional: true } 选项已从通过函数创建的组件中移除
介绍
在 Vue 2 中,函数式组件主要有两个应用场景:
- 作为性能优化,因为它们的初始化速度比有状态组件快得多
- 返回多个根节点
然而,在 Vue 3 中,有状态组件的性能已经提高到它们之间的区别可以忽略不计的程度。此外,有状态组件现在也支持返回多个根节点。
因此,函数式组件剩下的唯一应用场景
就是简单组件,比如创建动态标题的组件。否则,建议你像平常一样使用有状态组件。
异步组件现在需要通过 defineAsyncComponent 方法进行创建
异步组件的主要作用是延迟组件的加载,只有在组件需要被渲染时才会进行加载和实例化,而不是在页面加载时就加载所有的组件
概览
以下是对变化的总体概述:
- 新的 defineAsyncComponent 助手方法,用于显式地定义异步组件
- component 选项被重命名为 loader
- Loader 函数本身不再接收 resolve 和 reject 参数,且必须返回一个 Promise
介绍
以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,例如:
typescript
const asyncModal = () => import('./Modal.vue')
或
javascript
const asyncModal = {
component: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
error: ErrorComponent,
loading: LoadingComponent
}
现在,在 Vue 3 中,由于函数式组件被定义为纯函数,因此异步组件需要通过将其包裹在新的 defineAsyncComponent 助手方法中来显式地定义:
javascript
import { defineAsyncComponent } from 'vue'
import ErrorComponent from './components/ErrorComponent.vue'
import LoadingComponent from './components/LoadingComponent.vue'
// 不带选项的异步组件
const asyncModal = defineAsyncComponent(() => import('./Modal.vue'))
// 带选项的异步组件
const asyncModalWithOptions = defineAsyncComponent({
// component 重命名为 loader
loader: () => import('./Modal.vue'),
delay: 200,
timeout: 3000,
errorComponent: ErrorComponent,
loadingComponent: LoadingComponent
})
与 2.x 不同,loader 函数不再接收 resolve 和 reject 参数,且必须始终返回 Promise。
javascript
// 2.x 版本
const oldAsyncComponent = (resolve, reject) => {
/* ... */
}
// 3.x 版本
const asyncComponent = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
/* ... */
})
)
组件事件现在应该使用 emits 选项进行声明
Vue 3 现在提供一个 emits 选项(也就是上文的 defineEmits),和现有的 props 选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。
行为
在 Vue 2 中,你可以定义一个组件可接收的 prop,但是你无法声明它可以触发哪些事件:
javascript
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text']
}
</script>
在 vue 3.x 中,和 prop 类似,现在可以通过 emits 选项来定义组件可触发的事件:
javascript
<template>
<div>
<p>{{ text }}</p>
<button v-on:click="$emit('accepted')">OK</button>
</div>
</template>
<script>
export default {
props: ['text'],
emits: ['accepted']
}
</script>
迁移策略
强烈建议
使用 emits 记录每个组件所触发的所有事件。
这尤为重要,因为我们移除了 .native 修饰符。任何未在 emits 中声明的事件监听器都会被算入组件的 $attrs,并将默认绑定到组件的根节点上。
渲染函数
渲染函数 API 更改
此更改不会影响 <template>
用户。
以下是更改的简要总结:
- h 现在是全局导入,而不是作为参数传递给渲染函数
- 更改渲染函数参数,使其在有状态组件和函数组件的表现更加一致
- VNode 现在有一个扁平的 prop 结构
$listeners 被移除或整合到 $attrs
$attrs 现在包含 class 和 style attribute
其他小改变
destroyed 生命周期选项被重命名为 unmounted
beforeDestroy 生命周期选项被重命名为 beforeUnmount
Props 的 default 工厂函数不再可以访问 this 上下文
自定义指令的 API 已更改为与组件生命周期一致,且 binding.expression 已移除
data 选项应始终被声明为一个函数
在 2.x 中,开发者可以通过 object 或者是 function 定义 data 选项。
typescript
<!-- Object 声明 -->
<script>
const app = new Vue({
data: {
apiKey: 'a1b2c3'
}
})
</script>
<!-- Function 声明 -->
<script>
const app = new Vue({
data() {
return {
apiKey: 'a1b2c3'
}
}
})
</script>
在 3.x 中,data 选项已标准化为只接受返回 object 的 function。
此外,当来自组件的 data() 及其 mixin 或 extends 基类被合并时,合并操作现在将被浅层次
地执行:
Tip:mixin 的深度合并非常隐式,这让代码逻辑更难理解和调试。
javascript
const Mixin = {
data() {
return {
user: {
name: 'Jack',
id: 1
}
}
}
}
const CompA = {
mixins: [Mixin],
data() {
return {
user: {
id: 2
}
}
}
}
在 Vue 2.x 中,生成的 $data 是:
javascript
{
"user": {
"id": 2,
"name": "Jack"
}
}
在 3.0 中,其结果将会是:
javascript
{
"user": {
"id": 2
}
}
来自 mixin 的 data 选项现在为浅合并
Attribute 强制策略已更改
这是一个底层的内部 API 更改,绝大多数开发人员不会受到影响。
Transition 的一些 class 被重命名
过渡类名 v-enter 修改为 v-enter-from、过渡类名 v-leave 修改为 v-leave-from。
<TransitionGroup> 不再默认渲染包裹元素
<transition-group>
不再默认渲染根元素,但仍然可以用 tag attribute 创建根元素。
当侦听一个数组时,只有当数组被替换时,回调才会触发,如果需要在变更时触发,则必须指定 deep 选项
非兼容: 当侦听一个数组时,只有当数组被替换时才会触发回调。如果你需要在数组被改变时触发回调,必须指定 deep 选项。
没有特殊指令的标记 (v-if/else-if/else、v-for 或 v-slot) 的 <template> 现在被视为普通元素,并将渲染为原生的 <template> 元素,而不是渲染其内部内容。
这种变化主要是为了更好地与 Web 标准保持一致,并提高 Vue 在静态分析和工具支持方面的表现。虽然在 Vue 2 中,没有用于 Vue 指令的 <template>
会被视为特殊的 Vue 模板标记,但在 Vue 3 中,它们被认为是普通的 HTML 元素。
已挂载的应用不会替换它所挂载的元素
在 Vue 2.x 中,当挂载一个具有 template 的应用时,被渲染的内容会替换我们要挂载的目标元素。在 Vue 3.x 中,被渲染的应用会作为子元素插入,从而替换目标元素的 innerHTML。
生命周期的 hook: 事件前缀改为 vue:
被移除的 API
keyCode 作为 v-on 修饰符的支持
- 非兼容:不再支持使用数字 (即键码) 作为 v-on 修饰符
- 非兼容:不再支持 config.keyCodes
on、off 和 $once 实例方法
on,off 和 $once 实例方法已被移除,组件实例不再实现事件触发接口。
vue2 中用于实现事件总线的可以用外部的库替代,例如 mitt。
在绝大多数情况下,不鼓励使用全局的事件总线在组件之间进行通信。虽然在短期内往往是最简单的解决方案,但从长期来看,它维护起来总是令人头疼。根据具体情况来看,有多种事件总线的替代方案
过滤器 (filter)
在 3.x 中,过滤器已移除,且不再支持。取而代之的是,我们建议用方法调用或计算属性来替换它们。
$children 实例 property
$children
实例 property 已从 Vue 3.0 中移除,不再支持。如果你需要访问子组件实例,我们建议使用模板引用(即 ref)。
propsData 选项
propsData 选项已经被移除。如果你需要在实例创建时向根组件传入 prop,你应该使用 createApp 的第二个参数
$destroy 实例方法。用户不应该再手动管理单个 Vue 组件的生命周期。
完全销毁一个实例。
vue2:在大多数场景中你不应该调用这个方法。最好使用 v-if 和 v-for 指令以数据驱动的方式控制子组件的生命周期。