3

vue3 快速入门系列 —— 其他API - 彭加李

 1 month ago
source link: https://www.cnblogs.com/pengjiali/p/18150115
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

其他章节请看:

vue3 快速入门 系列

前面我们已经学习了 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。基于代理的变化检测已经不再需要它们了。

其他章节请看:

vue3 快速入门 系列


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK