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

  • vue3 快速入门系列 —— 其他API已关闭评论
  • 25 次浏览
  • A+
所属分类:Web前端
摘要

其他章节请看:vue3 快速入门 系列前面我们已经学习了 vue3 的一些基础知识,本篇将继续讲解一些常用的其他api,以及较完整的分析vue2 和 vue3 的改变。


其他章节请看:

vue3 快速入门 系列

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

其他章节请看:

vue3 快速入门 系列