vue3 快速入门系列 —— 其他API

其他API

前面我们已经学习了 vue3 的一些基础知识,本篇将继续讲解一些常用的其他api,以及较完整的分析vue2 和 vue3 的改变。

浅层响应式数据

shallowRef

shallow 中文:“浅层的”

shallowRef:浅的 ref()。

先用 ref 写个例子:

<!-- 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 能触发页面数据的变化:

<!-- 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个按钮都能修改页面数据:

<!-- 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:

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 能传入响应式数据,并返回一个只读代理

请看示例:

<!-- 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>

浏览器呈现:

# 组件A

name: p2
// 按钮1
change name

copyName: p2
// 按钮2
change copyName

点击第一个按钮,发现 copyName 的值也跟着变化了(说明不是一锤子买卖),但是点击第二个按钮,页面数据不会变化。浏览器控制台也会警告:

[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 数据不是只读的。

请看示例:

<!-- 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 创建一个备份数据,点击第一个按钮没反应,点击第二个按钮,页面变成:

# 组件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().

用于获取一个响应式对象的原始对象。修改原始对象,不会在触发视图。

const foo = {}
const reactiveFoo = reactive(foo)

console.log(toRaw(reactiveFoo) === foo) // true

比如这个使用场景:

<!-- 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组件对象。
import {reactive} from 'vue'
let o = {
    getAge() {
        console.log(18)
    }
}
// Proxy(Object) {getAge: ƒ}
let o2 = reactive(o)
  • 当使用不可变数据源呈现大型列表时,跳过代理转换可以提高性能。

请问输出什么:

import {reactive} from 'vue'
let o = {
    name: 'p',
    age: 18,
}
let o2 = reactive(o)

console.log(o);
console.log(o2);

答案是:

{name: 'p', age: 18}
Proxy(Object) {name: 'p', age: 18}

通过 reactive 会将数据转为响应式。

请看 markRaw 示例:

import {reactive, markRaw} from 'vue'
// 标记 o 不能被转成响应式
let o = markRaw({
    getAge() {
        console.log(18)
    }
})
let o2 = reactive(o)

// {__v_skip: true, getAge: ƒ}
console.log(o2);

比如中国的城市,数据是固定不变的,我不做成响应式的,别人也不许做成响应式的。我可以这么写:

// 中国就这些地方,不会变。我自己不做成响应式的,别人也不许做成响应式的
let citys = markRow([
    {name: '北京'},
    {name: '上海'},
    {name: '深圳'},
    ...
])

customRef

自定义 ref 可用于解决内置 ref 不能解决的问题。

ref 用于创建响应式数据,数据一变,视图也会立刻更新。比如要1秒后更新视图,这个 ref 办不到。

先用ref写个例子:input 输入字符,msg 立刻更新:

<!-- 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 解决这个问题。

实现如下:

<!-- 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。使用起来非常方便。就像这样:

// 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 一样方便。就像这样:

<!-- 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 元素中:

<!-- ChildA.vue -->
<template>
    <p># 组件A</p>
    <div class="box">
        <p>我是组件A内的弹框</p>
    </div>
</template>

我可以利用 Teleport 新增组件将其移到body下面。

<!-- 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 必填,语法是选择器或实际元素

<Teleport to="#some-id" />
<Teleport to=".some-class" />
<Teleport to="[data-teleport]" />

Suspense

suspense 官网说是一个实验性功能。用来在组件树中协调对异步依赖的处理。

我们首先在子组件中异步请求,请看示例:

<!-- Father.vue -->
<template>
    <p># 父亲</p>
    <hr>
    <ChildA/>
</template>

<script lang="ts" setup name="App">
import ChildA from '@/views/ChildA.vue'
</script>
<!-- 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,之所以能这么写,是因为底层帮我们做了。

浏览器查看,发现子组件没有渲染出来。控制台输出:

// 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 方法包裹,子组件能正常显示。

<!-- 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 组件即可。请看代码:

<!-- 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>

子组件也稍微调整下:

<!-- 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...,然后在显示

# 组件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" } }

:数据是一次性出来的,不是先展示 {} 在展示 {...}。所以我们再看官网,就能理解下面这段内容:

<Suspense>
└─ <Dashboard>
   ├─ <Profile>
   │  └─ <FriendStatus>(组件有异步的 setup())
   └─ <Content>
      ├─ <ActivityFeed> (异步组件)
      └─ <Stats>(异步组件)

在这个组件树中有多个嵌套组件,要渲染出它们,首先得解析一些异步资源。如果没有 <Suspense>,则它们每个都需要处理自己的加载、报错和完成状态。在最坏的情况下,我们可能会在页面上看到三个旋转的加载态,在不同的时间显示出内容。

有了 <Suspense> 组件后,我们就可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。

Tip: 在 React 中可以使用 Suspense 组件和 React.lazy() 函数来实现组件的延迟加载。就像这样:

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就是这个:

import { createApp } from 'vue'

const app = createApp({
  /* 根组件选项 */
})

这些 API 以前在 Vue 2 中是全局可用的,但在 Vue 3 中,出于更好的模块化和灵活性考虑,许多 API 被转移到了应用对象中。

app.component

对应 vue2 中 Vue.component,用于注册和获取全局组件。

例如定义一个组件:

<template>
    <p>我的Apple组件</p>
</template>

在 main.ts 中注册:

import Apple from '@/views/Apple.vue'
app.component('Apple', Apple)

现在在任何地方都能直接使用,例如在 ChildA.vue 中:

<!-- 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',在任意组件中就可以获取:

<template>
    <p># 组件A</p>
    x: {{ x }}
    <Apple/>
</template>

但是 ts 会报错,因为找不到 x。

解决方法在官网中有提供。创建一个 ts:

// test.ts
// 官网:https://cn.vuejs.org/api/application.html#app-config-globalproperties
// 正常工作。
export {}

declare module 'vue' {
  interface ComponentCustomProperties {
    x: string,
  }
}

然后在 main.ts 中引入:

import '@/utils/test'
app.config.globalProperties.x = 'hello'

不要随便使用,否则你一下定义100个,以后出问题不好维护。

app.directive

Vue.directive() - 注册或获取全局指令。

我们用函数形式的指令,就像这样:

// https://v2.cn.vuejs.org/v2/guide/custom-directive.html#函数简写
Vue.directive('color-swatch', function (el, binding) {
  el.style.backgroundColor = binding.value
})

比如我写一个这样的指令:

// main.ts 注册一个全局指令
app.directive('green', (element, {value}, vnode) => {
    element.innerText += value
    element.style.color = 'green'
})

接着使用指令:

<!-- 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>

页面呈现:

# 组件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,你可能会用过这种方式:

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 现在通过具名导出进行访问。例如,我们之前的代码片段现在应该如下所示:

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 中的绑定。

<!-- 模板 -->
<div id="red" v-bind="{ id: 'blue' }"></div>
<!-- 结果 -->
<div id="red"></div>

在 3.x 中,如果一个元素同时定义了 v-bind="object" 和一个相同的独立 attribute,那么绑定的声明顺序将决定它们如何被合并

<!-- 模板 -->
<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 修饰符

<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 的函数来创建的,例如:

const asyncModal = () => import('./Modal.vue')

const asyncModal = {
  component: () => import('./Modal.vue'),
  delay: 200,
  timeout: 3000,
  error: ErrorComponent,
  loading: LoadingComponent
}

现在,在 Vue 3 中,由于函数式组件被定义为纯函数,因此异步组件需要通过将其包裹在新的 defineAsyncComponent 助手方法中来显式地定义:

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。

// 2.x 版本
const oldAsyncComponent = (resolve, reject) => {
  /* ... */
}

// 3.x 版本
const asyncComponent = defineAsyncComponent(
  () =>
    new Promise((resolve, reject) => {
      /* ... */
    })
)

组件事件现在应该使用 emits 选项进行声明

Vue 3 现在提供一个 emits 选项(也就是上文的 defineEmits),和现有的 props 选项类似。这个选项可以用来定义一个组件可以向其父组件触发的事件。

行为

在 Vue 2 中,你可以定义一个组件可接收的 prop,但是你无法声明它可以触发哪些事件:

<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 选项来定义组件可触发的事件:

<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 选项。

<!-- 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 的深度合并非常隐式,这让代码逻辑更难理解和调试。

const Mixin = {
  data() {
    return {
      user: {
        name: 'Jack',
        id: 1
      }
    }
  }
}

const CompA = {
  mixins: [Mixin],
  data() {
    return {
      user: {
        id: 2
      }
    }
  }
}

在 Vue 2.x 中,生成的 $data 是:

{
  "user": {
    "id": 2,
    "name": "Jack"
  }
}

在 3.0 中,其结果将会是:

{
  "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 指令以数据驱动的方式控制子组件的生命周期。

全局函数 set 和 delete 以及实例方法 $set 和 $delete。基于代理的变化检测已经不再需要它们了。
上一篇:LocalAi,Ollama+AnythingLLM搭建部署本地大模型AI知识库,汉化版本


下一篇:日志架构演进:从集中式到分布式的Kubernetes日志策略