Vue.js 设计的初衷就包括可以被渐进式地采用。这意味着它可以根据需求以多种方式集成到一个项目中。
将 Vue.js 添加到项目中有三种主要方式。
最新版本:
每个版本的详细发行说明可在 GitHub 上找到。
当前是 Beta 版——Vuex 和 Router 的集成仍然是 WIP
在使用 Vue 时,我们推荐在你的浏览器上安装 Vue Devtools,它允许你在一个更友好的界面中审查和调试 Vue 应用。
对于制作原型或学习,你可以这样使用最新版本
<script src="https://unpkg.com/vue@next" rel="external nofollow" ></script>
对于生产环境,我们推荐链接到一个明确的版本号和构建文件,以避免新版本造成的不可预期的破坏:
在用 Vue 构建大型应用时推荐使用 npm 安装 。NPM 能很好地和诸如 Webpack 或 Browserify 模块打包器配合使用。同时 Vue 也提供配套工具来开发单文件组件。
# 最新稳定版$ npm install vue@next
Vue 提供了一个官方的 CLI,为单页面应用 (SPA) 快速搭建繁杂的脚手架。它为现代前端工作流提供了 batteries-included 的构建设置。只需要几分钟的时间就可以运行起来并带有热重载、保存时 lint 校验,以及生产环境可用的构建版本。更多详情可查阅 Vue CLI 的文档。
TIP
CLI 工具假定用户对 Node.js 和相关构建工具有一定程度的了解。如果你是新手,我们强烈建议先在不用构建工具的情况下通读指南,在熟悉 Vue 本身之后再使用 CLI。
对于 Vue 3,你应该使用 npm
上可用的 Vue CLI v4.5 作为 @vue/cli@next
。要升级,你应该需要全局重新安装最新版本的 @vue/cli
:
yarn global add @vue/cli@next# ORnpm install -g @vue/cli@next
然后在 Vue 项目运行:
vue upgrade --next
Vite 是一个 web 开发构建工具,由于其原生 ES 模块导入方法,它允许快速提供代码。
通过在终端中运行以下命令,可以使用 Vite 快速构建 Vue 项目。
使用 npm:
$ npm init vite-app <project-name>$ cd <project-name>$ npm install$ npm run dev
或者 yarn:
$ yarn create vite-app <project-name>$ cd <project-name>$ yarn$ yarn dev
在 npm 包的 dist/ 目录你将会找到很多不同的 Vue.js 构建版本。这里列出了它们之间的差别:
vue(.runtime).global(.prod).js
:<script src="...">
直接使用,则暴露 Vue 全局;vue.global.js
是包含编译器和运行时的“完整”构建,因此它支持动态编译模板。vue.runtime.global.js
只包含运行时,并且需要在构建步骤期间预编译模板。*.prod.js
用于生产的文件。提示
全局打包不是 UMD 构建的,它们被打包成 IIFEs,并且仅用于通过 <script src="...">
直接使用。
vue(.runtime).esm-browser(.prod).js
:<script type="module">
;vue(.runtime).esm-bundler.js
:webpack
,rollup
和 parcel
。process.env.NODE_ENV guards
(需要更换构建工具)@vue/runtime-core
,@vue/runtime-compiler
)
vue.runtime.esm-bundler.js
(默认) 仅运行时,并要求所有模板都要预先编译。这是打包工具的默认入口 (通过 package.json
中的 module 字段),因为在使用 bundler 时,模板通常是预先编译的 (例如:在 *.vue
文件中),你需要将打包工具配置 vue 别名到这个文件vue.cjs(.prod).js
:
require()
进行服务器端渲染。target: 'node'
的 webpack 打包在一起,并正确地将 vue
外部化,则将加载此构建。process.env.NODE_env
会自动需要相应的文件。如果需要在客户端上编译模板 (即:将字符串传递给 template 选项,或使用其在 DOM 中 HTML 作为模板挂载到元素),你需要编译器,因此需要完整的版本:
// 需要编译器Vue.createApp({ template: '<div>{{ hi }}</div>'})// 不需要Vue.createApp({ render() { return Vue.h('div', {}, this.hi) }})
当使用 vue-loader
时,*.vue
文件中的模板在生成时预编译为 JavaScript,在最终的打包器中并不需要编译器,因此可以只使用运行时构建。
提示
已经了解 Vue 2,只想了解 Vue 3 的新功能可以参阅迁移指南!
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带你了解其核心概念和一个示例工程。
如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。
TIP
官方指南假设你已了解关于 HTML、CSS 和 JavaScript 的中级知识。如果你刚开始学习前端开发,将框架作为你的第一步可能不是最好的主意——掌握好基础知识再来吧!之前有其它框架的使用经验会有帮助,但这不是必需的
尝试 Vue.js 最简单的方法是使用 Hello World 例子,你可以在浏览器新标签页中打开它,跟着例子学习一些基础用法。
安装教程给出了更多安装 Vue 的方式。请注意我们不推荐新手直接使用 vue-cli
,尤其是在你还不熟悉基于 Node.js 的构建工具时。
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统:
<div id="counter"> Counter: {{ counter }}</div>
const Counter = { data() { return { counter: 0 } }}Vue.createApp(Counter).mount('#counter')
我们已经成功创建了第一个 Vue 应用!看起来这跟渲染一个字符串模板非常类似,但是 Vue 在背后做了大量工作。现在数据和 DOM 已经被建立了关联,所有东西都是响应式的。我们要怎么确认呢?请看下面的示例,其中 counter
property 每秒递增,你将看到渲染的 DOM 是如何变化的:
const CounterApp = { data() { return { counter: 0 } }, mounted() { setInterval(() => { this.counter++ }, 1000) }}
Counter: 36
Stop timer
除了文本插值,我们还可以像这样绑定元素的 attribute:
<div id="bind-attribute"> <span v-bind:title="message"> 鼠标悬停几秒钟查看此处动态绑定的提示信息! </span></div>
const AttributeBinding = { data() { return { message: 'You loaded this page on ' + new Date().toLocaleString() } }}Vue.createApp(AttributeBinding).mount('#bind-attribute')
这里我们遇到了一点新东西。你看到的 v-bind
attribute 被称为指令。指令带有前缀 v-
,以表示它们是 Vue 提供的特殊 attribute。可能你已经猜到了,它们会在渲染的 DOM 上应用特殊的响应式行为。在这里,该指令的意思是:“将这个元素节点的 title
attribute 和当前活跃实例的 message
property 保持一致”。
为了让用户和应用进行交互,我们可以用 v-on
指令添加一个事件监听器,通过它调用在实例中定义的方法:
<div id="event-handling"> <p>{{ message }}</p> <button v-on:click="reverseMessage">反转 Message</button></div>
const EventHandling = { data() { return { message: 'Hello Vue.js!' } }, methods: { reverseMessage() { this.message = this.message .split('') .reverse() .join('') } }}Vue.createApp(EventHandling).mount('#event-handling')
注意在这个方法中,我们更新了应用的状态,但没有触碰 DOM——所有的 DOM 操作都由 Vue 来处理,你编写的代码只需要关注逻辑层面即可。
Vue 还提供了 v-model
指令,它能轻松实现表单输入和应用状态之间的双向绑定。
<div id="two-way-binding"> <p>{{ message }}</p> <input v-model="message" /></div>
const TwoWayBinding = { data() { return { message: 'Hello Vue!' } }}Vue.createApp(TwoWayBinding).mount('#two-way-binding')
控制切换一个元素是否显示也相当简单:
<div id="conditional-rendering"> <span v-if="seen">现在你看到我了</span></div>
const ConditionalRendering = { data() { return { seen: true } }}Vue.createApp(ConditionalRendering).mount('#conditional-rendering')
这个例子演示了我们不仅可以把数据绑定到 DOM 文本或 attribute,还可以绑定到 DOM 的结构。此外,Vue 也提供一个强大的过渡效果系统,可以在 Vue 插入/更新/移除元素时自动应用过渡效果。
你可以在下面的沙盒中将 seen
从 true
更改为 false
,以检查效果:点击此处实现
还有其它很多指令,每个都有特殊的功能。例如,v-for
指令可以绑定数组的数据来渲染一个项目列表:
<div id="list-rendering"> <ol> <li v-for="todo in todos"> {{ todo.text }} </li> </ol></div>
const ListRendering = { data() { return { todos: [ { text: 'Learn JavaScript' }, { text: 'Learn Vue' }, { text: 'Build something awesome' } ] } }}Vue.createApp(ListRendering).mount('#list-rendering')
组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。
在 Vue 中,组件本质上是一个具有预定义选项的实例。在 Vue 中注册组件很简单:如对 App
对象所做的那样创建一个组件对象,并将其定义在父级组件的 components
选项中:
// 创建 Vue 应用const app = Vue.createApp(...)// 定义名为 todo-item 的新组件app.component('todo-item', { template: `<li>This is a todo</li>`})// 挂载 Vue 应用app.mount(...)
现在,你可以将其放到到另一个组件的模板中:
<ol> <!-- 创建一个 todo-item 组件实例 --> <todo-item></todo-item></ol>
app.component('todo-item', { props: ['todo'], template: `<li>{{ todo.text }}</li>`})
现在,我们可以使用 v-bind
指令将待办项传到循环输出的每个组件中:
<div id="todo-list-app"> <ol> <!-- 现在我们为每个 todo-item 提供 todo 对象 todo 对象是变量,即其内容可以是动态的。 我们也需要为每个组件提供一个“key”,稍后再 作详细解释。 --> <todo-item v-for="item in groceryList" v-bind:todo="item" v-bind:key="item.id" ></todo-item> </ol></div>
const TodoList = { data() { return { groceryList: [ { id: 0, text: 'Vegetables' }, { id: 1, text: 'Cheese' }, { id: 2, text: 'Whatever else humans are supposed to eat' } ] } }}const app = Vue.createApp(TodoList)app.component('todo-item', { props: ['todo'], template: `<li>{{ todo.text }}</li>`})app.mount('#todo-list-app')
尽管这只是一个刻意设计的例子,但是我们已经设法将应用分割成了两个更小的单元。子单元通过 prop 接口与父单元进行了良好的解耦。我们现在可以进一步改进 <todo-item>
组件,提供更为复杂的模板和逻辑,而不会影响到父应用。
在一个大型应用中,有必要将整个应用程序划分为多个组件,以使开发更易管理。在后续教程中我们将详述组件,不过这里有一个 (假想的) 例子,以展示使用了组件的应用模板是什么样的:
<div id="app"> <app-nav></app-nav> <app-view> <app-sidebar></app-sidebar> <app-content></app-content> </app-view></div>
你可能已经注意到 Vue 组件非常类似于自定义元素——它是 Web 组件规范的一部分,这是因为 Vue 的组件语法部分参考了该规范。例如 Vue 组件实现了 Slot API 与 is
attribute。但是,还是有几个关键差别:
虽然 Vue 内部没有使用自定义元素,不过在应用使用自定义元素、或以自定义元素形式发布时,依然有很好的互操作性。Vue CLI 也支持将 Vue 组件构建成为原生的自定义元素。
我们刚才简单介绍了 Vue 核心最基本的功能——本教程的其余部分将更加详细地涵盖这些功能以及其它高阶功能,所以请务必读完整个教程!Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带你了解其核心概念和一个示例工程。如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带你了解其核心概念和一个示例工程。如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。
每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的:
const app = Vue.createApp({ /* 选项 */ })
该应用实例是用来在应用中注册“全局”组件的。我们将在后面的指南中详细讨论,简单的例子:
const app = Vue.createApp({})app.component('SearchInput', SearchInputComponent)app.directive('focus', FocusDirective)app.use(LocalePlugin)
应用实例暴露的大多数方法都会返回该同一实例,允许链式:
Vue.createApp({}) .component('SearchInput', SearchInputComponent) .directive('focus', FocusDirective) .use(LocalePlugin)
你可以在 API 参考 中浏览完整的应用 API。
传递给 createApp
的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。
一个应用需要被挂载到一个 DOM 元素中。例如,如果我们想把一个 Vue 应用挂载到 <div id="app"></div>
,我们应该传递 #app
:
const RootComponent = { /* 选项 */ }const app = Vue.createApp(RootComponent)const vm = app.mount('#app')
与大多数应用方法不同的是,mount
不返回应用本身。相反,它返回的是根组件实例。
虽然没有完全遵循 MVVM 模型_blank_nofollow,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm
(ViewModel 的缩写) 这个变量名表示组件实例。
尽管本页面上的所有示例都只需要一个单一的组件就可以,但是大多数的真实应用都是被组织成一个嵌套的、可重用的组件树。举个例子,一个 todo 应用组件树可能是这样的:
Root Component└─ TodoList ├─ TodoItem │ ├─ DeleteTodoButton │ └─ EditTodoButton └─ TodoListFooter ├─ ClearTodosButton └─ TodoListStatistics
每个组件将有自己的组件实例 vm
。对于一些组件,如 TodoItem
,在任何时候都可能有多个实例渲染。这个应用中的所有组件实例将共享同一个应用实例。
我们会在稍后的组件系统章节具体展开。不过现在,你只需要明白根组件与其他组件没什么不同,配置选项是一样的,所对应的组件实例行为也是一样的。
在前面的指南中,我们认识了 data
property。在 data
中定义的 property 是通过组件实例暴露的:
const app = Vue.createApp({ data() { return { count: 4 } }})const vm = app.mount('#app')console.log(vm.count) // => 4
还有各种其他的组件选项,可以将用户定义的 property 添加到组件实例中,例如 methods
,props
,computed
,inject
和 setup
。我们将在后面的指南中深入讨论它们。组件实例的所有 property,无论如何定义,都可以在组件的模板中访问。
Vue 还通过组件实例暴露了一些内置 property,如 $attrs
和 $emit
。这些 property 都有一个 $
前缀,以避免与用户定义的 property 名冲突。
每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
比如 created 钩子可以用来在一个实例被创建之后执行代码:
Vue.createApp({ data() { return { count: 1} }, created() { // `this` 指向 vm 实例 console.log('count is: ' + this.count) // => "count is: 1" }})
也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mounted、updated 和 unmounted。生命周期钩子的 this
上下文指向调用它的当前活动实例。
TIP
不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a)
或 vm.$watch('a', newValue => this.myMethod())
。因为箭头函数并没有 this
,this
会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefined
或 Uncaught TypeError: this.myMethod is not a function
之类的错误。
下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的
Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层组件实例的数据。所有 Vue.js 的模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。
在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应性系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。
如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量,你也可以不用模板,直接写渲染 (render) 函数,使用可选的 JSX 语法。
数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值:
<span>Message: {{ msg }}</span>
Mustache 标签将会被替代为对应组件实例中 msg
property 的值。无论何时,绑定的组件实例上 msg
property 发生了改变,插值处的内容都会更新。
通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:
<span v-once>这个将不会改变: {{ msg }}</span>
双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用v-html
指令:
<p>Using mustaches: {{ rawHtml }}</p><p>Using v-html directive: <span v-html="rawHtml"></span></p>
这个 span
的内容将会被替换成为 property 值 rawHtml
,直接作为 HTML——会忽略解析 property 值中的数据绑定。注意,你不能使用 v-html
来复合局部模板,因为 Vue 不是基于字符串的模板引擎。反之,对于用户界面 (UI),组件更适合作为可重用和可组合的基本单位。
TIP
在你的站点上动态渲染任意的 HTML 是非常危险的,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要将用户提供的内容作为插值。
Mustache 语法不能在 HTML attribute 中使用 ,然而,可以使用 v-bind
指令:
<div v-bind:id="dynamicId"></div>
对于布尔 attribute (它们只要存在就意味着值为 true
),v-bind
工作起来略有不同,在这个例子中:
<button v-bind:disabled="isButtonDisabled">按钮</button>
如果 isButtonDisabled
的值是 null
或 undefined
,则 disabled
attribute 甚至不会被包含在渲染出来的 <button>
元素中。
迄今为止,在我们的模板中,我们一直都只绑定简单的 property 键值。但实际上,对于所有的数据绑定,Vue.js 都提供了完全的 JavaScript 表达式支持。
{{ number + 1 }} {{ ok ? 'YES' : 'NO' }} {{ message.split('').reverse().join('')}}<div v-bind:id="'list-' + id"></div>
这些表达式会在当前活动实例的数据作用域下作为 JavaScript 被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。
<!-- 这是语句,不是表达式:-->{{ var a = 1 }}<!-- 流控制也不会生效,请使用三元表达式 -->{{ if (ok) { return message } }}
指令 (Directives) 是带有 v-
前缀的特殊 attribute。指令 attribute 的值预期是单个 JavaScript 表达式 (v-for
和 v-on
是例外情况,稍后我们再讨论)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。回顾我们在介绍中看到的例子:
<p v-if="seen">现在你看到我了</p>
这里,v-if
指令将根据表达式 seen
的值的真假来插入/移除 <p>
元素。
一些指令能够接收一个“参数”,在指令名称之后以冒号表示。例如,v-bind
指令可以用于响应式地更新 HTML attribute:
<a v-bind:href="url"> ... </a>
在这里 href
是参数,告知 v-bind
指令将该元素的 href
attribute 与表达式 url
的值绑定。
另一个例子是 v-on
指令,它用于监听 DOM 事件:
<a v-on:click="doSomething"> ... </a>
在这里参数是监听的事件名。我们也会更详细地讨论事件处理。
也可以在指令参数中使用 JavaScript 表达式,方法是用方括号括起来:
<!--注意,参数表达式的写法存在一些约束,如之后的“对动态参数表达式的约束”章节所述。--><a v-bind:[attributeName]="url"> ... </a>
这里的 attributeName
会被作为一个 JavaScript 表达式进行动态求值,求得的值将会作为最终的参数来使用。例如,如果你的组件实例有一个 data property attributeName
,其值为 "href"
,那么这个绑定将等价于 v-bind:href
。
同样地,你可以使用动态参数为一个动态的事件名绑定处理函数:
<a v-on:[eventName]="doSomething"> ... </a>
在这个示例中,当 eventName
的值为 "focus"
时,v-on:[eventName]
将等价于 v-on:focus
修饰符 (modifier) 是以半角句号.
指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent
修饰符告诉 v-on
指令对于触发的事件调用 event.preventDefault()
:
<form v-on:submit.prevent="onSubmit">...</form>
在接下来对 v-on
和 v-for
等功能的探索中,你会看到修饰符的其它例子。
v-
前缀作为一种视觉提示,用来识别模板中 Vue 特定的 attribute。当你在使用 Vue.js 为现有标签添加动态行为 (dynamic behavior) 时,v- 前缀很有帮助,然而,对于一些频繁用到的指令来说,就会感到使用繁琐。同时,在构建由 Vue 管理所有模板的单页面应用程序 (SPA - single page application) 时,v-
前缀也变得没那么重要了。因此,Vue 为 v-bind
和 v-on
这两个最常用的指令,提供了特定简写:
v-bind
缩写<!-- 完整语法 --><a v-bind:href="url"> ... </a><!-- 缩写 --><a :href="url"> ... </a><!-- 动态参数的缩写 --><a :[key]="url"> ... </a>
v-on
缩写<!-- 完整语法 --><a v-on:click="doSomething"> ... </a><!-- 缩写 --><a @click="doSomething"> ... </a><!-- 动态参数的缩写 (2.6.0+) --><a @[event]="doSomething"> ... </a>
它们看起来可能与普通的 HTML 略有不同,但 :
与 @
对于 attribute 名来说都是合法字符,在所有支持 Vue 的浏览器都能被正确地解析。而且,它们不会出现在最终渲染的标记中。缩写语法是完全可选的,但随着你更深入地了解它们的作用,你会庆幸拥有它们。
从下一页开始,我们将在示例中使用缩写,因为这是 Vue 开发者最常用的用法。
动态参数预期会求出一个字符串,异常情况下值为 null
。这个特殊的 null
值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。
动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。例如:
<!-- 这会触发一个编译警告 --><a v-bind:['foo' + bar]="value"> ... </a>
变通的办法是使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式。
在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:
<!--在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。除非在实例中有一个名为“someattr”的 property,否则代码不会工作。--><a v-bind:[someAttr]="value"> ... </a>
模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 Math
和 Date
。你不应该在模板表达式中试图访问用户定义的全局变量。
组件的 data
选项是一个函数。Vue 在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue 会通过响应性系统将其包裹起来,并以 $data
的形式存储在组件实例中。为方便起见,该对象的任何顶级 property 也直接通过组件实例暴露出来:
const app = Vue.createApp({ data() { return { count: 4 } }})const vm = app.mount('#app')console.log(vm.$data.count) // => 4console.log(vm.count) // => 4// 修改 vm.count 的值也会更新 $data.countvm.count = 5console.log(vm.$data.count) // => 5// 反之亦然vm.$data.count = 6console.log(vm.count) // => 6
这些实例 property 仅在实例首次创建时被添加,所以你需要确保它们都在 data
函数返回的对象中。必要时,要对尚未提供所需值的 property 使用 null
、undefined
或其他占位的值。。
直接将不包含在 data
中的新 property 添加到组件实例是可行的。但由于该 property 不在背后的响应式 $data
对象内,所以 Vue 的响应性系统不会自动跟踪它。
Vue 使用 $
前缀通过组件实例暴露自己的内置 API。它还为内部 property 保留 _
前缀。你应该避免使用这两个字符开头的的顶级 data
property 名称。
我们用 methods
选项向组件实例添加方法,它应该是一个包含所需方法的对象:
const app = Vue.createApp({ data() { return { count: 4 } }, methods: { increment() { // `this` 指向该组件实例 this.count++ } }})const vm = app.mount('#app')console.log(vm.count) // => 4vm.increment()console.log(vm.count) // => 5
Vue 自动为 methods
绑定 this
,以便于它始终指向组件实例。这将确保方法在用作事件监听或回调时保持正确的 this
指向。在定义 methods
时应避免使用箭头函数,因为这会阻止 Vue 绑定恰当的 this
指向。
这些 methods
和组件实例的其它所有 property 一样可以在组件的模板中被访问。在模板中,它们通常被当做事件监听使用:
<button @click="increment">Up vote</button>
在上面的例子中,点击 <button>
时,会调用 increment
方法。
也可以直接从模板中调用方法。就像下一章节即将看到的,通常换做计算属性会更好。但是,在计算属性不可行的情况下,使用方法可能会很有用。你可以在模板支持 JavaScript 表达式的任何地方调用方法:
<span :title="toTitleDate(date)"> {{ formatDate(date) }}</span>
如果 toTitleDate
或 formatDate
访问任何响应式数据,则将其作为渲染依赖项进行跟踪,就像直接在模板中使用过一样。
从模板调用的方法不应该有任何副作用,比如更改数据或触发异步进程。如果你想这么做,应该换做生命周期钩子。
Vue 没有内置支持防抖和节流,但可以使用 Lodash 等库来实现。
如果某个组件仅使用一次,可以在 methods
中直接应用防抖:
<script src="https://unpkg.com/lodash@4.17.20/lodash.min.js" rel="external nofollow" ></script><script> Vue.createApp({ methods: { // 用 Lodash 的防抖函数 click: _.debounce(function() { // ... 响应点击 ... }, 500) } }).mount('#app')</script>
但是,这种方法对于可复用组件有潜在的问题,因为它们都共享相同的防抖函数。为了使组件实例彼此独立,可以在生命周期钩子的 created
里添加该防抖函数:
app.component('save-button', { created() { // 用 Lodash 的防抖函数 this.debouncedClick = _.debounce(this.click, 500) }, unmounted() { // 移除组件时,取消定时器 this.debouncedClick.cancel() }, methods: { click() { // ... 响应点击 ... } }, template: ` <button @click="debouncedClick"> Save </button> `})
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如,有一个嵌套数组对象:
Vue.createApp({ data() { return { author: { name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] } } }})
我们想根据 author
是否已经有一些书来显示不同的消息
<div id="computed-basics"> <p>Has published books:</p> <span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span></div>
此时,模板不再是简单的和声明性的。你必须先看一下它,然后才能意识到它执行的计算取决于 author.books
。如果要在模板中多次包含此计算,则问题会变得更糟。
所以,对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性。
<div id="computed-basics"> <p>Has published books:</p> <span>{{ publishedBooksMessage }}</span></div>
Vue.createApp({ data() { return { author: { name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] } } }, computed: { // 计算属性的 getter publishedBooksMessage() { // `this` points to the vm instance return this.author.books.length > 0 ? 'Yes' : 'No' } }}).mount('#computed-basics')
Result:点击此处实现
这里声明了一个计算属性 publishedBooksMessage
。
尝试更改应用程序 data
中 books
数组的值,你将看到 publishedBooksMessage
如何相应地更改。
你可以像普通属性一样将数据绑定到模板中的计算属性。Vue 知道 vm.publishedBookMessage
依赖于 vm.author.books
,因此当 vm.author.books
发生改变时,所有依赖 vm.publishedBookMessage
绑定也会更新。而且最妙的是我们已经声明的方式创建了这个依赖关系:计算属性的 getter 函数没有副作用,这使得更易于测试和理解。
你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:
<p>{{ calculateBooksMessage() }}</p>
// 在组件中methods: { calculateBooksMessage() { return this.author.books.length > 0 ? 'Yes' : 'No' }}
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的反应依赖关系缓存的。计算属性只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 author.books
还没有发生改变,多次访问 publishedBookMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将不再更新,因为 Date.now () 不是响应式依赖:
computed: { now() { return Date.now() }}
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 list
,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 list
。如果没有缓存,我们将不可避免的多次执行 list
的 getter!如果你不希望有缓存,请用 method
来替代。
计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:
// ...computed: { fullName: { // getter get() { return this.firstName + ' ' + this.lastName }, // setter set(newValue) { const names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } }}// ...
现在再运行 vm.fullName = 'John Doe'
时,setter 会被调用,vm.firstName
和 vm.lastName
也会相应地被更新。
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch
选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
例如:
<div id="watch-example"> <p> Ask a yes/no question: <input v-model="question" /> </p> <p>{{ answer }}</p></div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 --><!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 --><script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js" rel="external nofollow" ></script><script> const watchExampleVM = Vue.createApp({ data() { return { question: '', answer: 'Questions usually contain a question mark. ;-)' } }, watch: { // whenever question changes, this function will run question(newQuestion, oldQuestion) { if (newQuestion.indexOf('?') > -1) { this.getAnswer() } } }, methods: { getAnswer() { this.answer = 'Thinking...' axios .get('https://yesno.wtf/api') .then(response => { this.answer = response.data.answer }) .catch(error => { this.answer = 'Error! Could not reach the API. ' + error }) } } }).mount('#watch-example')</script>
结果:点击此处实现
在这个示例中,使用 watch
选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
除了 watch 选项之外,你还可以使用命令式的 vm.$watch API。
Vue 提供了一种更通用的方式来观察和响应当前活动的实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch
——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch
回调。细想一下这个例子:
<div id="demo">{{ fullName }}</div>
const vm = Vue.createApp({ data() { return { firstName: 'Foo', lastName: 'Bar', fullName: 'Foo Bar' } }, watch: { firstName(val) { this.fullName = val + ' ' + this.lastName }, lastName(val) { this.fullName = this.firstName + ' ' + val } }}).mount('#demo')
上面代码是命令式且重复的。将它与计算属性的版本进行比较:
const vm = Vue.createApp({ data() { return { firstName: 'Foo', lastName: 'Bar' } }, computed: { fullName() { return this.firstName + ' ' + this.lastName } }}).mount('#demo')
好得多了,不是吗?
操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind
处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind
用于 class
和 style
时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
我们可以传给 :class
(v-bind:class
的简写) 一个对象,以动态地切换 class:
<div :class="{ active: isActive }"></div>
上面的语法表示 active
这个 class 存在与否将取决于数据 property isActive
的 truthiness。
你可以在对象中传入更多字段来动态切换多个 class。此外,:class
指令也可以与普通的 class
attribute 共存。当有如下模板:
<div class="static" :class="{ active: isActive, 'text-danger': hasError }"></div>
和如下 data:
data() { return { isActive: true, hasError: false }}
渲染的结果为:
<div class="static active"></div>
当 isActive
或者 hasError
变化时,class 列表将相应地更新。例如,如果 hasError
的值为 true
,class 列表将变为 "static active text-danger"
。
绑定的数据对象不必内联定义在模板里:
<div :class="classObject"></div>
data() { return { classObject: { active: true, 'text-danger': false } }}
渲染的结果和上面一样。我们也可以在这里绑定一个返回对象的计算属性。这是一个常用且强大的模式:
<div :class="classObject"></div>
data() { return { isActive: true, error: null }},computed: { classObject() { return { active: this.isActive && !this.error, 'text-danger': this.error && this.error.type === 'fatal' } }}
我们可以把一个数组传给 :class
,以应用一个 class 列表:
<div :class="[activeClass, errorClass]"></div>
data() { return { activeClass: 'active', errorClass: 'text-danger' }}
渲染的结果为:
<div class="active text-danger"></div>
如果你想根据条件切换列表中的 class,可以使用三元表达式:
<div :class="[isActive ? activeClass : '', errorClass]"></div>
这样写将始终添加 errorClass
,但是只有在 isActive
为 truthy[1] 时才添加 activeClass。
不过,当有多个条件 class 时这样写有些繁琐。所以在数组语法中也可以使用对象语法:
<div :class="[{ active: isActive }, errorClass]"></div>
这个章节假设你已经对 Vue 组件有一定的了解。当然你也可以先跳过这里,稍后再回过头来看。
例如,如果你声明了这个组件:
const app = Vue.createApp({})app.component('my-component', { template: `<p class="foo bar">Hi!</p>`})
然后在使用它的时候添加一些 class:
<div id="app"> <my-component class="baz boo"></my-component></div>
HTML 将被渲染为:
<p class="foo bar baz boo">Hi</p>
对于带数据绑定 class 也同样适用:
<my-component :class="{ active: isActive }"></my-component>
当 isActive 为 truthy[1] 时,HTML 将被渲染成为:
<p class="foo bar active">Hi</p>
如果你的组件有多个根元素,你需要定义哪些部分将接收这个类。可以使用 $attrs
组件属性执行此操作:
<div id="app"> <my-component class="baz"></my-component></div>
const app = Vue.createApp({})app.component('my-component', { template: ` <p :class="$attrs.class">Hi!</p> <span>This is a child component</span> `})
你可以在非 prop Attribute 小节了解更多关于组件属性继承的信息。
:style
的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data() { return { activeColor: 'red', fontSize: 30 }}
直接绑定到一个样式对象通常更好,这会让模板更清晰:
<div :style="styleObject"></div>
data() { return { styleObject: { color: 'red', fontSize: '13px' } }}
同样的,对象语法常常结合返回对象的计算属性使用。
:style
的数组语法可以将多个样式对象应用到同一个元素上:
<div :style="[baseStyles, overridingStyles]"></div>
在 :style
中使用需要 (浏览器引擎前缀) vendor prefixes 的 CSS property 时,如 transform
,Vue 将自动侦测并添加相应的前缀。
可以为 style 绑定中的 property 提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex
。
v-if
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。
<h1 v-if="awesome">Vue is awesome!</h1>
也可以用 v-else
添加一个“else 块”:
<h1 v-if="awesome">Vue is awesome!</h1><h1 v-else>Oh no ????</h1>
<template>
元素上使用 v-if
条件渲染分组因为 v-if
是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template>
元素当做不可见的包裹元素,并在上面使用 v-if
。最终的渲染结果将不包含 <template>
元素。
<template v-if="ok"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p></template>
v-else
你可以使用 v-else
指令来表示 v-if
的“else 块”:
<div v-if="Math.random() > 0.5"> Now you see me</div><div v-else> Now you don't</div>
v-else
元素必须紧跟在带 v-if
或者 v-else-if
的元素的后面,否则它将不会被识别。
v-else-if
v-else-if
,顾名思义,充当 v-if
的“else-if 块”,可以连续使用:
<div v-if="type === 'A'"> A</div><div v-else-if="type === 'B'"> B</div><div v-else-if="type === 'C'"> C</div><div v-else> Not A/B/C</div>
类似于 v-else
,v-else-if
也必须紧跟在带 v-if
或者 v-else-if
的元素之后。
v-show
另一个用于根据条件展示元素的选项是 v-show
指令。用法大致一样:
<h1 v-show="ok">Hello!</h1>
不同的是带有 v-show
的元素始终会被渲染并保留在 DOM 中。v-show
只是简单地切换元素的 CSS property display。
注意,v-show
不支持 <template>
元素,也不支持 v-else
。
v-if
vs v-show
v-if
是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好。
v-if
与 v-for
一起使用提示
不推荐同时使用 v-if
和 v-for
。请查阅风格指南以获取更多信息。
当 v-if
与 v-for
一起使用时,v-if
具有比 v-for
更高的优先级。请查阅列表渲染指南以获取详细信息。
v-for
把一个数组对应为一组元素我们可以用 v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用 item in items
形式的特殊语法,其中 items 是源数据数组,而 item
则是被迭代的数组元素的别名。
<ul id="array-rendering"> <li v-for="item in items"> {{ item.message }} </li></ul>
Vue.createApp({ data() { return { items: [{ message: 'Foo' }, { message: 'Bar' }] } }}).mount('#array-rendering')
结果:点击此处实现
在 v-for
块中,我们可以访问所有父作用域的 property。v-for
还支持一个可选的第二个参数,即当前项的索引。
<ul id="array-with-index"> <li v-for="(item, index) in items"> {{ parentMessage }} - {{ index }} - {{ item.message }} </li></ul>
Vue.createApp({ data() { return { parentMessage: 'Parent', items: [{ message: 'Foo' }, { message: 'Bar' }] } }}).mount('#array-with-index')
结果:点击此处实现
你也可以用 of
替代 in
作为分隔符,因为它更接近 JavaScript 迭代器的语法:
<div v-for="item of items"></div>
v-for
里使用对象你也可以用 v-for
来遍历一个对象的 property。
<ul id="v-for-object" class="demo"> <li v-for="value in myObject"> {{ value }} </li></ul>
Vue.createApp({ data() { return { myObject: { title: 'How to do lists in Vue', author: 'Jane Doe', publishedAt: '2016-04-10' } } }}).mount('#v-for-object')
结果:点击此处实现
你也可以提供第二个的参数为 property 名称 (也就是键名 key):
<li v-for="(value, name) in myObject"> {{ name }}: {{ value }}</li>
还可以用第三个参数作为索引:
<li v-for="(value, name, index) in myObject"> {{ index }}. {{ name }}: {{ value }}</li>
提示
在遍历对象时,会按 Object.keys()
的结果遍历,但是不能保证它在不同 JavaScript 引擎下的结果都一致。
当 Vue 正在更新使用 v-for
渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key
attribute:
<div v-for="item in items" :key="item.id"> <!-- content --></div>
建议尽可能在使用 v-for
时提供 key
attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
因为它是 Vue 识别节点的一个通用机制,key
并不仅与 v-for
特别关联。后面我们将在指南中看到,它还具有其它用途。
提示
不要使用对象或数组之类的非基本类型值作为 v-for
的 key。请用字符串或数值类型的值。
更多 key
attribute 的细节用法请移步至 key
的 API 文档。
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
你可以打开控制台,然后对前面例子的 items
数组尝试调用变更方法。比如 example1.items.push({ message: 'Baz' })
。
变更方法,顾名思义,会变更调用了这些方法的原始数组。相比之下,也有非变更方法,例如 filter()
、concat()
和 slice()
。它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组:
example1.items = example1.items.filter(item => item.message.match(/Foo/))
你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。
有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际变更或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。
例如:
<li v-for="n in evenNumbers">{{ n }}</li>
data() { return { numbers: [ 1, 2, 3, 4, 5 ] }},computed: { evenNumbers() { return this.numbers.filter(number => number % 2 === 0) }}
在计算属性不适用的情况下 (例如,在嵌套 v-for
循环中) 你可以使用一个方法:
<ul v-for="numbers in sets"> <li v-for="n in even(numbers)">{{ n }}</li></ul>
data() { return { sets: [[ 1, 2, 3, 4, 5 ], [6, 7, 8, 9, 10]] }},methods: { even(numbers) { return numbers.filter(number => number % 2 === 0) }}
v-for
里使用值的范围v-for
也可以接受整数。在这种情况下,它会把模板重复对应次数。
<div id="range" class="demo"> <span v-for="n in 10">{{ n }} </span></div>
结果:点击此处实现
<template>
中使用 v-for
类似于 v-if
,你也可以利用带有 v-for
的 <template>
来循环渲染一段包含多个元素的内容。比如:
<ul> <template v-for="item in items"> <li>{{ item.msg }}</li> <li class="divider" role="presentation"></li> </template></ul>
v-for
与 v-if
一同使用TIP
注意我们不推荐在同一元素上使用 v-if
和 v-for
。更多细节可查阅风格指南。
当它们处于同一节点,v-if
的优先级比 v-for
更高,这意味着 v-if
将没有权限访问 v-for
里的变量:
<!-- This will throw an error because property "todo" is not defined on instance. --><li v-for="todo in todos" v-if="!todo.isComplete"> {{ todo }}</li>
可以把 v-for
移动到 <template>
标签中来修正:
<template v-for="todo in todos"> <li v-if="!todo.isComplete"> {{ todo }} </li></template>
v-for
这部分内容假定你已经了解组件相关知识。你也完全可以先跳过它,以后再回来查看。
在自定义组件上,你可以像在任何普通元素上一样使用 v-for
:
<my-component v-for="item in items" :key="item.id"></my-component>
然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要使用 props:
<my-component v-for="(item, index) in items" :item="item" :index="index" :key="item.id"></my-component>
不自动将 item
注入到组件里的原因是,这会使得组件与 v-for
的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。
下面是一个简单的 todo 列表的完整例子:
<div id="todo-list-example"> <form v-on:submit.prevent="addNewTodo"> <label for="new-todo">Add a todo</label> <input v-model="newTodoText" id="new-todo" placeholder="E.g. Feed the cat" /> <button>Add</button> </form> <ul> <todo-item v-for="(todo, index) in todos" :key="todo.id" :title="todo.title" @remove="todos.splice(index, 1)" ></todo-item> </ul></div>
const app = Vue.createApp({ data() { return { newTodoText: '', todos: [ { id: 1, title: 'Do the dishes' }, { id: 2, title: 'Take out the trash' }, { id: 3, title: 'Mow the lawn' } ], nextTodoId: 4 } }, methods: { addNewTodo() { this.todos.push({ id: this.nextTodoId++, title: this.newTodoText }) this.newTodoText = '' } }})app.component('todo-item', { template: ` <li> {{ title }} <button @click="$emit('remove')">Remove</button> </li> `, props: ['title']})app.mount('#todo-list-example')
我们可以使用 v-on
指令 (通常缩写为 @
符号) 来监听 DOM 事件,并在触发事件时执行一些 JavaScript。用法为 v-on:click="methodName"
或使用快捷方式 @click="methodName"
例如:
<div id="basic-event"> <button @click="counter += 1">Add 1</button> <p>The button above has been clicked {{ counter }} times.</p></div>
Vue.createApp({ data() { return { counter: 1 } }}).mount('#basic-event')
结果:点击此处实现
然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on
指令中是不可行的。因此 v-on
还可以接收一个需要调用的方法名称。
例如:
<div id="event-with-method"> <!-- `greet` 在下面定义的方法名 --> <button @click="greet">Greet</button></div>
Vue.createApp({ data() { return { name: 'Vue.js' } }, methods: { greet(event) { // `this` 内部 `methods` 指向当前活动实例 alert('Hello ' + this.name + '!') // `event` 是原生 DOM event if (event) { alert(event.target.tagName) } } }}).mount('#event-with-method')
结果:点击此处实现
除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法:
<div id="inline-handler"> <button @click="say('hi')">Say hi</button> <button @click="say('what')">Say what</button></div>
Vue.createApp({ methods: { say(message) { alert(message) } }}).mount('#inline-handler')
结果:点击此处实现
有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event
把它传入方法:
<button @click="warn('Form cannot be submitted yet.', $event)"> Submit</button>
// ...methods: { warn(message, event) { // now we have access to the native event if (event) { event.preventDefault() } alert(message) }}
事件处理程序中可以有多个方法,这些方法由逗号运算符分隔:
<!-- 这两个 one() 和 two() 将执行按钮点击事件 --><button @click="one($event), two($event)"> Submit</button>
// ...methods: { one(event) { // first handler logic... }, two(event) { // second handler logic... }}
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on
提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。
.stop
.prevent
.capture
.self
.once
.passive
<!-- 阻止单击事件继续传播 --><a @click.stop="doThis"></a><!-- 提交事件不再重载页面 --><form @submit.prevent="onSubmit"></form><!-- 修饰符可以串联 --><a @click.stop.prevent="doThat"></a><!-- 只有修饰符 --><form @submit.prevent></form><!-- 添加事件监听器时使用事件捕获模式 --><!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 --><div @click.capture="doThis">...</div><!-- 只当在 event.target 是当前元素自身时触发处理函数 --><!-- 即事件不是从内部元素触发的 --><div @click.self="doThat">...</div>
TIP
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self
会阻止所有的点击,而 v-on:click.self.prevent
只会阻止对元素自身的点击。
<!-- 点击事件将只会触发一次 --><a @click.once="doThis"></a>
不像其它只能对原生的 DOM 事件起作用的修饰符,.once
修饰符还能被用到自定义的组件事件上。如果你还没有阅读关于组件的文档,现在大可不必担心。
Vue 还对应 addEventListener
中的 passive 选项提供了 .passive
修饰符。
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --><!-- 而不会等待 `onScroll` 完成 --><!-- 这其中包含 `event.preventDefault()` 的情况 --><div @scroll.passive="onScroll">...</div>
这个 .passive
修饰符尤其能够提升移动端的性能。
TIP
不要把 .passive
和 .prevent
一起使用,因为 .prevent
将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive
会告诉浏览器你不想阻止事件的默认行为。
在监听键盘事件时,我们经常需要检查详细的按键。Vue 允许为 v-on
或者 @
在监听键盘事件时添加按键修饰符:
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` --><input @keyup.enter="submit" />
你可以直接将 KeyboardEvent.key
暴露的任意有效按键名转换为 kebab-case 来作为修饰符。
<input @keyup.page-down="onPageDown" />
在上述示例中,处理函数只会在 $event.key
等于 'PageDown'
时被调用。
Vue 为最常用的键提供了别名:
.enter
.tab
.delete
(捕获“删除”和“退格”键).esc
.space
.up
.down
.left
.right
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
.ctrl
.alt
.shift
.meta
提示
注意:在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆)。在其他特定键盘上,尤其在 MIT 和 Lisp 机器的键盘、以及其后继产品,比如 Knight 键盘、space-cadet 键盘,meta 被标记为“META”。在 Symbolics 键盘上,meta 被标记为“META”或者“Meta”。
例如:
<!-- Alt + Enter --><input @keyup.alt.enter="clear" /><!-- Ctrl + Click --><div @click.ctrl="doSomething">Do something</div>
TIP
请注意修饰键与常规按键不同,在和 keyup
事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住 ctrl
的情况下释放其它按键,才能触发 keyup.ctrl
。而单单释放 ctrl
也不会触发事件。
.exact
修饰符.exact
修饰符允许你控制由精确的系统修饰符组合触发的事件。
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 --><button @click.ctrl="onClick">A</button><!-- 有且只有 Ctrl 被按下的时候才触发 --><button @click.ctrl.exact="onCtrlClick">A</button><!-- 没有任何系统修饰符被按下的时候才触发 --><button @click.exact="onClick">A</button>
.left
.right
.middle
这些修饰符会限制处理函数仅响应特定的鼠标按钮。
你可能注意到这种事件监听的方式违背了关注点分离 (separation of concern) 这个长期以来的优良传统。但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 v-on
或 @
有几个好处:
你可以用 v-model 指令在表单 <input>
、<textarea>
及 <select>
元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model
本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。
提示
v-model
会忽略所有表单元素的 value
、checked
、selected
attribute 的初始值而总是将当前活动实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data
选项中声明初始值。
v-model
在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
value
property 和 input
事件;checked
property 和 change
事件;value
作为 prop 并将 change
作为事件。提示
对于需要使用输入法 (如中文、日文、韩文等) 的语言,你会发现 v-model
不会在输入法组织文字过程中得到更新。如果你也想处理这个过程,请使用 input
事件。
<input v-model="message" placeholder="edit me" /><p>Message is: {{ message }}</p>
<span>Multiline message is:</span><p style="white-space: pre-line;">{{ message }}</p><br /><textarea v-model="message" placeholder="add multiple lines"></textarea>
在文本区域插值不起作用,应该使用 v-model
来代替。
<!-- bad --><textarea>{{ text }}</textarea><!-- good --><textarea v-model="text"></textarea>
单个复选框,绑定到布尔值:
<input type="checkbox" id="checkbox" v-model="checked" /><label for="checkbox">{{ checked }}</label>
多个复选框,绑定到同一个数组:
<div id="v-model-multiple-checkboxes"> <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" /> <label for="jack">Jack</label> <input type="checkbox" id="john" value="John" v-model="checkedNames" /> <label for="john">John</label> <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" /> <label for="mike">Mike</label> <br /> <span>Checked names: {{ checkedNames }}</span></div>
Vue.createApp({ data() { return { checkedNames: [] } }}).mount('#v-model-multiple-checkboxes')
<div id="v-model-radiobutton"> <input type="radio" id="one" value="One" v-model="picked" /> <label for="one">One</label> <br /> <input type="radio" id="two" value="Two" v-model="picked" /> <label for="two">Two</label> <br /> <span>Picked: {{ picked }}</span></div>
Vue.createApp({ data() { return { picked: '' } }}).mount('#v-model-radiobutton')
单选时:
<div id="v-model-select" class="demo"> <select v-model="selected"> <option disabled value="">Please select one</option> <option>A</option> <option>B</option> <option>C</option> </select> <span>Selected: {{ selected }}</span></div>
Vue.createApp({ data() { return { selected: '' } }}).mount('#v-model-select')
Note
如果 v-model
表达式的初始值未能匹配任何选项,<select>
元素将被渲染为“未选中”状态。在 iOS 中,这会使用户无法选择第一个选项。因为这样的情况下,iOS 不会触发 change
事件。因此,更推荐像上面这样提供一个值为空的禁用选项。
多选时 (绑定到一个数组):
<select v-model="selected" multiple> <option>A</option> <option>B</option> <option>C</option></select><br /><span>Selected: {{ selected }}</span>
用 v-for
渲染的动态选项:
<div id="v-model-select-dynamic" class="demo"> <select v-model="selected"> <option v-for="option in options" :value="option.value"> {{ option.text }} </option> </select> <span>Selected: {{ selected }}</span></div>
Vue.createApp({ data() { return { selected: 'A', options: [ { text: 'One', value: 'A' }, { text: 'Two', value: 'B' }, { text: 'Three', value: 'C' } ] } }}).mount('#v-model-select-dynamic')
对于单选按钮,复选框及选择框的选项,v-model
绑定的值通常是静态字符串 (对于复选框也可以是布尔值):
<!-- 当选中时,`picked` 为字符串 "a" --><input type="radio" v-model="picked" value="a" /><!-- `toggle` 为 true 或 false --><input type="checkbox" v-model="toggle" /><!-- 当选中第一个选项时,`selected` 为字符串 "abc" --><select v-model="selected"> <option value="abc">ABC</option></select>
但是有时我们可能想把值绑定到当前活动实例的一个动态 property 上,这时可以用 v-bind
实现,此外,使用 v-bind
可以将输入值绑定到非字符串。
<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
// when checked:vm.toggle === 'yes'// when unchecked:vm.toggle === 'no'
Tip
这里的 true-value
和 false-value
attribute 并不会影响输入控件的 value
attribute,因为浏览器在提交表单时并不会包含未被选中的复选框。如果要确保表单中这两个值中的一个能够被提交,(即“yes”或“no”),请换用单选按钮。
<input type="radio" v-model="pick" v-bind:value="a" />
// 当选中时vm.pick === vm.a
<select v-model="selected"> <!-- 内联对象字面量 --> <option :value="{ number: 123 }">123</option></select>
// 当被选中时typeof vm.selected // => 'object'vm.selected.number // => 123
.lazy
在默认情况下,v-model
在每次 input
事件触发后将输入框的值与数据进行同步 (除了上述输入法组织文字时)。你可以添加 lazy
修饰符,从而转为在 change
事件_之后_进行同步:
<!-- 在“change”时而非“input”时更新 --><input v-model.lazy="msg" />
.number
如果想自动将用户的输入值转为数值类型,可以给 v-model
添加 number
修饰符:
<input v-model.number="age" type="number" />
这通常很有用,因为即使在 type="number"
时,HTML 输入元素的值也总会返回字符串。如果这个值无法被 parseFloat()
解析,则会返回原始的值。
.trim
如果要自动过滤用户输入的首尾空白字符,可以给 v-model
添加 trim
修饰符:
<input v-model.trim="msg" />
v-model
如果你还不熟悉 Vue 的组件,可以暂且跳过这里。
HTML 原生的输入元素类型并不总能满足需求。幸好,Vue 的组件系统允许你创建具有完全自定义行为且可复用的输入组件。这些输入组件甚至可以和 v-model
一起使用!
要了解更多,请参阅组件指南中的自定义输入组件。
这里有一个 Vue 组件的示例:
// 创建一个Vue 应用const app = Vue.createApp({})// 定义一个名为 button-counter 的新全局组件app.component('button-counter', { data() { return { count: 0 } }, template: ` <button @click="count++"> You clicked me {{ count }} times. </button>`})
INFO
在这里演示的是一个简单的示例,但是在典型的 Vue 应用程序中,我们使用单个文件组件而不是字符串模板。你可以在本节找到有关它们的更多信息。
组件是可复用的组件实例,且带有一个名字:在这个例子中是 <button-counter>
。我们可以在一个通过 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用:
<div id="components-demo"> <button-counter></button-counter></div>
app.mount('#components-demo')
因为组件是可复用的组件实例,所以它们与 new Vue 接收相同的选项,例如 data
、computed
、watch
、methods
以及生命周期钩子等。仅有的例外是像 el
这样根实例特有的选项。
你可以将组件进行任意次数的复用:
<div id="components-demo"> <button-counter></button-counter> <button-counter></button-counter> <button-counter></button-counter></div>
注意当点击按钮时,每个组件都会各自独立维护它的 count
。因为你每用一次组件,就会有一个它的新实例被创建。
通常一个应用会以一棵嵌套的组件树的形式来组织
例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。
为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册和局部注册。至此,我们的组件都只是通过 component
全局注册的:
const app = Vue.createApp({})app.component('my-component-name', { // ... 选项 ...})
全局注册的组件可以在随后创建的 app
实例模板中使用,也包括根实例组件树中的所有子组件的模板中。
到目前为止,关于组件注册你需要了解的就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把组件注册读完。
早些时候,我们提到了创建一个博文组件的事情。问题是如果你不能向这个组件传递某一篇博文的标题或内容之类的我们想展示的数据的话,它是没有办法使用的。这也正是 prop 的由来。
Prop 是你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个 property。为了给博文组件传递一个标题,我们可以用一个 props 选项将其包含在该组件可接受的 prop
列表中:
const app = Vue.createApp({})app.component('blog-post', { props: ['title'], template: `<h4>{{ title }}</h4>`})app.mount('#blog-post-demo')
一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 data
中的值一样。
一个 prop 被注册之后,你就可以像这样把数据作为一个自定义 attribute 传递进来:
<div id="blog-post-demo" class="demo"> <blog-post title="My journey with Vue"></blog-post> <blog-post title="Blogging with Vue"></blog-post> <blog-post title="Why Vue is so fun"></blog-post></div>
然而在一个典型的应用中,你可能在 data
里有一个博文的数组:
const App = { data() { return { posts: [ { id: 1, title: 'My journey with Vue' }, { id: 2, title: 'Blogging with Vue' }, { id: 3, title: 'Why Vue is so fun' } ] } }}const app = Vue.createApp({})app.component('blog-post', { props: ['title'], template: `<h4>{{ title }}</h4>`})app.mount('#blog-posts-demo')
并想要为每篇博文渲染一个组件:
<div id="blog-posts-demo"> <blog-post v-for="post in posts" :key="post.id" :title="post.title" ></blog-post></div>
如上所示,你会发现我们可以使用 v-bind
来动态传递 prop。这在你一开始不清楚要渲染的具体内容,是非常有用的。
到目前为止,关于 prop 你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把 prop 读完。
在我们开发 <blog-post>
组件时,它的一些功能可能要求我们和父级组件进行沟通。例如我们可能会引入一个辅助功能来放大博文的字号,同时让页面的其它部分保持默认的字号。
在其父组件中,我们可以通过添加一个 postFontSize
数据 property 来支持这个功能:
const App = { data() { return { posts: [ /* ... */ ], postFontSize: 1 } }}
它可以在模板中用来控制所有博文的字号:
<div id="blog-posts-events-demo"> <div v-bind:style="{ fontSize: postFontSize + 'em' }"> <blog-post v-for="post in posts" :key="post.id" :title="title"></blog-post> </div></div>
现在我们在每篇博文正文之前添加一个按钮来放大字号:
app.component('blog-post', { props: ['title'], template: ` <div class="blog-post"> <h4>{{ title }}</h4> <button> Enlarge text </button> </div> `})
问题是这个按钮不会做任何事:
<button> Enlarge text</button>
当点击这个按钮时,我们需要告诉父级组件放大所有博文的文本。幸好组件实例提供了一个自定义事件的系统来解决这个问题。父级组件可以像处理 native DOM 事件一样通过 v-on
或 @
监听子组件实例的任意事件:
<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>
同时子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个事件:
<button @click="$emit('enlarge-text')"> Enlarge text</button>
多亏了 @enlarge-text="postFontSize += 0.1"
监听器,父级将接收事件并更新 postFontSize
值。点击此处实现
我们可以在组件的 emits
选项中列出已抛出的事件。
app.component('blog-post', { props: ['title'], emits: ['enlarge-text']})
这将允许你检查组件抛出的所有事件,还可以选择 validate them
有的时候用一个事件来抛出一个特定的值是非常有用的。例如我们可能想让 <blog-post>
组件决定它的文本要放大多少。这时可以使用 $emit
的第二个参数来提供这个值:
<button @click="$emit('enlarge-text', 0.1)"> Enlarge text</button>
然后当在父级组件监听这个事件的时候,我们可以通过 $event
访问到被抛出的这个值:
<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>
或者,如果这个事件处理函数是一个方法:
<blog-post ... @enlarge-text="onEnlargeText"></blog-post>
那么这个值将会作为第一个参数传入这个方法:
methods: { onEnlargeText(enlargeAmount) { this.postFontSize += enlargeAmount }}
自定义事件也可以用于创建支持 v-model
的自定义输入组件。记住:
<input v-model="searchText" />
等价于:
<input :value="searchText" @input="searchText = $event.target.value" />
当用在组件上时,v-model
则会这样:
<custom-input :model-value="searchText" @update:model-value="searchText = $event"></custom-input>
WARNING
请注意,我们在这里使用的是 model value
,因为我们使用的是 DOM 模板中的 kebab-case。你可以在 DOM Template Parsing Caveats 部分找到关于 kebab cased 和 camelCased 属性的详细说明
为了让它正常工作,这个组件内的 <input>
必须:
value
attribute 绑定到一个名叫 modelValue
的 prop 上input
事件被触发时,将新的值通过自定义的 update:modelValue
事件抛出写成代码之后是这样的:
app.component('custom-input', { props: ['modelValue'], template: ` <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" > `})
现在 v-model
就应该可以在这个组件上完美地工作起来了:
<custom-input v-model="searchText"></custom-input>
在自定义组件中创建 v-model
功能的另一种方法是使用 computed
property 的功能来定义 getter 和 setter。
在下面的示例中,我们使用计算属性重构 <custom-input>
组件。
请记住,get
方法应返回 modelValue
property,或用于绑定的任何 property,set
方法应为该 property 触发相应的 $emit
。
app.component('custom-input', { props: ['modelValue'], template: ` <input v-model="value"> `, computed: { value: { get() { return this.modelValue }, set(value) { this.$emit('update:modelValue', value) } } }})
现在你只需要了解自定义组件事件,但一旦你读完本页并对其内容还觉得不错,我们建议你稍后再阅读有关自定义事件
和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样:
<alert-box> Something bad happened.</alert-box>
可能会渲染出这样的东西:点击此处实现
幸好,Vue 自定义的 <slot>
元素让这变得非常简单:
app.component('alert-box', { template: ` <div class="demo-alert-box"> <strong>Error!</strong> <slot></slot> </div> `})
如你所见,我们只要在需要的地方加入插槽就行了——就这么简单!
到目前为止,关于插槽你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把插槽读完。
有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里:点击此处实现
上述内容可以通过 Vue 的 <component>
元素加一个特殊的 is
attribute 来实现:
<!-- 组件会在 `currentTabComponent` 改变时改变 --><component :is="currentTabComponent"></component>
在上述示例中,currentTabComponent
可以包括
你可以在这里查阅并体验完整的代码,或在这个版本了解绑定组件选项对象,而不是已注册组件名的示例。
请留意,这个 attribute 可以用于常规 HTML 元素,但这些元素将被视为组件,这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value
这样的 property,若想让其如预期般工作,你需要使用 .prop 修饰器。
到目前为止,关于动态组件你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把动态 & 异步组件读完。
有些 HTML 元素,诸如 <ul>
、<ol>
、<table>
和 <select>
,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>
、<tr>
和 <option>
,只能出现在其它某些特定的元素内部。
这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:
<table> <blog-post-row></blog-post-row></table>
这个自定义组件 <blog-post-row>
会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 v-is
attribute 给了我们一个变通的办法:
<table> <tr v-is="'blog-post-row'"></tr></table>
WARNING
v-is
值应为 JavaScript 字符串文本:
<!-- 错误的,这样不会渲染任何东西 --><tr v-is="blog-post-row"></tr><!-- 正确的 --><tr v-is="'blog-post-row'"></tr>
另外,HTML 属性名不区分大小写,因此浏览器将把所有大写字符解释为小写。这意味着当你在 DOM 模板中使用时,驼峰 prop 名称和 event 处理器参数需要使用它们的 kebab-cased (横线字符分隔) 等效值:
// 在JavaScript中的驼峰app.component('blog-post', { props: ['postTitle'], template: ` <h3>{{ postTitle }}</h3> `})
<!-- 在HTML则是横线字符分割 --><blog-post post-title="hello!"></blog-post>
需要注意的是如果我们从以下来源使用模板的话,这条限制是*不存在*的:
template: '...'
)<script type="text/x-template">
到这里,你需要了解的解析 DOM 模板时的注意事项——实际上也是 Vue 的全部必要内容,大概就是这些了。恭喜你!接下来还有很多东西要去学习,不过首先,我们推荐你先休息一下,试用一下 Vue,自己随意做些好玩的东西。
如果你感觉已经掌握了这些知识,我们推荐你再回来把完整的组件&异步组件指南,包括侧边栏中组件深入章节的所有页面读完。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了:
const app = Vue.createApp({...})app.component('my-component-name', { /* ... */})
该组件名就是 app.component
的第一个参数,在上面的例子中,组件的名称是“my-component-name”。
你给予组件的名字可能依赖于你打算拿它来做什么。当直接在 DOM 中使用一个组件 (而不是在字符串模板或单文件组件) 的时候,我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。
这样会帮助你避免与当前以及未来的 HTML 元素发生冲突。
你可以在风格指南中查阅到关于组件名的其它建议。
在字符串模板或单个文件组件中定义组件时,定义组件名的方式有两种:
app.component('my-component-name', { /* ... */})
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>
。
app.component('MyComponentName', { /* ... */})
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name>
和 <MyComponentName>
都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
到目前为止,我们只用过 app.component
来创建组件:
Vue.createApp({...}).component('my-component-name', { // ... 选项 ...})
这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的组件实例的模板中。比如:
const app = Vue.createApp({})app.component('component-a', { /* ... */})app.component('component-b', { /* ... */})app.component('component-c', { /* ... */})app.mount('#app')
<div id="app"> <component-a></component-a> <component-b></component-b> <component-c></component-c></div>
在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以相互使用。
全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:
const ComponentA = { /* ... */}const ComponentB = { /* ... */}const ComponentC = { /* ... */}
然后在 components
选项中定义你想要使用的组件:
const app = Vue.createApp({ components: { 'component-a': ComponentA, 'component-b': ComponentB }})
对于 components
对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。
注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentA
在 ComponentB
中可用,则你需要这样写:
const ComponentA = { /* ... */}const ComponentB = { components: { 'component-a': ComponentA } // ...}
或者如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像:
import ComponentA from './ComponentA.vue'export default { components: { ComponentA } // ...}
注意在 ES2015+ 中,在对象中放一个类似 ComponentA
的变量名其实是 ComponentA
:ComponentA
的缩写,即这个变量名同时是:
如果你没有通过 import
/require
使用一个模块系统,也许可以暂且跳过这个章节。如果你使用了,那么我们会为你提供一些特殊的使用说明和注意事项。
如果你还在阅读,说明你使用了诸如 Babel 和 webpack 的模块系统。在这些情况下,我们推荐创建一个 components
目录,并将每个组件放置在其各自的文件中。
然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的 ComponentB.js
或 ComponentB.vue
文件中:
import ComponentA from './ComponentA'import ComponentC from './ComponentC'export default { components: { ComponentA, ComponentC } // ...}
现在 ComponentA
和 ComponentC
都可以在 ComponentB
的模板中使用了。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
到这里,我们只看到了以字符串数组形式列出的 prop:
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
但是,通常你希望每个 prop 都有指定的值类型。这时,你可以以对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型:
props: { title: String, likes: Number, isPublished: Boolean, commentIds: Array, author: Object, callback: Function, contactsPromise: Promise // 或任何其他构造函数}
这不仅为你的组件提供了文档,还会在它们遇到错误的类型时从浏览器的 JavaScript 控制台提示用户。你会在这个页面接下来的部分看到类型检查和其它 prop 验证。
这样,你已经知道了可以像这样给 prop 传入一个静态的值:
<blog-post title="My journey with Vue"></blog-post>
你也知道 prop 可以通过 v-bind
或简写 :
动态赋值,例如:
<!-- 动态赋予一个变量的值 --><blog-post :title="post.title"></blog-post><!-- 动态赋予一个复杂表达式的值 --><blog-post :title="post.title + ' by ' + post.author.name"></blog-post>
在上述两个示例中,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个 prop。
<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue --><!-- 这是一个 JavaScript 表达式而不是一个字符串。 --><blog-post :likes="42"></blog-post><!-- 用一个变量进行动态赋值。--><blog-post :likes="post.likes"></blog-post>
<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。 --><blog-post is-published></blog-post><!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue --><!-- 这是一个 JavaScript 表达式而不是一个字符串。 --><blog-post :is-published="false"></blog-post><!-- 用一个变量进行动态赋值。 --><blog-post :is-published="post.isPublished"></blog-post>
<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue --><!-- 这是一个 JavaScript 表达式而不是一个字符串。 --><blog-post :comment-ids="[234, 266, 273]"></blog-post><!-- 用一个变量进行动态赋值。 --><blog-post :comment-ids="post.commentIds"></blog-post>
<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue --><!-- 这是一个 JavaScript 表达式而不是一个字符串。 --><blog-post :author="{ name: 'Veronica', company: 'Veridian Dynamics' }"></blog-post><!-- 用一个变量进行动态赋值。 --><blog-post :author="post.author"></blog-post>
如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind
(取代 v-bind
:prop-name
)。例如,对于一个给定的对象 post
:
post: { id: 1, title: 'My Journey with Vue'}
下面的模板:
<blog-post v-bind="post"></blog-post>
等价于:
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
这里有两种常见的试图变更一个 prop 的情形:
props: ['initialCounter'],data() { return { counter: this.initialCounter }}
props: ['size'],computed: { normalizedSize: function () { return this.size.trim().toLowerCase() }}
提示
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
我们可以为组件的 prop 指定验证要求,例如你知道的这些类型。如果有一个需求没有被满足,则 Vue 会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。
为了定制 prop 的验证方式,你可以为 props
中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:
app.component('my-component', { props: { // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证) propA: Number, // 多个可能的类型 propB: [String, Number], // 必填的字符串 propC: { type: String, required: true }, // 带有默认值的数字 propD: { type: Number, default: 100 }, // 带有默认值的对象 propE: { type: Object, // 对象或数组默认值必须从一个工厂函数获取 default: function() { return { message: 'hello' } } }, // 自定义验证函数 propF: { validator: function(value) { // 这个值必须匹配下列字符串中的一个 return ['success', 'warning', 'danger'].indexOf(value) !== -1 } }, // 具有默认值的函数 propG: { type: Function, // 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数 default: function() { return 'Default function' } } }})
当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。
提示
注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data
、computed
等) 在 default
或 validator
函数中是不可用的。
type
可以是下列原生构造函数中的一个:
此外,type
还可以是一个自定义的构造函数,并且通过 instanceof
来进行检查确认。例如,给定下列现成的构造函数:
function Person(firstName, lastName) { this.firstName = firstName this.lastName = lastName}
你可以使用:
app.component('blog-post', { props: { author: Person }})
用于验证 author
prop 的值是否是通过 new Person
创建的。
HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:
const app = Vue.createApp({})app.component('blog-post', { // camelCase in JavaScript props: ['postTitle'], template: '<h3>{{ postTitle }}</h3>'})
<!-- kebab-case in HTML --><blog-post post-title="hello!"></blog-post>
重申一次,如果你使用字符串模板,那么这个限制就不存在了。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 props 或 emits 定义的 attribute。常见的示例包括 class
、style
和 id
属性。
当组件返回单个根节点时,非 prop attribute 将自动添加到根节点的 attribute 中。例如,在 <date-picker>
组件的实例中:
app.component('date-picker', { template: ` <div class="date-picker"> <input type="datetime" /> </div> `})
如果我们需要通过 data status
property 定义 <date-picker>
组件的状态,它将应用于根节点 (即 div.date-picker
)。
<!-- 具有非prop attribute的Date-picker组件--><date-picker data-status="activated"></date-picker><!-- 渲染 date-picker 组件 --><div class="date-picker" data-status="activated"> <input type="datetime" /></div>
同样的规则适用于事件监听器:
<date-picker @change="submitChange"></date-picker>
app.component('date-picker', { created() { console.log(this.$attrs) // { onChange: () => {} } }})
当有一个 HTML 元素将 change
事件作为 date-picker
的根元素时,这可能会有帮助。
app.component('date-picker', { template: ` <select> <option value="1">Yesterday</option> <option value="2">Today</option> <option value="3">Tomorrow</option> </select> `})
在这种情况下,change
事件监听器从父组件传递到子组件,它将在原生 select
的 change
事件上触发。我们不需要显式地从 date-picker
发出事件:
<div id="date-picker" class="demo"> <date-picker @change="showChange"></date-picker></div>
const app = Vue.createApp({ methods: { showChange(event) { console.log(event.target.value) // 将记录所选选项的值 } }})
如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false
。例如:
禁用 attribute 继承的常见情况是需要将 attribute 应用于根节点之外的其他元素。
通过将 inheritAttrs
选项设置为 false
,你可以访问组件的 $attrs
property,该 property 包括组件 props
和 emits
property 中未包含的所有属性 (例如,class
、style
、v-on
监听器等)。
使用上一节中的 date-picker 组件示例,如果需要将所有非 prop attribute 应用于 input
元素而不是根 div
元素,则可以使用 v-bind
缩写来完成。
app.component('date-picker', { inheritAttrs: false, template: ` <div class="date-picker"> <input type="datetime" v-bind="$attrs" /> </div> `})
有了这个新配置,data status
attribute 将应用于 input
元素!
<!-- Date-picker 组件 使用非 prop attribute --><date-picker data-status="activated"></date-picker><!-- 渲染 date-picker 组件 --><div class="date-picker"> <input type="datetime" data-status="activated" /></div>
与单个根节点组件不同,具有多个根节点的组件不具有自动 attribute 回退行为。如果未显式绑定 $attrs
,将发出运行时警告。
<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
// 这将发出警告app.component('custom-layout', { template: ` <header>...</header> <main>...</main> <footer>...</footer> `})// 没有警告,$attrs被传递到<main>元素app.component('custom-layout', { template: ` <header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer> `})
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个 camelCase 名字的事件:
this.$emit('myEvent')
则监听这个名字的 kebab-case 版本是不会有任何效果的:
<!-- 没有效果 --><my-component @my-event="doSomething"></my-component>
不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCase 或 PascalCase 了。并且 v-on
事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 @myEvent
将会变成 @myevent
——导致 myEvent
不可能被监听到。
因此,我们推荐你始终使用 kebab-case 的事件名。
在 Vue School 上观看关于定义自定义事件的免费视频。
可以通过 emits
选项在组件上定义已发出的事件。
app.component('custom-form', { emits: ['in-focus', 'submit']})
当在 emits
选项中定义了原生事件 (如 click
) 时,将使用组件中的事件替代原生事件侦听器。
TIP
建议定义所有发出的事件,以便更好地记录组件应该如何工作。
与 prop 类型验证类似,如果使用对象语法而不是数组语法定义发出的事件,则可以验证它。
要添加验证,将为事件分配一个函数,该函数接收传递给 $emit
调用的参数,并返回一个布尔值以指示事件是否有效。
app.component('custom-form', { emits: { // 没有验证 click: null, // 验证submit 事件 submit: ({ email, password }) => { if (email && password) { return true } else { console.warn('Invalid submit event payload!') return false } } }, methods: { submitForm() { this.$emit('submit', { email, password }) } }})
v-model
参数默认情况下,组件上的 v-model
使用 modelValue
作为 prop 和 update:modelValue
作为事件。我们可以通过向 v-model
传递参数来修改这些名称:
<my-component v-model:foo="bar"></my-component>
在本例中,子组件将需要一个 foo
prop 并发出 update:foo
要同步的事件:
const app = Vue.createApp({})app.component('my-component', { props: { foo: String }, template: ` <input type="text" :value="foo" @input="$emit('update:foo', $event.target.value)"> `})
<my-component v-model:foo="bar"></my-component>
v-model
绑定通过利用以特定 prop 和事件为目标的能力,正如我们之前在 v-model
参数中所学的那样,我们现在可以在单个组件实例上创建多个 v-model 绑定。
每个 v-model 将同步到不同的 prop,而不需要在组件中添加额外的选项:
<user-name v-model:first-name="firstName" v-model:last-name="lastName"></user-name>
const app = Vue.createApp({})app.component('user-name', { props: { firstName: String, lastName: String }, template: ` <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)"> <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)"> `})
v-model
修饰符在 2.x 中,我们对组件 v-model
上的 .trim
等修饰符提供了硬编码支持。但是,如果组件可以支持自定义修饰符,则会更有用。在 3.x 中,添加到组件 v-model
的修饰符将通过 modelModifiers
prop 提供给组件:
当我们学习表单输入绑定时,我们看到 v-model
有内置修饰符——.trim
、.number
和 .lazy
。但是,在某些情况下,你可能还需要添加自己的自定义修饰符。
让我们创建一个示例自定义修饰符 capitalize
,它将 v-model
绑定提供的字符串的第一个字母大写。
添加到组件 v-model
的修饰符将通过 modelModifiers
prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers
prop。
请注意,当组件的 created
生命周期钩子触发时,modelModifiers
prop 包含 capitalize
,其值为 true
——因为它被设置在 v-model
绑定 v-model.capitalize="bar"
。
<my-component v-model.capitalize="bar"></my-component>
app.component('my-component', { props: { modelValue: String, modelModifiers: { default: () => ({}) } }, template: ` <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"> `, created() { console.log(this.modelModifiers) // { capitalize: true } }})
现在我们已经设置了 prop,我们可以检查 modelModifiers
对象键并编写一个处理器来更改发出的值。在下面的代码中,每当 <input/>
元素触发 input
事件时,我们都将字符串大写。
<div id="app"> <my-component v-model.capitalize="myText"></my-component> {{ myText }}</div>
const app = Vue.createApp({ data() { return { myText: '' } }})app.component('my-component', { props: { modelValue: String, modelModifiers: { default: () => ({}) } }, methods: { emitValue(e) { let value = e.target.value if (this.modelModifiers.capitalize) { value = value.charAt(0).toUpperCase() + value.slice(1) } this.$emit('update:modelValue', value) } }, template: `<input type="text" :value="modelValue" @input="emitValue">`})app.mount('#app')
对于带参数的 v-model
绑定,生成的 prop 名称将为 arg + "Modifiers"
:
<my-component v-model:foo.capitalize="bar"></my-component>
app.component('my-component', { props: ['foo', 'fooModifiers'], template: ` <input type="text" :value="foo" @input="$emit('update:foo', $event.target.value)"> `, created() { console.log(this.fooModifiers) // { capitalize: true } }})
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot>
元素作为承载分发内容的出口。
它允许你像这样合成组件:
<todo-button> Add todo</todo-button>
然后在 <todo-button>
的模板中,你可能有:
<!-- todo-button 组件模板 --><button class="btn-primary"> <slot></slot></button>
当组件渲染的时候,将会被替换为“Add Todo”。
<!-- 渲染 HTML --><button class="btn-primary"> Add todo</button>
不过,字符串只是开始!插槽还可以包含任何模板代码,包括 HTML:
<todo-button> <!-- 添加一个Font Awesome 图标 --> <i class="fas fa-plus"></i> Add todo</todo-button>
或其他组件
<todo-button> <!-- 添加一个图标的组件 --> <font-awesome-icon name="plus"></font-awesome-icon> Add todo</todo-button>
如果 <todo-button>
的 template 中没有包含一个 <slot>
元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃
<!-- todo-button 组件模板 --><button class="btn-primary"> Create a new item</button>
<todo-button> <!-- 以下文本不会渲染 --> Add todo</todo-button>
当你想在一个插槽中使用数据时,例如:
<todo-button> Delete a {{ item.name }}</todo-button>
该插槽可以访问与模板其余部分相同的实例 property (即相同的“作用域”)。
插槽不能访问 <todo-button>
的作用域。例如,尝试访问 action
将不起作用:
<todo-button action="delete"> Clicking here will {{ action }} an item <!-- `action` 未被定义,因为它的内容是传递*到* <todo-button>,而不是*在* <todo-button>里定义的。 --></todo-button>
作为一条规则,请记住:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 <submit-button>
组件中:
<button type="submit"> <slot></slot></button>
我们可能希望这个 <button>
内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在 <slot>
标签内:
<button type="submit"> <slot>Submit</slot></button>
现在当我在一个父级组件中使用 <submit-button
> 并且不提供任何插槽内容时:
<submit-button></submit-button>
后备内容“Submit”将会被渲染:
<button type="submit"> Submit</button>
但是如果我们提供内容:
<submit-button> Save</submit-button>
则这个提供的内容将会被渲染从而取代后备内容:
<button type="submit"> Save</button>
有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout>
组件:
<div class="container"> <header> <!-- 我们希望把页头放这里 --> </header> <main> <!-- 我们希望把主要内容放这里 --> </main> <footer> <!-- 我们希望把页脚放这里 --> </footer></div>
对于这样的情况,<slot>
元素有一个特殊的 attribute:name
。这个 attribute 可以用来定义额外的插槽:
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer></div>
一个不带 name
的 <slot>
出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 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 v-slot:footer> <p>Here's some contact info</p> </template></base-layout>
现在 <template>
元素中的所有内容都将会被传入相应的插槽。
渲染的 HTML 将会是:
<div class="container"> <header> <h1>Here might be a page title</h1> </header> <main> <p>A paragraph for the main content.</p> <p>And another one.</p> </main> <footer> <p>Here's some contact info</p> </footer></div>
注意,v-slot
只能添加在 <template>
上 (只有一种例外情况)
有时让插槽内容能够访问子组件中才有的数据是很有用的。当一个组件被用来渲染一个项目数组时,这是一个常见的情况,我们希望能够自定义每个项目的渲染方式。
例如,我们有一个组件,包含 todo-items 的列表。
app.component('todo-list', { data() { return { items: ['Feed a cat', 'Buy milk'] } }, template: ` <ul> <li v-for="(item, index) in items"> {{ item }} </li> </ul> `})
我们可能需要替换插槽以在父组件上自定义它:
<todo-list> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
但是,这是行不通的,因为只有 <todo-list>
组件可以访问 item
,我们将从其父组件提供槽内容。
要使 item
可用于父级提供的 slot 内容,我们可以添加一个 <slot>
元素并将其绑定为属性:
<ul> <li v-for="( item, index ) in items"> <slot :item="item"></slot> </li></ul>
绑定在 <slot
> 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot
来定义我们提供的插槽 prop 的名字:
<todo-list> <template v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </template></todo-list>
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps
,但你也可以使用任意你喜欢的名字。
在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot
直接用在组件上:
<todo-list v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span></todo-list>
这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot
被假定对应默认插槽:
<todo-list v-slot="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span></todo-list>
注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:
<!-- 无效,会导致警告 --><todo-list v-slot="slotProps"> <todo-list v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </todo-list> <template v-slot:other="otherSlotProps"> slotProps is NOT available here </template></todo-list>
只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template>
的语法:
<todo-list> <template v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </template> <template v-slot:other="otherSlotProps"> ... </template></todo-list>
作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:
function (slotProps) { // ... 插槽内容 ...}
这意味着 v-slot
的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:
<todo-list v-slot="{ item }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 item
重命名为 todo
:
<todo-list v-slot="{ item: todo }"> <i class="fas fa-check"></i> <span class="green">{{ todo }}</span></todo-list>
你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:
<todo-list v-slot="{ item = 'Placeholder' }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
动态指令参数也可以用在 v-slot
上,来定义动态的插槽名:
<base-layout> <template v-slot:[dynamicSlotName]> ... </template></base-layout>
跟 v-on
和 v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符 #
。例如 v-slot:header
可以被重写为 #header
:
<base-layout> <template #header> <h1>Here might be a page title</h1> </template> <template #default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template #footer> <p>Here's some contact info</p> </template></base-layout>
然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:
<!-- This will trigger a warning --><todo-list #="{ item }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:
<todo-list #default="{ item }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
通常,当我们需要将数据从父组件传递到子组件时,我们使用 props。想象一下这样的结构:你有一些深嵌套的组件,而你只需要来自深嵌套子组件中父组件的某些内容。在这种情况下,你仍然需要将 prop 传递到整个组件链中,这可能会很烦人。
对于这种情况,我们可以使用 provide
和 inject
对。父组件可以作为其所有子组件的依赖项提供程序,而不管组件层次结构有多深。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这个数据。
例如,如果我们有这样的层次结构:
Root└─ TodoList ├─ TodoItem └─ TodoListFooter ├─ ClearTodosButton └─ TodoListStatistics
如果要将 todo-items 的长度直接传递给 TodoListStatistics
,我们将把这个属性向下传递到层次结构:TodoList
-> TodoListFooter
-> TodoListStatistics
。通过 provide/inject 方法,我们可以直接执行以下操作:
const app = Vue.createApp({})app.component('todo-list', { data() { return { todos: ['Feed a cat', 'Buy tickets'] } }, provide: { user: 'John Doe' }, template: ` <div> {{ todos.length }} <!-- 模板的其余部分 --> </div> `})app.component('todo-list-statistics', { inject: ['user'], created() { console.log(`Injected property: ${this.user}`) // > 注入 property: John Doe }})
但是,如果我们尝试在此处提供一些组件实例 property,则这将不起作用:
app.component('todo-list', { data() { return { todos: ['Feed a cat', 'Buy tickets'] } }, provide: { todoLength: this.todos.length // 将会导致错误 'Cannot read property 'length' of undefined` }, template: ` ... `})
要访问组件实例 property,我们需要将 provide
转换为返回对象的函数
app.component('todo-list', { data() { return { todos: ['Feed a cat', 'Buy tickets'] } }, provide() { return { todoLength: this.todos.length } }, template: ` ... `})
这使我们能够更安全地继续开发该组件,而不必担心可能会更改/删除子组件所依赖的某些内容。这些组件之间的接口仍然是明确定义的,就像 prop 一样。
实际上,你可以将依赖注入看作是“long range props”,除了:
inject
property 来自哪里在上面的例子中,如果我们更改了 todos
的列表,这个更改将不会反映在注入的 todoLength
property 中。这是因为默认情况下,provide/inject
绑定不是被动绑定。我们可以通过将 ref
property 或 reactive
对象传递给 provide
来更改此行为。在我们的例子中,如果我们想对祖先组件中的更改做出反应,我们需要为我们提供的 todoLength
分配一个组合式 API computed
property:
app.component('todo-list', { // ... provide() { return { todoLength: Vue.computed(() => this.todos.length) } }})
在这种情况下,对 todos.length
将正确反映在组件中,其中“todoLength”被注入。在组合式 API 部分中阅读关于 reactive
provide/inject 的更多信息。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
keep-alive
我们之前曾经在一个多标签的界面中使用 is
attribute 来切换不同的组件:
<component :is="currentTabComponent"></component>
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。例如我们来展开说一说这个多标签界面:
你会注意到,如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent
实例。
重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 <keep-alive>
元素将其动态组件包裹起来。
<!-- 失活的组件将会被缓存!--><keep-alive> <component :is="currentTabComponent"></component></keep-alive>
来看看修改后的结果:
现在这个 Posts 标签保持了它的状态 (被选中的文章) 甚至当它未被渲染时也是如此。你可以在这个示例查阅到完整的代码。
你可以在 API 参考文档查阅更多关于 <keep-alive>
的细节。
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 有一个 defineAsyncComponent
方法:
const app = Vue.createApp({})const AsyncComp = Vue.defineAsyncComponent( () => new Promise((resolve, reject) => { resolve({ template: '<div>I am async!</div>' }) }))app.component('async-example', AsyncComp)
如你所见,此方法接受返回 Promise
的工厂函数。从服务器检索组件定义后,应调用 Promise 的 resolve
回调。你也可以调用 reject(reason)
,以指示加载失败。
你也可以在工厂函数中返回一个 Promise
,所以把 webpack 2 和 ES2015 语法加在一起,我们可以这样使用动态导入:
import { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent(() => import('./components/AsyncComponent.vue'))app.component('async-component', AsyncComp)
当在本地注册组件时,你也可以使用 defineAsyncComponent
import { createApp, defineAsyncComponent } from 'vue'createApp({ // ... components: { AsyncComponent: defineAsyncComponent(() => import('./components/AsyncComponent.vue') ) }})
异步组件在默认情况下是可挂起的。这意味着如果它在父链中有一个 <Suspense>
,它将被视为该 <Suspense>
的异步依赖。在这种情况下,加载状态将由 <Suspense>
控制,组件自身的加载、错误、延迟和超时选项将被忽略。
异步组件可以选择退出 Suspense
控制,并通过在其选项中指定 suspensable:false
,让组件始终控制自己的加载状态。
你可以在中查看可用选项的列表 API 参考
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
尽管存在 prop 和事件,但有时你可能仍然需要直接访问 JavaScript 中的子组件。为此,可以使用 ref
attribute 为子组件或 HTML 元素指定引用 ID。例如:
<input ref="input" />
例如,你希望以编程的方式 focus 这个 input 在组件上挂载,这可能有用
const app = Vue.createApp({})app.component('base-input', { template: ` <input ref="input" /> `, methods: { focusInput() { this.$refs.input.focus() } }, mounted() { this.focusInput() }})
此外,还可以向组件本身添加另一个 ref
,并使用它从父组件触发 focusInput
事件:
<base-input ref="usernameInput"></base-input>
this.$refs.usernameInput.focusInput()
当 ref
与 v-for
一起使用时,你得到的 ref
将是一个数组,其中包含镜像数据源的子组件。
WARNING
$refs
只会在组件渲染完成之后生效。这仅作为一个用于直接操作子元素的“逃生舱”——你应该避免在模板或计算属性中访问 $refs
。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
提示
这里记录的都是和处理边界情况有关的功能,即一些需要对 Vue 的规则做一些小调整的特殊情况。不过注意这些功能都是有劣势或危险的场景的。我们会在每个案例中注明,所以当你使用每个功能的时候请稍加留意。
得益于其响应性系统,Vue 总是知道何时更新 (如果你使用正确的话)。但是,在某些边缘情况下,你可能希望强制更新,尽管事实上没有任何响应式数据发生更改。还有一些情况下,你可能希望防止不必要的更新。
如果你发现自己需要在 Vue 中强制更新,在 99.99%的情况下,你在某个地方犯了错误。例如,你可能依赖于 Vue 响应性系统未跟踪的状态,例如,在组件创建之后添加了 data
属性。
但是,如果你已经排除了上述情况,并且发现自己处于这种非常罕见的情况下,必须手动强制更新,那么你可以使用 $forceUpdate
。
v-once
在 Vue 中渲染纯 HTML 元素的速度非常快,但有时你可能有一个包含很多静态内容的组件。在这些情况下,可以通过向根元素添加 v-once
指令来确保只对其求值一次,然后进行缓存,如下所示:
app.component('terms-of-service', { template: ` <div v-once> <h1>Terms of Service</h1> ... a lot of static content ... </div> `})
TIP
再次提醒,不要过度使用这种模式。虽然在极少数情况下需要渲染大量静态内容时很方便,但除非你注意到渲染速度——慢,否则就没有必要这样做—另外,这可能会在以后引起很多混乱。例如,假设另一个开发人员不熟悉 v-once
或者只是在模板中遗漏了它。他们可能会花上几个小时来弄清楚为什么模板没有正确更新。
Vue 提供了一些抽象概念,可以帮助处理过渡和动画,特别是在响应某些变化时。这些抽象的概念包括:
<transition>
组件来钩住组件中进入和离开 DOM。<transition-group>
组件,通过 FLIP 技术来提高性能。watchers
来处理应用中不同状态的过渡。我们将在本指南接下来的三个部分中介绍所有这些以及更多内容。然而,除了提供这些有用的 API 之外,值得一提的是,我们前面介绍的 class 和 style 声明也可以应用于动画和过渡,用于更简单的用例。
在下一节中,我们将回顾一些 web 动画和过渡的基础知识,并提供一些资源链接以进行进一步的研究。如果你已经熟悉 web 动画,并且了解这些原理如何与 Vue 的某些指令配合使用,可以跳过这一节。对于希望在开始学习之前进一步了解网络动画基础知识的其他人,请继续阅读。
尽管 <transition>
组件对于组件的进入和离开非常有用,但你也可以通过添加一个条件 class 来激活动画,而无需挂载组件。
<div id="demo"> Push this button to do something you shouldn't be doing:<br /> <div :class="{ shake: noActivated }"> <button @click="noActivated = true">Click me</button> <span v-if="noActivated">Oh no!</span> </div></div>
.shake { animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; transform: translate3d(0, 0, 0); backface-visibility: hidden; perspective: 1000px;}@keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); }}
const Demo = { data() { return { noActivated: false } }}Vue.createApp(Demo).mount('#demo')
一些过渡效果可以通过插值的方式来实现,例如在发生交互时将样式绑定到元素上。以这个例子为例:
<div id="demo"> <div @mousemove="xCoordinate" :style="{ backgroundColor: `hsl(${x}, 80%, 50%)` }" class="movearea" > <h3>Move your mouse across the screen...</h3> <p>x: {{x}}</p> </div></div>
.movearea { transition: 0.2s background-color ease;}
const Demo = { data() { return { x: 0 } }, methods: { xCoordinate(e) { this.x = e.clientX } }}Vue.createApp(Demo).mount('#demo')
在这个例子中,我们是通过使用插值来创建动画,将触发条件添加到鼠标的移动过程上。同时将 CSS 过渡属性应用在元素上,让元素知道在更新时要使用什么过渡效果。
你可能注意到上面显示的动画使用了 transforms
之类的东西,并应用了诸如 perspective
之类的奇怪的 property——为什么它们是这样实现的,而不是仅仅使用 margin
和 top
等?
我们可以通过对性能的了解,在 web 上创建极其流畅的动画。我们希望尽可能对元素动画进行硬件加速,并使用不触发重绘的 property。我们来介绍一下如何实现这个目标。
我们可以通过工具,例如 CSS Triggers 来查看哪些属性会在动画时触发重绘。在工具中,查看 transform
的相关内容,你将看到:
非常好的是,更改 transform 不会触发任何几何形状变化或绘制。这意味着该操作可能是由合成器线程在 GPU 的帮助下执行。
opacity
属性的行为也类似。因此,他们是在 web 上做元素移动的理想选择。
诸如 perspective
、backface-visibility
和 transform:translateZ(x)
等 property 将让浏览器知道你需要硬件加速。
如果要对一个元素进行硬件加速,可以应用以下任何一个 property (并不是需要全部,任意一个就可以):
perspective: 1000px;backface-visibility: hidden;transform: translateZ(0);
许多像 GreenSock 这样的 JS 库都会默认你需要硬件加速,并在默认情况下应用,所以你不需要手动设置它们。
对于简单 UI 过渡,即从一个状态到另一个没有中间状态的状态,通常使用 0.1s 到 0.4s 之间的计时,大多数人发现 0.25s 是一个最佳选择。你能用这个定时做任何事情吗?并不是。如果你有一些元素需要移动更大的距离,或者有更多的步骤或状态变化,0.25s 并不会有很好的效果,你将不得不有更多的目的性,而且定时也需要更加独特。但这并不意味着你不能在应用中重复使用效果好的默认值。
你也可能会发现,起始动画比结束动画的时间稍长一些,看起来会更好一些。用户通常是在动画开始时被引导的,而在动画结束时没有那么多耐心,因为他们想继续他们的动作。
Easing 是在动画中表达深度的一个重要方式。动画新手最常犯的一个错误是在起始动画节点使用 ease-in
,在结束动画节点使用 ease-out
。实际上你需要的是反过来的。
如果我们将这些状态应用于过渡,它应该像这样:
.button { background: #1b8f5a; /* 应用于初始状态,因此此转换将应用于返回状态 */ transition: background 0.25s ease-in;}.button:hover { background: #3eaf7c; /* 应用于悬停状态,因此在触发悬停时将应用此过渡 */ transition: background 0.35s ease-out;}
Easing 也可以表达动画元素的质量。以下面的 Pen 为例,你认为哪个球是硬的,哪个球是软的?
你可以通过调整你的 Easing 来获得很多独特的效果,使你的动画非常时尚。CSS 允许你通过调整 cubic bezier 属性来修改 Easing,Lea Verou 开发的这个 playground 对探索这个问题非常有帮助。
虽然使用 cubic-bezier ease 提供的两个控制柄可以为简单的动画获得很好的效果,但是 JavaScript 允许多个控制柄,以此支持更多的变化。
以弹跳为例。在 CSS 中,我们必须声明向上和向下的每个关键帧。在 JavaScript 中,我们可以通过在 greensock API(GSAP) 中声明 bounce
来描述 ease 中所有这些移动 (其他 JS 库有其他类型的 easing 默认值)。
这里是 CSS 中用来实现 bounce 的代码 (来自 animate.css 的例子):
@keyframes bounceInDown { from, 60%, 75%, 90%, to { animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; transform: translate3d(0, -3000px, 0) scaleY(3); } 60% { opacity: 1; transform: translate3d(0, 25px, 0) scaleY(0.9); } 75% { transform: translate3d(0, -10px, 0) scaleY(0.95); } 90% { transform: translate3d(0, 5px, 0) scaleY(0.985); } to { transform: translate3d(0, 0, 0); }}.bounceInDown { animation-name: bounceInDown;}
下面是 JS 中使用 GreenSock 实现相同的 bounce:
gsap.from(element, { duration: 1, ease: 'bounce.out', y: -500 })
我们将在之后章节的部分示例中使用 GreenSock。他们有一个很棒的 ease visualizer,帮助你建立精心制作的画架。
在插入、更新或从 DOM 中移除项时,Vue 提供了多种应用转换效果的方法。这包括以下工具:
在这里,我们只会讲到进入、离开和列表的过渡,你也可以看下一节的管理过渡状态 。
Vue 提供了 transition
的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡
v-if
)v-show
)这里是一个典型的例子:
<div id="demo"> <button @click="show = !show"> Toggle </button> <transition name="fade"> <p v-if="show">hello</p> </transition></div>
const Demo = { data() { return { show: true } }}Vue.createApp(Demo).mount('#demo')
.fade-enter-active,.fade-leave-active { transition: opacity 0.5s ease;}.fade-enter-from,.fade-leave-to { opacity: 0;}
当插入或删除包含在 transition
组件中的元素时,Vue 将会做以下处理:
nextTick
概念不同)在进入/离开的过渡中,会有 6 个 class 切换。
v-enter-from
:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。v-enter-active
:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。v-enter-to
:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from
被移除),在过渡/动画完成之后移除。v-leave-from
:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。v-leave-active
:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。v-leave-to
:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from
被删除),在过渡/动画完成之后移除。对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 <transition>
,则 v-
是这些class名的默认前缀。如果你使用了 <transition name="my-transition">
,那么 v-enter-from
会替换为 my-transition-enter-from
。
v-enter-active
和 v-leave-active
可以控制进入/离开过渡的不同的缓和曲线,在下面章节会有个示例说明。
常用的过渡都是使用 CSS 过渡。
<div id="demo"> <button @click="show = !show"> Toggle render </button> <transition name="slide-fade"> <p v-if="show">hello</p> </transition></div>
const Demo = { data() { return { show: true } }}Vue.createApp(Demo).mount('#demo')
/* 可以设置不同的进入和离开动画 *//* 设置持续时间和动画函数 */.slide-fade-enter-active { transition: all 0.3s ease-out;}.slide-fade-leave-active { transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);}.slide-fade-enter-from,.slide-fade-leave-to { transform: translateX(20px); opacity: 0;}
CSS 动画用法同 CSS 过渡,区别是在动画中 v-enter-from
类名在节点插入 DOM 后不会立即删除,而是在 animationend
事件触发时删除。
下面是一个例子,为了简洁起见,省略了带前缀的 CSS 规则:
<div id="demo"> <button @click="show = !show">Toggle show</button> <transition name="bounce"> <p v-if="show"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus. </p> </transition></div>
const Demo = { data() { return { show: true } }}Vue.createApp(Demo).mount('#demo')
.bounce-enter-active { animation: bounce-in 0.5s;}.bounce-leave-active { animation: bounce-in 0.5s reverse;}@keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.25); } 100% { transform: scale(1); }}
我们可以通过以下 attribute 来自定义过渡类名:
enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css. 结合使用十分有用。
示例:
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css" rel="external nofollow" target="_blank" rel="stylesheet" type="text/css"/><div id="demo"> <button @click="show = !show"> Toggle render </button> <transition name="custom-classes-transition" enter-active-class="animate__animated animate__tada" leave-active-class="animate__animated animate__bounceOutRight" > <p v-if="show">hello</p> </transition></div>
const Demo = { data() { return { show: true } }}Vue.createApp(Demo).mount('#demo')
Vue 为了知道过渡的完成,必须设置相应的事件监听器。它可以是 transitionend
或 animationend
,这取决于给元素应用的 CSS 规则。如果你使用其中任何一种,Vue 能自动识别类型并设置监听。
但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如 animation 很快的被触发并完成了,而 transition 效果还没结束。在这种情况中,你就需要使用 type
attribute 并设置 animation
或 transition
来明确声明你需要 Vue 监听的类型。
在很多情况下,Vue 可以自动得出过渡效果的完成时机。默认情况下,Vue 会等待其在过渡效果的根元素的第一个 transitionend
或 animationend
事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果。
在这种情况下你可以用 <transition>
组件上的 duration
prop 定制一个显性的过渡持续时间 (以毫秒计):
<transition :duration="1000">...</transition>
你也可以定制进入和移出的持续时间:
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
可以在 attribute 中声明 JavaScript 钩子
<transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @enter-cancelled="enterCancelled" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" @leave-cancelled="leaveCancelled" :css="false"> <!-- ... --></transition>
// ...methods: { // -------- // ENTERING // -------- beforeEnter(el) { // ... }, // 当与 CSS 结合使用时 // 回调函数 done 是可选的 enter(el, done) { // ... done() }, afterEnter(el) { // ... }, enterCancelled(el) { // ... }, // -------- // 离开时 // -------- beforeLeave(el) { // ... }, // 当与 CSS 结合使用时 // 回调函数 done 是可选的 leave(el, done) { // ... done() }, afterLeave(el) { // ... }, // leaveCancelled 只用于 v-show 中 leaveCancelled(el) { // ... }}
这些钩子函数可以结合 CSS transitions/animations 使用,也可以单独使用。
当只用 JavaScript 过渡的时候,在 enter
和 leave
钩中必须使用 done
进行回调。否则,它们将被同步调用,过渡会立即完成。添加 :css="false"
,也会让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。
现在让我们来看一个例子。下面是一个使用 GreenSock 的 JavaScript 过渡:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js" rel="external nofollow" ></script><div id="demo"> <button @click="show = !show"> Toggle </button> <transition @before-enter="beforeEnter" @enter="enter" @leave="leave" :css="false" > <p v-if="show"> Demo </p> </transition></div>
const Demo = { data() { return { show: false } }, methods: { beforeEnter(el) { gsap.set(el, { scaleX: 0.8, scaleY: 1.2 }) }, enter(el, done) { gsap.to(el, { duration: 1, scaleX: 1.5, scaleY: 0.7, opacity: 1, x: 150, ease: 'elastic.inOut(2.5, 1)', onComplete: done }) }, leave(el, done) { gsap.to(el, { duration: 0.7, scaleX: 1, scaleY: 1, x: 300, ease: 'elastic.inOut(2.5, 1)' }) gsap.to(el, { duration: 0.2, delay: 0.5, opacity: 0, onComplete: done }) } }}Vue.createApp(Demo).mount('#demo')
可以通过 appear
attribute 设置节点在初始渲染的过渡
<transition appear> <!-- ... --></transition>
我们之后讨论多个组件的过渡,对于原生标签可以使用 v-if
/v-else
。最常见的多标签过渡是一个列表和描述这个列表为空消息的元素:
<transition> <table v-if="items.length > 0"> <!-- ... --> </table> <p v-else>Sorry, no items found.</p></transition>
实际上,通过使用多个 v-if
或将单个元素绑定到一个动态 property,可以在任意数量的元素之间进行过渡。例如:
<transition> <button v-if="docState === 'saved'" key="saved"> Edit </button> <button v-if="docState === 'edited'" key="edited"> Save </button> <button v-if="docState === 'editing'" key="editing"> Cancel </button></transition>
也可以写为:
<transition> <button :key="docState"> {{ buttonMessage }} </button></transition>
// ...computed: { buttonMessage() { switch (this.docState) { case 'saved': return 'Edit' case 'edited': return 'Save' case 'editing': return 'Cancel' } }}
这里还有一个问题,试着点击下面的按钮:
在“on”按钮和“off”按钮的过渡中,两个按钮都被重绘了,一个离开过渡的时候另一个开始进入过渡。这是 <transition>
的默认行为 —— 进入和离开同时发生。
有时这很有效,例如当过渡项绝对位于彼此的 top 时:
同时生效的进入和离开的过渡不能满足所有要求,所以 Vue 提供了过渡模式
in-out
: 新元素先进行过渡,完成之后当前元素过渡离开。out-in
: 当前元素先进行过渡,完成之后新元素过渡进入。TIP
很快就会发现 out-in
是你大多数时候想要的状态 ????
现在让我们用 out-in
更新 on/off 按钮的转换:
<transition name="fade" mode="out-in"> <!-- ... the buttons ... --></transition>
通过添加一个 attribute,我们修复了原来的过渡,而不必添加任何特殊 style。
我们可以用它来协调更具表现力的动作,例如折叠卡片,如下所示。实际上是两个元素在彼此之间转换,但是由于开始状态和结束状态的比例是相同的:水平为0,它看起来就像一个流体运动。这种轻描淡写对于真实的 UI 微交互非常有用:
组件之间的过渡更简单 —— 我们甚至不需要 key
属性。相反,我们包装了一个动态组件 :
<div id="demo"> <input v-model="view" type="radio" value="v-a" id="a"><label for="a">A</label> <input v-model="view" type="radio" value="v-b" id="b"><label for="b">B</label> <transition name="component-fade" mode="out-in"> <component :is="view"></component> </transition></div>
const Demo = { data() { return { view: 'v-a' } }, components: { 'v-a': { template: '<div>Component A</div>' }, 'v-b': { template: '<div>Component B</div>' } }}Vue.createApp(Demo).mount('#demo')
.component-fade-enter-active,.component-fade-leave-active { transition: opacity 0.3s ease;}.component-fade-enter-from,.component-fade-leave-to { opacity: 0;}
目前为止,关于过渡我们已经讲到:
那么怎么同时渲染整个列表,比如使用 v-for
?在这种场景中,使用 <transition-group>
组件。在我们深入例子之前,先了解关于这个组件的几个特点:
<transition>
,它会以一个真实元素渲染:默认为一个 <span>
。你也可以通过 tag
attribute 更换为其他元素。key
attribute 值。现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 CSS class 名。
<div id="list-demo"> <button @click="add">Add</button> <button @click="remove">Remove</button> <transition-group name="list" tag="p"> <span v-for="item in items" :key="item" class="list-item"> {{ item }} </span> </transition-group></div>
const Demo = { data() { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 } }, methods: { randomIndex() { return Math.floor(Math.random() * this.items.length) }, add() { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove() { this.items.splice(this.randomIndex(), 1) } }}Vue.createApp(Demo).mount('#list-demo')
.list-item { display: inline-block; margin-right: 10px;}.list-enter-active,.list-leave-active { transition: all 1s ease;}.list-enter-from,.list-leave-to { opacity: 0; transform: translateY(30px);}
这个例子有个问题,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。
<transition-group>
组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 v-move
class,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过 name
attribute 来自定义前缀,也可以通过 move-class
attribute 手动设置。
该 class 主要用于指定过渡 timing 和 easing 曲线,如下所示:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" rel="external nofollow" ></script><div id="flip-list-demo"> <button @click="shuffle">Shuffle</button> <transition-group name="flip-list" tag="ul"> <li v-for="item in items" :key="item"> {{ item }} </li> </transition-group></div>
const Demo = { data() { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9] } }, methods: { shuffle() { this.items = _.shuffle(this.items) } }}Vue.createApp(Demo).mount('#flip-list-demo')
.flip-list-move { transition: transform 0.8s ease;}
这个看起来很神奇,内部的实现,Vue 使用了一个叫 FLIP 简单的动画队列使用 transforms 将元素从之前的位置平滑过渡新的位置。
我们将之前实现的例子和这个技术结合,使我们列表的一切变动都会有动画过渡。
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js" rel="external nofollow" ></script><div id="list-complete-demo" class="demo"> <button @click="shuffle">Shuffle</button> <button @click="add">Add</button> <button @click="remove">Remove</button> <transition-group name="list-complete" tag="p"> <span v-for="item in items" :key="item" class="list-complete-item"> {{ item }} </span> </transition-group></div>
const Demo = { data() { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 } }, methods: { randomIndex() { return Math.floor(Math.random() * this.items.length) }, add() { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove() { this.items.splice(this.randomIndex(), 1) }, shuffle() { this.items = _.shuffle(this.items) } }}Vue.createApp(Demo).mount('#list-complete-demo')
.list-complete-item { transition: all 0.8s ease; display: inline-block; margin-right: 10px;}.list-complete-enter-from,.list-complete-leave-to { opacity: 0; transform: translateY(30px);}.list-complete-leave-active { position: absolute;}
TIP
需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline
。作为替代方案,可以设置为 display: inline-block
或者放置于 flex 中
FLIP 动画不仅可以实现单列过渡,多维网格也同样可以过渡:
TODO:示例
通过 data attribute 与 JavaScript 通信,就可以实现列表的交错过渡:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js" rel="external nofollow" ></script><div id="demo"> <input v-model="query" /> <transition-group name="staggered-fade" tag="ul" :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave" > <li v-for="(item, index) in computedList" :key="item.msg" :data-index="index" > {{ item.msg }} </li> </transition-group></div>
const Demo = { data() { return { query: '', list: [ { msg: 'Bruce Lee' }, { msg: 'Jackie Chan' }, { msg: 'Chuck Norris' }, { msg: 'Jet Li' }, { msg: 'Kung Fury' } ] } }, computed: { computedList() { var vm = this return this.list.filter(item => { return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1 }) } }, methods: { beforeEnter(el) { el.style.opacity = 0 el.style.height = 0 }, enter(el, done) { gsap.to(el, { opacity: 1, height: '1.6em', delay: el.dataset.index * 0.15, onComplete: done }) }, leave(el, done) { gsap.to(el, { opacity: 0, height: 0, delay: el.dataset.index * 0.15, onComplete: done }) } }}Vue.createApp(Demo).mount('#demo')
过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将 <transition>
或者 <transition-group>
作为根组件,然后将任何子组件放置在其中就可以了。
TODO:使用 Vue3 重构
使用 template 的简单例子:
Vue.component('my-special-transition', { template: ' <transition name="very-special-transition" mode="out-in" @before-enter="beforeEnter" @after-enter="afterEnter" > <slot></slot> </transition> ', methods: { beforeEnter(el) { // ... }, afterEnter(el) { // ... } }})
函数式组件更适合完成这个任务:
Vue.component('my-special-transition', { functional: true, render: function(createElement, context) { var data = { props: { name: 'very-special-transition', mode: 'out-in' }, on: { beforeEnter(el) { // ... }, afterEnter(el) { // ... } } } return createElement('transition', data, context.children) }})
在 Vue 中即使是过渡也是数据驱动的!动态过渡最基本的例子是通过 name
attribute 来绑定动态值。
<transition :name="transitionName"> <!-- ... --></transition>
当你想用 Vue 的过渡系统来定义的 CSS 过渡/动画在不同过渡间切换会非常有用。
所有过渡 attribute 都可以动态绑定,但我们不仅仅只有 attribute 可以利用,还可以通过事件钩子获取上下文中的所有数据,因为事件钩子都是方法。这意味着,根据组件的状态不同,你的 JavaScript 过渡会有不同的表现
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js" rel="external nofollow" ></script><div id="dynamic-fade-demo" class="demo"> Fade In: <input type="range" v-model="fadeInDuration" min="0" :max="maxFadeDuration" /> Fade Out: <input type="range" v-model="fadeOutDuration" min="0" :max="maxFadeDuration" /> <transition :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave" > <p v-if="show">hello</p> </transition> <button v-if="stop" @click="stop = false; show = false"> Start animating </button> <button v-else @click="stop = true">Stop it!</button></div>
const app = Vue.createApp({ data() { return { show: true, fadeInDuration: 1000, fadeOutDuration: 1000, maxFadeDuration: 1500, stop: true } }, mounted() { this.show = false }, methods: { beforeEnter(el) { el.style.opacity = 0 }, enter(el, done) { var vm = this Velocity( el, { opacity: 1 }, { duration: this.fadeInDuration, complete: function() { done() if (!vm.stop) vm.show = false } } ) }, leave(el, done) { var vm = this Velocity( el, { opacity: 0 }, { duration: this.fadeOutDuration, complete: function() { done() vm.show = true } } ) } }})app.mount('#dynamic-fade-demo')
TODO:示例
最后,创建动态过渡的最终方案是组件通过接受 props 来动态修改之前的过渡。一句老话,唯一的限制是你的想象力。
Vue 的过渡系统提供了非常多简单的方法设置进入、离开和列表的动效。那么对于数据元素本身的动效呢,比如:
这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,我们就可以结合 Vue 的响应性和组件系统,使用第三方库来实现切换元素的过渡状态。
通过侦听器我们能监听到任何数值 property 的数值更新。可能听起来很抽象,所以让我们先来看看使用 GreenSock 一个例子:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js" rel="external nofollow" rel="external nofollow" ></script><div id="animated-number-demo"> <input v-model.number="number" type="number" step="20" /> <p>{{ animatedNumber }}</p></div>
const Demo = { data() { return { number: 0, tweenedNumber: 0 } }, computed: { animatedNumber() { return this.tweenedNumber.toFixed(0) } }, watch: { number(newValue) { gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue }) } }}Vue.createApp(Demo).mount('#animated-number-demo')
更新数字时,更改将在输入下方设置动画。
就像 Vue 的过渡组件一样,数据背后状态过渡会实时更新,这对于原型设计十分有用。当你修改一些变量,即使是一个简单的 SVG 多边形也可实现很多难以想象的效果。
管理太多的状态过渡会很快的增加组件实例复杂性,幸好很多的动画可以提取到专用的子组件。我们来将之前的示例改写一下:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js" rel="external nofollow" rel="external nofollow" ></script><div id="app"> <input v-model.number="firstNumber" type="number" step="20" /> + <input v-model.number="secondNumber" type="number" step="20" /> = {{ result }} <p> <animated-integer :value="firstNumber"></animated-integer> + <animated-integer :value="secondNumber"></animated-integer> = <animated-integer :value="result"></animated-integer> </p></div>
const app = Vue.createApp({ data() { return { firstNumber: 20, secondNumber: 40 } }, computed: { result() { return this.firstNumber + this.secondNumber } }})app.component('animated-integer', { template: '<span>{{ fullValue }}</span>', props: { value: { type: Number, required: true } }, data() { return { tweeningValue: 0 } }, computed: { fullValue() { return Math.floor(this.tweeningValue) } }, methods: { tween(newValue, oldValue) { gsap.to(this.$data, { duration: 0.5, tweeningValue: newValue, ease: 'sine' }) } }, watch: { value(newValue, oldValue) { this.tween(newValue, oldValue) } }, mounted() { this.tween(this.value, 0) }})app.mount('#app')
我们能在组件中结合使用这一节讲到各种过渡策略和 Vue 内建的过渡系统。总之,对于完成各种过渡动效几乎没有阻碍。
你可以看到我们如何使用它进行数据可视化,物理效果,角色动画和交互,天空是极限。
只要一个动画,就可以带来生命。不幸的是,当设计师创建图标、logo 和吉祥物的时候,他们交付的通常都是图片或静态的 SVG。所以,虽然 GitHub 的章鱼猫、Twitter 的小鸟以及其它许多 logo 类似于生灵,它们看上去实际上并不是活着的。
Vue 可以帮到你。因为 SVG 的本质是数据,我们只需要这些动物兴奋、思考或警戒的样例。然后 Vue 就可以辅助完成这几种状态之间的过渡动画,来制作你的欢迎页面、加载指示、以及更加带有情感的提示。
Sarah Drasner 展示了下面这个 demo,这个 demo 结合了时间和交互相关的状态改变:
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
例子:
// define a mixin objectconst myMixin = { created() { this.hello() }, methods: { hello() { console.log('hello from mixin!') } }}// define an app that uses this mixinconst app = Vue.createApp({ mixins: [myMixin]})app.mount('#mixins-basic') // => "hello from mixin!"
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
const myMixin = { data() { return { message: 'hello', foo: 'abc' } }}const app = Vue.createApp({ mixins: [myMixin], data() { return { message: 'goodbye', bar: 'def' } }, created() { console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" } }})
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
const myMixin = { created() { console.log('mixin hook called') }}const app = Vue.createApp({ mixins: [myMixin], created() { console.log('component hook called') }})// => "混入对象的钩子被调用"// => "组件钩子被调用"
值为对象的选项,例如 methods
、components
和 directives
,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
const myMixin = { methods: { foo() { console.log('foo') }, conflicting() { console.log('from mixin') } }}const app = Vue.createApp({ mixins: [myMixin], methods: { bar() { console.log('bar') }, conflicting() { console.log('from self') } }})const vm = app.mount('#mixins-basic')vm.foo() // => "foo"vm.bar() // => "bar"vm.conflicting() // => "from self"
你还可以为 Vue 应用程序全局应用 mixin:
const app = Vue.createApp({ myOption: 'hello!'})// 为自定义的选项 'myOption' 注入一个处理器。app.mixin({ created() { const myOption = this.$options.myOption if (myOption) { console.log(myOption) } }})app.mount('#mixins-global') // => "hello!"
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的组件 (例如,每个子组件)。
const app = Vue.createApp({ myOption: 'hello!'})// 为自定义的选项 'myOption' 注入一个处理器。app.mixin({ created() { const myOption = this.$options.myOption if (myOption) { console.log(myOption) } }})// 将myOption也添加到子组件app.component('test-component', { myOption: 'hello from component!'})app.mount('#mixins-global')// => "hello!"// => "hello from component!"
大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件发布,以避免重复应用混入。
自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 app.config.optionMergeStrategies
添加一个函数:
const app = Vue.createApp({})app.config.optionMergeStrategies.customOption = (toVal, fromVal) => { // return mergedVal}
合并策略接收在父实例和子实例上定义的该选项的值,分别作为第一个和第二个参数。让我们来检查一下使用 mixin 时,这些参数有哪些:
const app = Vue.createApp({ custom: 'hello!'})app.config.optionMergeStrategies.custom = (toVal, fromVal) => { console.log(fromVal, toVal) // => "goodbye!", undefined // => "hello", "goodbye!" return fromVal || toVal}app.mixin({ custom: 'goodbye!', created() { console.log(this.$options.custom) // => "hello!" }})
如你所见,在控制台中,我们先从 mixin 打印 toVal
和 fromVal
,然后从 app
打印。如果存在,我们总是返回 fromVal
,这就是为什么 this.$options.custom
设置为 你好!
最后。让我们尝试将策略更改为始终从子实例返回值:
const app = Vue.createApp({ custom: 'hello!'})app.config.optionMergeStrategies.custom = (toVal, fromVal) => toVal || fromValapp.mixin({ custom: 'goodbye!', created() { console.log(this.$options.custom) // => "goodbye!" }})
在 Vue 2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:
为了解决这些问题,我们添加了一种通过逻辑关注点组织代码的新方法:组合式 API。
除了核心功能默认内置的指令 (v-model
和 v-show
),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。举个聚焦输入框的例子,如下:
当页面加载时,该元素将获得焦点 (注意:autofocus
在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。此外,你可以单击 Rerun
按钮,输入将被聚焦。
现在让我们用指令来实现这个功能:
const app = Vue.createApp({})// 注册一个全局自定义指令 `v-focus`app.directive('focus', { // 当被绑定的元素插入到 DOM 中时…… mounted(el) { // Focus the element el.focus() }})
如果想注册局部指令,组件中也接受一个 directives
的选项:
directives: { focus: { // 指令的定义 mounted(el) { el.focus() } }}
然后你可以在模板中任何元素上使用新的 v-focus
property,如下:
<input v-focus />
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
beforeMount
:当指令第一次绑定到元素并且在挂载父组件之前调用。在这里你可以做一次性的初始化设置。mounted
:在挂载绑定元素的父组件时调用。beforeUpdate
:在更新包含组件的 VNode 之前调用。提示
我们会在稍后讨论渲染函数时介绍更多 VNodes 的细节。
updated
:在包含组件的 VNode 及其子组件的 VNode 更新后调用。beforeUnmount
:在卸载绑定元素的父组件之前调用unmounted
:当指令与元素解除绑定且父组件已卸载时,只调用一次。接下来我们来看一下在自定义指令 API 钩子函数的参数 (即 el
、binding
、vnode
和 prevNnode
)
指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value"
中,argument
参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。
例如你想要创建一个自定义指令,用来通过固定布局将元素固定在页面上。我们可以像这样创建一个通过指令值来更新竖直位置像素值的自定义指令:
<div id="dynamic-arguments-example" class="demo"> <p>Scroll down the page</p> <p v-pin="200">Stick me 200px from the top of the page</p></div>
const app = Vue.createApp({})app.directive('pin', { mounted(el, binding) { el.style.position = 'fixed' // binding.value is the value we pass to directive - in this case, it's 200 el.style.top = binding.value + 'px' }})app.mount('#dynamic-arguments-example')
这会把该元素固定在距离页面顶部 200 像素的位置。但如果场景是我们需要把元素固定在左侧而不是顶部又该怎么办呢?这时使用动态参数就可以非常方便地根据每个组件实例来进行更新。
<div id="dynamicexample"> <h3>Scroll down inside this section ↓</h3> <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p></div>
const app = Vue.createApp({ data() { return { direction: 'right' } }})app.directive('pin', { mounted(el, binding) { el.style.position = 'fixed' // binding.arg is an argument we pass to directive const s = binding.arg || 'top' el.style[s] = binding.value + 'px' }})app.mount('#dynamic-arguments-example')
结果:
我们的定制指令现在已经足够灵活,可以支持一些不同的用例。为了使其更具动态性,我们还可以允许修改绑定值。让我们创建一个附加属性 pinPadding
,并将其绑定到 <input type="range">
。
<div id="dynamicexample"> <h2>Scroll down the page</h2> <input type="range" min="0" max="500" v-model="pinPadding"> <p v-pin:[direction]="pinPadding">Stick me {{ pinPadding + 'px' }} from the {{ direction }} of the page</p></div>
const app = Vue.createApp({ data() { return { direction: 'right', pinPadding: 200 } }})
让我们扩展我们的指令逻辑来重新计算固定元件更新的距离。
app.directive('pin', { mounted(el, binding) { el.style.position = 'fixed' const s = binding.arg || 'top' el.style[s] = binding.value + 'px' }, updated(el, binding) { const s = binding.arg || 'top' el.style[s] = binding.value + 'px' }})
结果:
在很多时候,你可能想在 mounted
和 updated
时触发相同行为,而不关心其它的钩子。比如这样写:
app.directive('pin', (el, binding) => { el.style.position = 'fixed' const s = binding.arg || 'top' el.style[s] = binding.value + 'px'})
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => { console.log(binding.value.color) // => "white" console.log(binding.value.text) // => "hello!"})
在 3.0 中,有了片段支持,组件可能有多个根节点。如果在具有多个根节点的组件上使用自定义指令,则会产生问题。
要解释自定义指令如何在 3.0 中的组件上工作的详细信息,我们首先需要了解自定义指令在 3.0 中是如何编译的。对于这样的指令:
<div v-demo="test"></div>
将大概编译成:
const vDemo = resolveDirective('demo')return withDirectives(h('div'), [[vDemo, test]])
其中 vDemo
是用户编写的指令对象,其中包含 mounted
和 updated
等钩子。
withDirectives
返回一个克隆的 VNode,其中用户钩子被包装并作为 VNode 生命周期钩子注入 (请参见渲染函数更多详情):
{ onVnodeMounted(vnode) { // call vDemo.mounted(...) }}
因此,自定义指令作为 VNode 数据的一部分完全包含在内。当在组件上使用自定义指令时,这些 onVnodeXXX
钩子作为无关的 prop 传递给组件,并以 this.$attrs
结束。
这也意味着可以像这样在模板中直接挂接到元素的生命周期中,这在涉及到自定义指令时非常方便:
<div @vnodeMounted="myHook" />
这和 非 prop 的 attribute类似。因此,组件上自定义指令的规则将与其他无关 attribute 相同:由子组件决定在哪里以及是否应用它。当子组件在内部元素上使用 v-bind="$attrs"
时,它也将应用对其使用的任何自定义指令。
Vue 鼓励我们通过将 UI 和相关行为封装到组件中来构建 UI。我们可以将它们嵌套在另一个内部,以构建一个组成应用程序 UI 的树。
然而,有时组件模板的一部分逻辑上属于该组件,而从技术角度来看,最好将模板的这一部分移动到 DOM 中 Vue app 之外的其他位置。
一个常见的场景是创建一个包含全屏模式的组件。在大多数情况下,你希望模态框的逻辑存在于组件中,但是模态框的快速定位就很难通过 CSS 来解决,或者需要更改组件组合。
考虑下面的 HTML 结构。
<body> <div style="position: relative;"> <h3>Tooltips with Vue 3 Teleport</h3> <div> <modal-button></modal-button> </div> </div></body>
让我们来看看 modal-button
组件:
该组件将有一个 button
元素来触发模态的打开,以及一个具有类 .modal
的 div
元素,它将包含模态的内容和一个用于自关闭的按钮。
const app = Vue.createApp({});app.component('modal-button', { template: ` <button @click="modalOpen = true"> Open full screen modal! </button> <div v-if="modalOpen" class="modal"> <div> I'm a modal! <button @click="modalOpen = false"> Close </button> </div> </div> `, data() { return { modalOpen: false } }})
当在初始的 HTML 结构中使用这个组件时,我们可以看到一个问题——模态是在深度嵌套的 div
中渲染的,而模态的 position:absolute
以父级相对定位的 div
作为引用。
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件。
让我们修改 modal-button
以使用 <teleport>
,并告诉 Vue “Teleport 这个 HTML 到该‘body’标签”。
app.component('modal-button', { template: ` <button @click="modalOpen = true"> Open full screen modal! (With teleport!) </button> <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> `, data() { return { modalOpen: false } }})
因此,一旦我们单击按钮打开模式,Vue 将正确地将模态内容渲染为 body
标签的子级。
如果 <teleport>
包含 Vue 组件,则它仍将是 <teleport>
父组件的逻辑子组件:
const app = Vue.createApp({ template: ` <h1>Root instance</h1> <parent-component /> `})app.component('parent-component', { template: ` <h2>This is a parent component</h2> <teleport to="#endofbody"> <child-component name="John" /> </teleport> `})app.component('child-component', { props: ['name'], template: ` <div>Hello, {{ name }}</div> `})
在这种情况下,即使在不同的地方渲染 child-component
,它仍将是 parent-component
的子级,并将从中接收 name
prop。
这也意味着来自父组件的注入按预期工作,并且子组件将嵌套在 Vue Devtools 中的父组件之下,而不是放在实际内容移动到的位置。
一个常见的用例场景是一个可重用的 <Modal>
组件,它可能同时有多个实例处于活动状态。对于这种情况,多个 <teleport>
组件可以将其内容挂载到同一个目标元素。顺序将是一个简单的追加——稍后挂载将位于目标元素中较早的挂载之后。
<teleport to="#modals"> <div>A</div></teleport><teleport to="#modals"> <div>B</div></teleport><!-- result--><div id="modals"> <div>A</div> <div>B</div></div>
你可以在 API 参考 查看 teleport
组件。
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
让我们深入一个简单的例子,这个例子里 render
函数很实用。假设我们要生成一些带锚点的标题:
<h1> <a name="hello-world" href="#hello-world"> Hello world! </a></h1>
锚点标题的使用非常频繁,我们应该创建一个组件:
<anchored-heading :level="1">Hello world!</anchored-heading>
当开始写一个只能通过 level
prop 动态生成标题 (heading) 的组件时,我们很快就可以得出这样的结论:
const app = Vue.createApp({})app.component('anchored-heading', { template: ` <h1 v-if="level === 1"> <slot></slot> </h1> <h2 v-else-if="level === 2"> <slot></slot> </h2> <h3 v-else-if="level === 3"> <slot></slot> </h3> <h4 v-else-if="level === 4"> <slot></slot> </h4> <h5 v-else-if="level === 5"> <slot></slot> </h5> <h6 v-else-if="level === 6"> <slot></slot> </h6> `, props: { level: { type: Number, required: true } }})
这个模板感觉不太好。它不仅冗长,而且我们为每个级别标题重复书写了 <slot></slot>
。当我们添加锚元素时,我们必须在每个 v-if/v-else-if
分支中再次重复它。
虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render
函数重写上面的例子:
const app = Vue.createApp({})app.component('anchored-heading', { render() { const { h } = Vue return h( 'h' + this.level, // tag name {}, // props/attributes this.$slots.default() // array of children ) }, props: { level: { type: Number, required: true } }})
render()
函数的实现要精简得多,但是需要非常熟悉组件的实例 property。在这个例子中,你需要知道,向组件中传递不带 v-slot
指令的子节点时,比如 anchored-heading
中的 Hello world!
,这些子节点被存储在组件实例中的 $slots.default
中。如果你还不了解,在深入渲染函数之前推荐阅读实例 property API。
在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML 为例:
<div> <h1>My title</h1> Some text content <!-- TODO: Add tagline --></div>
当浏览器读到这些代码时,它会建立一个 ”DOM 节点“ 树 来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
高效地更新所有这些节点会是比较困难的,不过所幸你不必手动完成这个工作。你只需要告诉 Vue 你希望页面上的 HTML 是什么,这可以是在一个模板里:
<h1>{{ blogTitle }}</h1>
或者一个渲染函数里:
render() { return Vue.h('h1', {}, this.blogTitle)}
在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle
发生了改变。
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:
return Vue.h('h1', {}, this.blogTitle)
h()
到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
h()
参数h()
函数是一个用于创建 vnode 的实用程序。也许可以更准确地将其命名为 createVNode()
,但由于频繁使用和简洁,它被称为 h()
。它接受三个参数:
// @returns {VNode}h( // {String | Object | Function | null} tag // 一个 HTML 标签名、一个组件、一个异步组件,或者 null。 // 使用 null 将会渲染一个注释。 // // 必需的。 'div', // {Object} props // 与 attribute、prop 和事件相对应的对象。 // 我们会在模板中使用。 // // 可选的。 {}, // {String | Array | Object} children // 子 VNodes, 使用 `h()` 构建, // 或使用字符串获取 "文本 Vnode" 或者 // 有 slot 的对象。 // // 可选的。 [ 'Some text comes first.', h('h1', 'A headline'), h(MyComponent, { someProp: 'foobar' }) ])
有了这些知识,我们现在可以完成我们最开始想实现的组件:
const app = Vue.createApp({})/** Recursively get text from children nodes */function getChildrenTextContent(children) { return children .map(node => { return typeof node.children === 'string' ? node.children : Array.isArray(node.children) ? getChildrenTextContent(node.children) : '' }) .join('')}app.component('anchored-heading', { render() { // create kebab-case id from the text contents of the children const headingId = getChildrenTextContent(this.$slots.default()) .toLowerCase() .replace(/W+/g, '-') // replace non-word characters with dash .replace(/(^-|-$)/g, '') // remove leading and trailing dashes return Vue.h('h' + this.level, [ Vue.h( 'a', { name: headingId, href: '#' + headingId }, this.$slots.default() ) ]) }, props: { level: { type: Number, required: true } }})
组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:
render() { const myParagraphVNode = Vue.h('p', 'hi') return Vue.h('div', [ // 错误 - 重复的Vnode! myParagraphVNode, myParagraphVNode ])}
如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:
render() { return Vue.h('div', Array.apply(null, { length: 20 }).map(() => { return Vue.h('p', 'hi') }) )}
v-if
和 v-for
只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-if
和 v-for
:
<ul v-if="items.length"> <li v-for="item in items">{{ item.name }}</li></ul><p v-else>No items found.</p>
这些都可以在渲染函数中用 JavaScript 的 if
/else
和 map()
来重写:
props: ['items'],render() { if (this.items.length) { return Vue.h('ul', this.items.map((item) => { return Vue.h('li', item.name) })) } else { return Vue.h('p', 'No items found.') }}
v-model
v-model
指令扩展为 modelValue
和 onUpdate:modelValue
在模板编译过程中,我们必须自己提供这些prop:
props: ['modelValue'],render() { return Vue.h(SomeComponent, { modelValue: this.modelValue, 'onUpdate:modelValue': value => this.$emit('update:modelValue', value) })}
v-on
我们必须为事件处理程序提供一个正确的prop名称,例如,要处理 click
事件,prop名称应该是 onClick
。
render() { return Vue.h('div', { onClick: $event => console.log('clicked', $event.target) })}
对于 .passive
、 .capture
和 .once
事件修饰符,Vue提供了处理程序的对象语法:
实例:
render() { return Vue.h('input', { onClick: { handler: this.doThisInCapturingMode, capture: true }, onKeyUp: { handler: this.doThisOnce, once: true }, onMouseOver: { handler: this.doThisOnceInCapturingMode, once: true, capture: true }, })}
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 | 处理函数中的等价操作 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
按键: .enter , .13 | if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码 |
修饰键: .ctrl , .alt , .shift , .meta | if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey , shiftKey , 或 metaKey ) |
这里是一个使用所有修饰符的例子:
render() { return Vue.h('input', { onKeyUp: event => { // 如果触发事件的元素不是事件绑定的元素 // 则返回 if (event.target !== event.currentTarget) return // 如果向上键不是回车键,则中止 // 没有同时按下按键 (13) 和 shift 键 if (!event.shiftKey || event.keyCode !== 13) return // 停止事件传播 event.stopPropagation() // 阻止该元素默认的 keyup 事件 event.preventDefault() // ... } })}
你可以通过 this.$slots
访问静态插槽的内容,每个插槽都是一个 VNode 数组:
render() { // `<div><slot></slot></div>` return Vue.h('div', {}, this.$slots.default())}
props: ['message'],render() { // `<div><slot :text="message"></slot></div>` return Vue.h('div', {}, this.$slots.default({ text: this.message }))}
要使用渲染函数将插槽传递给子组件,请执行以下操作:
render() { // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>` return Vue.h('div', [ Vue.h('child', {}, { // pass `slots` as the children object // in the form of { name: props => VNode | Array<VNode> } default: (props) => Vue.h('span', props.text) }) ])}
如果你写了很多渲染函数,可能会觉得下面这样的代码写起来很痛苦:
Vue.h( 'anchored-heading', { level: 1 }, [Vue.h('span', 'Hello'), ' world!'])
特别是对应的模板如此简单的情况下:
<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>
这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'new Vue({ el: '#demo', render() { return ( <AnchoredHeading level={1}> <span>Hello</span> world! </AnchoredHeading> ) }})
有关 JSX 如何映射到 JavaScript 的更多信息,请参阅使用文档 。
你可能会有兴趣知道,Vue 的模板实际上被编译成了渲染函数。这是一个实现细节,通常不需要关心。但如果你想看看模板的功能具体是怎样被编译的,可能会发现会非常有意思。下面是一个使用 Vue.compile
来实时编译模板字符串的简单示例:
插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开 install()
方法的 object
,也可以是 function
插件的功能范围没有严格的限制——一般有下面几种:
config.globalProperties
上实现。为了更好地理解如何创建自己的 Vue.js 版插件,我们将创建一个非常简化的插件版本,它显示 i18n
准备好的字符串。
每当这个插件被添加到应用程序中时,如果它是一个对象,就会调用 install
方法。如果它是一个 function
,则函数本身将被调用。在这两种情况下——它都会收到两个参数:由 Vue 的 createApp
生成的 app
对象和用户传入的选项。
让我们从设置插件对象开始。建议在单独的文件中创建它并将其导出,如下所示,以保持包含的逻辑和分离的逻辑。
// plugins/i18n.jsexport default { install: (app, options) => { // Plugin code goes here }}
我们想要一个函数来翻译整个应用程序可用的键,因此我们将使用 app.config.globalProperties
暴露它。
该函数将接收一个 key
字符串,我们将使用它在用户提供的选项中查找转换后的字符串。
// plugins/i18n.jsexport default { install: (app, options) => { app.config.globalProperties.$translate = key => { return key.split('.').reduce((o, i) => { if (o) return o[i] }, i18n) } }}
我们假设用户使用插件时,将在 options
参数中传递一个包含翻译后的键的对象。我们的 $translate
函数将使用诸如 greetings.hello
之类的字符串,查看用户提供的配置内部并返回转换后的值-在这种情况下为 Bonjour!
。
例如:
greetings: { hello: 'Bonjour!'}
插件还允许我们使用 inject
为插件的用户提供功能或 attribute。例如,我们可以允许应用程序访问 options
参数以能够使用翻译对象。
// plugins/i18n.jsexport default { install: (app, options) => { app.config.globalProperties.$translate = key => { return key.split('.').reduce((o, i) => { if (o) return o[i] }, i18n) } app.provide('i18n', options) }}
插件用户现在可以将 inject[in18]
到他们的组件并访问该对象。
另外,由于我们可以访问 app
对象,因此插件可以使用所有其他功能,例如使用 mixin
和 directive
。要了解有关 createApp
和应用程序实例的更多信息,请查看 Application API 文档。
// plugins/i18n.jsexport default { install: (app, options) => { app.config.globalProperties.$translate = (key) => { return key.split('.') .reduce((o, i) => { if (o) return o[i] }, i18n) } app.provide('i18n', options) app.directive('my-directive', { bind (el, binding, vnode, oldVnode) { // some logic ... } ... }) app.mixin({ created() { // some logic ... } ... }) }}
在使用 createApp()
初始化 Vue 应用程序后,你可以通过调用 use()
方法将插件添加到你的应用程序中。
我们将使用在编写插件部分中创建的 i18nPlugin
进行演示。
use()
方法有两个参数。第一个是要安装的插件,在这种情况下为 i18nPlugin
。
它还会自动阻止你多次使用同一插件,因此在同一插件上多次调用只会安装一次该插件。
第二个参数是可选的,并且取决于每个特定的插件。在演示 i18nPlugin
的情况下,它是带有转换后的字符串的对象。
INFO
如果你使用的是第三方插件 (例如 Vuex
或 Vue Router
),请始终查看文档以了解特定插件期望作为第二个参数接收的内容。
import { createApp } from 'vue'import Root from './App.vue'import i18nPlugin from './plugins/i18n'const app = createApp(Root)const i18nStrings = { greetings: { hi: 'Hallo!' }}app.use(i18nPlugin, i18nStrings)app.mount('#app')
awesome-vue 集合了大量由社区贡献的插件和库。
现在是时候深入了!Vue 最独特的特性之一,是其非侵入性的响应性系统。数据模型是被代理的 JavaScript 对象。而当你修改它们时,视图会进行更新。这让状态管理非常简单直观,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。在这个章节,我们将研究一下 Vue 响应性系统的底层的细节。
在 Vue Mastery 上免费观看关于深入响应性原理的视频。
这个术语在程序设计中经常被提及,但这是什么意思呢?响应性是一种允许我们以声明式的方式去适应变化的一种编程范例。人们通常展示的典型例子,是一份 excel 电子表格 (一个非常好的例子)。
如果将数字 2 放在第一个单元格中,将数字 3 放在第二个单元格中并要求提供 SUM,则电子表格会将其计算出来给你。不要惊奇,同时,如果你更新第一个数字,SUM 也会自动更新。
JavaScript 通常不是这样工作的——如果我们想用 JavaScript 编写类似的内容:
var val1 = 2var val2 = 3var sum = val1 + val2// sum// 5val1 = 3// sum// 5
如果我们更新第一个值,sum 不会被修改。
那么我们如何用 JavaScript 实现这一点呢?
当把一个普通的 JavaScript 对象作为 data
选项传给应用或组件实例的时候,Vue 会使用带有 getter 和 setter 的处理程序遍历其所有 property 并将其转换为 Proxy。这是 ES6 仅有的特性,但是我们在 Vue 3 版本也使用了 Object.defineProperty
来支持 IE 浏览器。两者具有相同的 Surface API,但是 Proxy 版本更精简,同时提升了性能。
该部分需要稍微地了解下 Proxy 的某些知识!所以,让我们深入了解一下。关于 Proxy 的文献很多,但是你真正需要知道的是 Proxy 是一个包含另一个对象或函数并允许你对其进行拦截的对象。
我们是这样使用它的:new Proxy(target, handler)
const dinner = { meal: 'tacos'}const handler = { get(target, prop) { return target[prop] }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
好的,到目前为止,我们只是包装这个对象并返回它。很酷,但还不是那么有用。请注意,我们把对象包装在 Proxy 里的同时可以对其进行拦截。这种拦截被称为陷阱。
const dinner = { meal: 'tacos'}const handler = { get(target, prop) { console.log('intercepted!') return target[prop] }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
除了控制台日志,我们可以在这里做任何我们想做的事情。如果我们愿意,我们甚至可以不返回实际值。这就是为什么 Proxy 对于创建 API 如此强大。
此外,Proxy 还提供了另一个特性。我们不必像这样返回值:target[prop]
,而是可以进一步使用一个名为 Reflect
的方法,它允许我们正确地执行 this
绑定,就像这样:
const dinner = { meal: 'tacos'}const handler = { get(target, prop, receiver) { return Reflect.get(...arguments) }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
我们之前提到过,为了有一个 API 能够在某些内容发生变化时更新最终值,我们必须在内容发生变化时设置新的值。我们在处理器,一个名为 track
的函数中执行此操作,该函数可以传入 target
和 key
两个参数。
const dinner = { meal: 'tacos'}const handler = { get(target, prop, receiver) { track(target, prop) return Reflect.get(...arguments) }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
最后,当某些内容发生改变时我们会设置新的值。为此,我们将通过触发这些更改来设置新 Proxy 的更改:
const dinner = { meal: 'tacos'}const handler = { get(target, prop, receiver) { track(target, prop) return Reflect.get(...arguments) }, set(target, key, value, receiver) { trigger(target, key) return Reflect.set(...arguments) }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
还记得几段前的列表吗?现在我们有了一些关于 Vue 如何处理这些更改的答案:
effect
trigger
Proxy 对象对于用户来说是不可见的,但是在内部,它们使 Vue 能够在 property 的值被访问或修改的情况下进行依赖跟踪和变更通知。从 Vue 3 开始,我们的响应性现在可以在独立的包中使用。需要注意的是,记录转换后的数据对象时,浏览器控制台输出的格式会有所不同,因此你可能需要安装 vue-devtools,以提供一种更易于检查的界面。
Vue 在内部跟踪所有已被设置为响应式的对象,因此它始终会返回同一个对象的 Proxy 版本。
从响应式 Proxy 访问嵌套对象时,该对象在返回之前也被转换为 Proxy:
const handler = { get(target, prop, receiver) { track(target, prop) const value = Reflect.get(...arguments) if (isObject(value)) { return reactive(value) } else { return value } } // ...}
Proxy 的使用确实引入了一个需要注意的新警告:在身份比较方面,被代理对象与原始对象不相等 (===
)。例如:
const obj = {}const wrapped = new Proxy(obj, handlers)console.log(obj === wrapped) // false
在大多数情况下,原始版本和包装版本的行为相同,但请注意,它们在依赖严格比对的操作下将是失败的,例如 .filter()
或 .map()
。使用选项式 API 时,这种警告不太可能出现,因为所有响应式都是从 this
访问的,并保证已经是 Proxy。
但是,当使用合成 API 显式创建响应式对象时,最佳做法是不要保留对原始对象的引用,而只使用响应式版本:
const obj = reactive({ count: 0}) // no reference to original
每个组件实例都有一个相应的侦听器实例,该实例将在组件渲染期间把“触碰”的所有 property 记录为依赖项。之后,当触发依赖项的 setter 时,它会通知侦听器,从而使得组件重新渲染。
将对象作为数据传递给组件实例时,Vue 会将其转换为 Proxy。这个 Proxy 使 Vue 能够在 property 被访问或修改时执行依赖项跟踪和更改通知。每个 property 都被视为一个依赖项。
首次渲染后,组件将跟踪一组依赖列表——即在渲染过程中被访问的 property。反过来,组件就成为了其每个 property 的订阅者。当 Proxy 拦截到 set 操作时,该 property 将通知其所有订阅的组件重新渲染。
如果你使用的是 Vue2.x 及以下版本,你可能会对这些版本中存在的一些更改检测警告感兴趣,在这里进行更详细的探讨。
要为 JavaScript 对象创建响应式状态,可以使用 reactive
方法:
import { reactive } from 'vue'// 响应式状态const state = reactive({ count: 0})
reactive
相当于 Vue 2.x 中的 Vue.observable()
API ,为避免与 RxJS 中的 observables 混淆因此对其重命名。该 API 返回一个响应式的对象状态。该响应式转换是“深度转换”——它会影响嵌套对象传递的所有 property。
Vue 中响应式状态的基本用例是我们可以在渲染期间使用它。因为依赖跟踪的关系,当响应式状态改变时视图会自动更新。
这就是 Vue 响应性系统的本质。当从组件中的 data()
返回一个对象时,它在内部交由 reactive()
使其成为响应式对象。模板会被编译成能够使用这些响应式 property 的渲染函数。
在响应性基础 API 章节你可以学习更多关于 reactive
的内容。
refs
想象一下,我们有一个独立的原始值 (例如,一个字符串),我们想让它变成响应式的。当然,我们可以创建一个拥有相同字符串 property 的对象,并将其传递给 reactive
。Vue 为我们提供了一个可以做相同事情的方法 ——ref
:
import { ref } from 'vue'const count = ref(0)
ref
会返回一个可变的响应式对象,该对象作为它的内部值——一个响应式的引用,这就是名称的来源。此对象只包含一个名为 value
的 property :
import { ref } from 'vue'const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
当 ref 作为渲染上下文 (从 setup() 中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动展开为内部值。不需要在模板中追加 .value
:
<template> <div> <span>{{ count }}</span> <button @click="count ++">Increment count</button> </div></template><script> import { ref } from 'vue' export default { setup() { const count = ref(0) return { count } } }</script>
当 ref
作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动展开内部值:
const count = ref(0)const state = reactive({ count})console.log(state.count) // 0state.count = 1console.log(count.value) // 1
如果将新的 ref 赋值给现有 ref 的 property,将会替换旧的 ref:
const otherCount = ref(2)state.count = otherCountconsole.log(state.count) // 2console.log(count.value) // 1
Ref 展开仅发生在被响应式 Object
嵌套的时候。当从 Array
或原生集合类型如 Map
访问 ref 时,不会进行展开:
const books = reactive([ref('Vue 3 Guide')])// 这里需要 .valueconsole.log(books[0].value)const map = reactive(new Map([['count', ref(0)]]))// 这里需要 .valueconsole.log(map.get('count').value)
当我们想使用大型响应式对象的一些 property 时,可能很想使用 ES6 解构来获取我们想要的 property:
import { reactive } from 'vue'const book = reactive({ author: 'Vue Team', year: '2020', title: 'Vue 3 Guide', description: 'You are reading this book right now ;)', price: 'free'})let { author, title } = book
遗憾的是,使用解构的两个 property 的响应性都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:
import { reactive, toRefs } from 'vue'const book = reactive({ author: 'Vue Team', year: '2020', title: 'Vue 3 Guide', description: 'You are reading this book right now ;)', price: 'free'})let { author, title } = toRefs(book)title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 refconsole.log(book.title) // 'Vue 3 Detailed Guide'
你可以在 Refs API 部分中了解更多有关 refs
的信息
readonly
防止更改响应式对象有时我们想跟踪响应式对象 (ref
或 reactive
) 的变化,但我们也希望防止在应用程序的某个位置更改它。例如,当我们有一个被 provide 的响应式对象时,我们不想让它在注入的时候被改变。为此,我们可以基于原始对象创建一个只读的 Proxy 对象:
import { reactive, readonly } from 'vue'const original = reactive({ count: 0 })const copy = readonly(original)// 在copy上转换original 会触发侦听器依赖original.count++// 转换copy 将导失败并导致警告copy.count++ // 警告: "Set operation on key 'count' failed: target is readonly."
本节使用单文件组件语法作为代码示例
有时我们需要依赖于其他状态的状态——在 Vue 中,这是用组件计算属性处理的,以直接创建计算值,我们可以使用 computed
方法:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象。
const count = ref(1)const plusOne = computed(() => count.value++)console.log(plusOne.value) // 2plusOne.value++ // error
或者,它可以使用一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
const count = ref(1)const plusOne = computed({ get: () => count.value + 1, set: val => { count.value = val - 1 }})plusOne.value = 1console.log(count.value) // 0
watchEffect
为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect
方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)watchEffect(() => console.log(count.value))// -> logs 0setTimeout(() => { count.value++ // -> logs 1}, 100)
当 watchEffect
在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
const stop = watchEffect(() => { /* ... */})// laterstop()
有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个 onInvalidate
函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
setup()
或生命周期钩子函数中使用了 watchEffect
,则在组件卸载时)watchEffect(onInvalidate => { const token = performAsyncOperation(id.value) onInvalidate(() => { // id has changed or watcher is stopped. // invalidate previously pending async operation token.cancel() })})
我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要。
在执行数据请求时,副作用函数往往是一个异步函数:
const data = ref(null)watchEffect(async onInvalidate => { onInvalidate(() => {...}) // 我们在Promise解析之前注册清除函数 data.value = await fetchData(props.id)})
我们知道异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误。
Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。在核心的具体实现中,组件的 update
函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 update
前执行:
<template> <div>{{ count }}</div></template><script> export default { setup() { const count = ref(0) watchEffect(() => { console.log(count.value) }) return { count } } }</script>
在这个例子中:
count
会在初始运行时同步打印出来count
时,将在组件更新前执行副作用。如果需要在组件更新后重新运行侦听器副作用,我们可以传递带有 flush
选项的附加 options
对象 (默认为 'pre'
):
// fire before component updateswatchEffect( () => { /* ... */ }, { flush: 'post' })
flush
选项还接受 sync
,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
onTrack
和 onTrigger
选项可用于调试侦听器的行为。
onTrack
将在响应式 property 或 ref 作为依赖项被追踪时被调用。onTrigger
将在依赖项变更导致副作用被触发时被调用。这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger
语句来检查依赖关系:
watchEffect( () => { /* 副作用 */ }, { onTrigger(e) { debugger } })
onTrack
和 onTrigger
只能在开发模式下工作。
watch
watch
API 完全等同于组件侦听器 property。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
watch
允许我们:侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref
:
// 侦听一个 getterconst state = reactive({ count: 0 })watch( () => state.count, (count, prevCount) => { /* ... */ })// 直接侦听refconst count = ref(0)watch(count, (count, prevCount) => { /* ... */})
侦听器还可以使用数组同时侦听多个源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */})
watchEffect
共享的行为watch
与 watchEffect
共享停止侦听,清除副作用 (相应地 onInvalidate
会作为回调的第三个参数传入)、副作用刷新时机和侦听器调试行为。
提示
在阅读文档之前,你应该已经熟悉了这两个 Vue 基础和创建组件。
在 Vue Mastery 上观看关于组合式 API 的免费视频。
通过创建 Vue 组件,我们可以将接口的可重复部分及其功能提取到可重用的代码段中。仅此一项就可以使我们的应用程序在可维护性和灵活性方面走得更远。然而,我们的经验已经证明,光靠这一点可能是不够的,尤其是当你的应用程序变得非常大的时候——想想几百个组件。在处理如此大的应用程序时,共享和重用代码变得尤为重要。
假设在我们的应用程序中,我们有一个视图来显示某个用户的仓库列表。除此之外,我们还希望应用搜索和筛选功能。处理此视图的组件可能如下所示:
// src/components/UserRepositories.vueexport default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String } }, data () { return { repositories: [], // 1 filters: { ... }, // 3 searchQuery: '' // 2 } }, computed: { filteredRepositories () { ... }, // 3 repositoriesMatchingSearchQuery () { ... }, // 2 }, watch: { user: 'getUserRepositories' // 1 }, methods: { getUserRepositories () { // 使用 `this.user` 获取用户仓库 }, // 1 updateFilters () { ... }, // 3 }, mounted () { this.getUserRepositories() // 1 }}
该组件有以下几个职责:
searchQuery
字符串搜索存储库filters
对象筛选仓库用组件的选项 (data
、computed
、methods
、watch
) 组织逻辑在大多数情况下都有效。然而,当我们的组件变得更大时,逻辑关注点的列表也会增长。这可能会导致组件难以阅读和理解,尤其是对于那些一开始就没有编写这些组件的人来说。
一个大型组件的示例,其中逻辑关注点是按颜色分组。
这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
如果我们能够将与同一个逻辑关注点相关的代码配置在一起会更好。而这正是组合式 API 使我们能够做到的。
既然我们知道了为什么,我们就可以知道怎么做。为了开始使用组合式 API,我们首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup
。
setup
组件选项新的 setup
组件选项在创建组件之前执行,一旦 props
被解析,并充当合成 API 的入口点。
WARNING
由于在执行 setup
时尚未创建组件实例,因此在 setup
选项中没有 this
。这意味着,除了 props
之外,你将无法访问组件中声明的任何属性——本地状态、计算属性或方法。
setup
选项应该是一个接受 props
和 context
的函数,我们将在稍后讨论。此外,我们从 setup
返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
让我们添加 setup
到我们的组件中:
// src/components/UserRepositories.vueexport default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String } }, setup(props) { console.log(props) // { user: '' } return {} // 这里返回的任何内容都可以用于组件的其余部分 } // 组件的“其余部分”}
现在让我们从提取第一个逻辑关注点开始 (在原始代码段中标记为“1”)。
- 从假定的外部 API 获取该用户名的仓库,并在用户更改时刷新它
我们将从最明显的部分开始:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'// 在我们的组件内setup (props) { let repositories = [] const getUserRepositories = async () => { repositories = await fetchUserRepositories(props.user) } return { repositories, getUserRepositories // 返回的函数与方法的行为相同 }}
这是我们的出发点,但它还不能工作,因为我们的 repositories
变量是非响应式的。这意味着从用户的角度来看,仓库列表将保持为空。我们来解决这个问题!
ref
的响应式变量在 Vue 3.0 中,我们可以通过一个新的 ref
函数使任何响应式变量在任何地方起作用,如下所示:
import { ref } from 'vue'const counter = ref(0)
ref
接受参数并返回它包装在具有 value
property 的对象中,然后可以使用该 property 访问或更改响应式变量的值:
import { ref } from 'vue'const counter = ref(0)console.log(counter) // { value: 0 }console.log(counter.value) // 0counter.value++console.log(counter.value) // 1
在对象中包装值似乎不必要,但在 JavaScript 中保持不同数据类型的行为统一是必需的。这是因为在 JavaScript 中,Number
或 String
等基本类型是通过值传递的,而不是通过引用传递的:
在任何值周围都有一个包装器对象,这样我们就可以在整个应用程序中安全地传递它,而不必担心在某个地方失去它的响应性。
提示
换句话说,ref
对我们的值创建了一个响应式引用。使用引用的概念将在整个组合式 API 中经常使用。
回到我们的例子,让我们创建一个响应式的 repositories
变量:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'import { ref } from 'vue'// in our componentsetup (props) { const repositories = ref([]) const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(props.user) } return { repositories, getUserRepositories }}
完成!现在,每当我们调用 getUserRepositories
时,repositories
都将发生变化,视图将更新以反映更改。我们的组件现在应该如下所示:
// src/components/UserRepositories.vueimport { fetchUserRepositories } from '@/api/repositories'import { ref } from 'vue'export default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String } }, setup (props) { const repositories = ref([]) const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(props.user) } return { repositories, getUserRepositories } }, data () { return { filters: { ... }, // 3 searchQuery: '' // 2 } }, computed: { filteredRepositories () { ... }, // 3 repositoriesMatchingSearchQuery () { ... }, // 2 }, watch: { user: 'getUserRepositories' // 1 }, methods: { updateFilters () { ... }, // 3 }, mounted () { this.getUserRepositories() // 1 }}
我们已经将第一个逻辑关注点中的几个部分移到了 setup
方法中,它们彼此非常接近。剩下的就是在 mounted
钩子中调用 getUserRepositories
,并设置一个监听器,以便在 user
prop 发生变化时执行此操作。
我们将从生命周期钩子开始。
setup
为了使组合式 API 的特性与选项式 API 相比更加完整,我们还需要一种在 setup
中注册生命周期钩子的方法。这要归功于从 Vue 导出的几个新函数。组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为 on
:即 mounted
看起来像 onMounted
。
这些函数接受在组件调用钩子时将执行的回调。
让我们将其添加到 setup
函数中:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'import { ref, onMounted } from 'vue'// in our componentsetup (props) { const repositories = ref([]) const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(props.user) } onMounted(getUserRepositories) // on `mounted` call `getUserRepositories` return { repositories, getUserRepositories }}
现在我们需要对 user
prop 所做的更改做出反应。为此,我们将使用独立的 watch
函数。
watch
响应式更改就像我们如何使用 watch
选项在组件内的 user
property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch
函数执行相同的操作。它接受 3 个参数:
下面让我们快速了解一下它是如何工作的
import { ref, watch } from 'vue'const counter = ref(0)watch(counter, (newValue, oldValue) => { console.log('The new counter value is: ' + counter.value)})
例如,每当 counter
被修改时 counter.value=5
,watch 将触发并执行回调 (第二个参数),在本例中,它将把 'The new counter value is:5'
记录到我们的控制台中。
以下是等效的选项式 API:
export default { data() { return { counter: 0 } }, watch: { counter(newValue, oldValue) { console.log('The new counter value is: ' + this.counter) } }}
有关 watch
的详细信息,请参阅我们的深入指南。
现在我们将其应用到我们的示例中:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'import { ref, onMounted, watch, toRefs } from 'vue'// 在我们组件中setup (props) { // 使用 `toRefs` 创建对prop的 `user` property 的响应式引用 const { user } = toRefs(props) const repositories = ref([]) const getUserRepositories = async () => { // 更新 `prop.user` 到 `user.value` 访问引用值 repositories.value = await fetchUserRepositories(user.value) } onMounted(getUserRepositories) // 在用户 prop 的响应式引用上设置一个侦听器 watch(user, getUserRepositories) return { repositories, getUserRepositories }}
你可能已经注意到在我们的 setup
的顶部使用了 toRefs
。这是为了确保我们的侦听器能够对 user
prop 所做的更改做出反应。
有了这些变化,我们就把第一个逻辑关注点移到了一个地方。我们现在可以对第二个关注点执行相同的操作——基于 searchQuery
进行过滤,这次是使用计算属性。
computed
属性与 ref
和 watch
类似,也可以使用从 Vue 导入的 computed
函数在 Vue 组件外部创建计算属性。让我们回到我们的 counter 例子:
import { ref, computed } from 'vue'const counter = ref(0)const twiceTheCounter = computed(() => counter.value * 2)counter.value++console.log(counter.value) // 1console.log(twiceTheCounter.value) // 2
在这里,computed
函数返回一个作为 computed
的第一个参数传递的 getter 类回调的输出的一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像使用 ref
一样使用 .value
property。
让我们将搜索功能移到 setup
中:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'import { ref, onMounted, watch, toRefs, computed } from 'vue'// in our componentsetup (props) { // 使用 `toRefs` 创建对 props 的 `user` property 的响应式引用 const { user } = toRefs(props) const repositories = ref([]) const getUserRepositories = async () => { // 更新 `props.user ` 到 `user.value` 访问引用值 repositories.value = await fetchUserRepositories(user.value) } onMounted(getUserRepositories) // 在用户 prop 的响应式引用上设置一个侦听器 watch(user, getUserRepositories) const searchQuery = ref('') const repositoriesMatchingSearchQuery = computed(() => { return repositories.value.filter( repository => repository.name.includes(searchQuery.value) ) }) return { repositories, getUserRepositories, searchQuery, repositoriesMatchingSearchQuery }}
对于其他的逻辑关注点我们也可以这样做,但是你可能已经在问这个问题了——这不就是把代码移到 setup
选项并使它变得非常大吗?嗯,那是真的。这就是为什么在继续其他任务之前,我们将首先将上述代码提取到一个独立的组合式函数。让我们从创建 useUserRepositories
开始:
// src/composables/useUserRepositories.jsimport { 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.jsimport { 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.vueimport 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 { // 因为我们并不关心未经过滤的仓库 // 我们可以在 `repositories` 名称下暴露过滤后的结果 repositories: repositoriesMatchingSearchQuery, getUserRepositories, searchQuery, } }, data () { return { filters: { ... }, // 3 } }, computed: { filteredRepositories () { ... }, // 3 }, methods: { updateFilters () { ... }, // 3 }}
此时,你可能已经知道了这个练习,所以让我们跳到最后,迁移剩余的过滤功能。我们不需要深入了解实现细节,因为这不是本指南的重点。
// src/components/UserRepositories.vueimport { toRefs } from 'vue'import useUserRepositories from '@/composables/useUserRepositories'import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'import useRepositoryFilters from '@/composables/useRepositoryFilters'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) const { filters, updateFilters, filteredRepositories } = useRepositoryFilters(repositoriesMatchingSearchQuery) return { // 因为我们并不关心未经过滤的仓库 // 我们可以在 `repositories` 名称下暴露过滤后的结果 repositories: filteredRepositories, getUserRepositories, searchQuery, filters, updateFilters } }}
我们完成了!
请记住,我们只触及了组合式 API 的表面以及它允许我们做什么。要了解更多信息,请参阅深入指南。
本节使用单文件组件代码示例的语法
本指南假定你已经阅读了组合式 API 简介和响应性原理。如果你不熟悉组合式 API,请先阅读这篇文章。
使用 setup
函数时,它将接受两个参数:
props
context
让我们更深入地研究如何使用每个参数。
setup
函数中的第一个参数是 props
。正如在一个标准组件中所期望的那样,setup
函数中的 props
是响应式的,当传入新的 prop 时,它将被更新。
// MyBook.vueexport default { props: { title: String }, setup(props) { console.log(props.title) }}
WARNING
但是,因为 props
是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。
如果需要解构 prop,可以通过使用 setup
函数中的 toRefs
来安全地完成此操作。
// MyBook.vueimport { toRefs } from 'vue'setup(props) { const { title } = toRefs(props) console.log(title.value)}
传递给 setup
函数的第二个参数是 context
。context
是一个普通的 JavaScript 对象,它暴露三个组件的 property:
// MyBook.vueexport default { setup(props, context) { // Attribute (非响应式对象) console.log(context.attrs) // 插槽 (非响应式对象) console.log(context.slots) // 触发事件 (方法) console.log(context.emit) }}
context
是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context
使用 ES6 解构。
// MyBook.vueexport default { setup(props, { attrs, slots, emit }) { ... }}
attrs
和 slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x
或 slots.x
的方式引用 property。请注意,与 props
不同,attrs
和 slots
是非响应式的。如果你打算根据 attrs
或 slots
更改应用副作用,那么应该在 onUpdated
生命周期钩子中执行此操作。
执行 setup
时,组件实例尚未被创建。因此,你只能访问以下 property:
props
attrs
slots
emit
换句话说,你将无法访问以下组件选项:
data
computed
methods
如果 setup
返回一个对象,则可以在组件的模板中像传递给 setup
的 props
property 一样访问该对象的 property:
<!-- MyBook.vue --><template> <div>{{ readersNumber }} {{ book.title }}</div></template><script> import { ref, reactive } from 'vue' export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // expose to template return { readersNumber, book } } }</script>
注意,从 setup
返回的 refs 在模板中访问时是被自动解开的,因此不应在模板中使用 .value
。
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:
// MyBook.vueimport { h, ref, reactive } from 'vue'export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // Please note that we need to explicitly expose ref value here return () => h('div', [readersNumber.value, book.title]) }}
this
在 setup()
内部,this
不会是该活跃实例的引用,因为 setup()
是在解析其它组件选项之前被调用的,所以 setup()
内部的 this
的行为与其它选项中的 this
完全不同。这在和其它选项式 API 一起使用 setup()
时可能会导致混淆。
本指南假定你已经阅读了 组合式 API 简介和响应性基础。如果你不熟悉组合式 API,请先阅读这篇文章。
在 Vue Mastery 上观看关于生命周期钩子的免费视频
你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。
下表包含如何在 setup () 内部调用生命周期钩子:
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
TIP
因为 setup
是围绕 beforeCreate
和 created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup
函数中编写。
这些函数接受一个回调函数,当钩子被组件调用时将会被执行:
// MyBook.vueexport default { setup() { // mounted onMounted(() => { console.log('Component is mounted!') }) }}
本指南假定你已经阅读了 Provide / Inject、组合式 API 介绍和响应性基础。如果你不熟悉组合式 API,请先阅读这篇文章。
我们也可以在组合式 API 中使用 provide/inject。两者都只能在当前活动实例的 setup()
期间调用。
假设我们要重写以下代码,其中包含一个 MyMap
组件,该组件使用组合式 API 为 MyMarker
组件提供用户的位置。
<!-- 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', 'geolocation']}</script>
在 setup()
中使用 provide
时,我们首先从 vue
显式导入 provide
方法。这使我们能够调用 provide
时来定义每个 property。
provide
函数允许你通过两个参数定义 property:
<String>
类型)使用 MyMap
组件,我们提供的值可以按如下方式重构:
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { provide('location', 'North Pole') provide('geolocation', { longitude: 90, latitude: 135 }) }}</script>
在 setup()
中使用 inject
时,还需要从 vue
显式导入它。一旦我们这样做了,我们就可以调用它来定义如何将它暴露给我们的组件。
inject
函数有两个参数:
使用 MyMarker
组件,可以使用以下代码对其进行重构:
<!-- src/components/MyMarker.vue --><script>import { inject } from 'vue'export default { setup() { const userLocation = inject('location', 'The Universe') const userGeolocation = inject('geolocation') return { userLocation, userGeolocation } }}</script>
为了增加提供值和注入值之间的响应性,我们可以在提供值时使用 ref 或 reactive。
使用 MyMap
组件,我们的代码可以更新如下:
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide, reactive, ref } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { const location = ref('North Pole') const geolocation = reactive({ longitude: 90, latitude: 135 }) provide('location', location) provide('geolocation', geolocation) }}</script>
现在,如果这两个 property 中有任何更改,MyMarker
组件也将自动更新!
当使用响应式提供/注入值时,建议尽可能,在*提供者*内保持响应式 property 的任何更改。
例如,在需要更改用户位置的情况下,我们最好在 MyMap
组件中执行此操作。
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide, reactive, ref } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { const location = ref('North Pole') const geolocation = reactive({ longitude: 90, latitude: 135 }) provide('location', location) provide('geolocation', geolocation) return { location } }, methods: { updateLocation() { this.location = 'South Pole' } }}</script>
然而,有时我们需要在注入数据的组件内部更新注入的数据。在这种情况下,我们建议提供一个方法来负责改变响应式 property。
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide, reactive, ref } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { const location = ref('North Pole') const geolocation = reactive({ longitude: 90, latitude: 135 }) const updateLocation = () => { location.value = 'South Pole' } provide('location', location) provide('geolocation', 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>
最后,如果要确保通过 provide
传递的数据不会被注入的组件更改,我们建议对提供者的 property 使用 readonly
。
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide, reactive, readonly, ref } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { const location = ref('North Pole') const geolocation = reactive({ longitude: 90, latitude: 135 }) const updateLocation = () => { location.value = 'South Pole' } provide('location', readonly(location)) provide('geolocation', readonly(geolocation)) provide('updateLocation', updateLocation) }}</script>
本节代码示例使用单文件组件的语法
本指南假定你已经阅读了组合式 API 简介和响应性基础。如果你不熟悉组合式 API,请先阅读此文章。
在使用组合式 API 时,响应式引用和模板引用的概念是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明 ref 并从 setup() 返回:
<template> <div ref="root">This is a root element</div></template><script> import { ref, onMounted } from 'vue' export default { setup() { const root = ref(null) onMounted(() => { // DOM元素将在初始渲染后分配给ref console.log(root.value) // <div>这是根元素</div> }) return { root } } }</script>
这里我们在渲染上下文中暴露 root
,并通过 ref="root"
,将其绑定到 div 作为其 ref。在虚拟 DOM 补丁算法中,如果 VNode 的 ref
键对应于渲染上下文中的 ref,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
作为模板使用的 ref 的行为与任何其他 ref 一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。
export default { setup() { const root = ref(null) return () => h('div', { ref: root }) // with JSX return () => <div ref={root} /> }}
v-for
中的用法组合式 API 模板引用在 v-for
内部使用时没有特殊处理。相反,请使用函数引用执行自定义处理:
<template> <div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }"> {{ item }} </div></template><script> import { ref, reactive, onBeforeUpdate } from 'vue' export default { setup() { const list = reactive([1, 2, 3]) const divs = ref([]) // 确保在每次更新之前重置ref onBeforeUpdate(() => { divs.value = [] }) return { list, divs } } }</script>
为了学习如何更好地使用 Vue,不需要阅读本页,但是它提供了更多信息,如果你想知道渲染在背后是如何工作的。
现在我们知道了侦听器是如何更新组件的,你可能会问这些更改最终是如何应用到 DOM 中的!也许你以前听说过虚拟 DOM,包括 Vue 在内的许多框架都使用这种方式来确保我们的接口能够有效地反映我们在 JavaScript 中更新的更改
我们用 JavaScript 复制了一个名为 Virtual Dom 的 DOM,我们这样做是因为用 JavaScript 接触 DOM 的计算成本很高。虽然用 JavaScript 执行更新很廉价,但是找到所需的 DOM 节点并用 JS 更新它们的成本很高。所以我们批处理调用,同时更改 DOM。
虚拟 DOM 是轻量级的 JavaScript 对象,由渲染函数创建。它包含三个参数:元素,带有数据的对象,prop,attr 以及更多,和一个数组。数组是我们传递子级的地方,子级也具有所有这些参数,然后它们可以具有子级,依此类推,直到我们构建完整的元素树为止。
如果需要更新列表项,可以使用前面提到的响应性在 JavaScript 中进行。然后,我们对 JavaScript 副本,虚拟 DOM 进行所有更改,并在此与实际 DOM 之间进行区分。只有这样,我们才能对已更改的内容进行更新。虚拟 DOM 允许我们对 UI 进行高效的更新!
该页面仅适用于 Vue 2.x 及更低版本,并假定你已经阅读了响应性部分。请先阅读该部分。
由于 JavaScript 的限制,有些 Vue 无法检测的更改类型。但是,有一些方法可以规避它们以维持响应性。
Vue 无法检测到 property 的添加或删除。由于 Vue 在实例初始化期间执行 getter/setter 转换过程,因此必须在 data
对象中存在一个 property,以便 Vue 对其进行转换并使其具有响应式。例如:
var vm = new Vue({ data: { a: 1 }})// `vm.a` 现在是响应式的vm.b = 2// `vm.b` 不是响应式的
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object,propertyName,value)
方法向嵌套对象添加响应式 property:
Vue.set(vm.someObject, 'b', 2)
你还可以使用 vm.$set
实例方法,这也是全局 Vue.set
方法的别名:
this.$set(this.someObject, 'b', 2)
有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign()
或 _.extend()
。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
// 而不是 `Object.assign(this.someObject, { a: 1, b: 2 })`this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
Vue 不能检测以下数组的变动:
vm.items[indexOfItem] = newValue
vm.items.length = newLength
例如:
var vm = new Vue({ data: { items: ['a', 'b', 'c'] }})vm.items[1] = 'x' // 不是响应式的vm.items.length = 2 // 不是响应式的
为了解决第一种问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue
相同的效果,同时也将在响应性系统内触发状态更新:
// Vue.setVue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splicevm.items.splice(indexOfItem, 1, newValue)
你也可以使用 vm.$set
实例方法,该方法是全局方法 Vue.set
的一个别名:
vm.$set(vm.items, indexOfItem, newValue)
为了解决第二种问题,你可以使用 splice
:
vm.items.splice(newLength)
由于 Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值:
var vm = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '<div>{{ message }}</div>'})// 之后设置 `message`vm.message = 'Hello!'
如果你未在 data 选项中声明 message
,Vue 将警告你渲染函数正在试图访问不存在的 property。
这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使组件实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data
对象就像组件的状态结构 (schema)。提前声明所有的响应式 property,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
例如,当你设置 vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)
。这样回调函数将在 DOM 更新完成后被调用。例如:
<div id="example">{{ message }}</div>
var vm = new Vue({ el: '#example', data: { message: '123' }})vm.message = 'new message' // 更改数据vm.$el.textContent === 'new message' // falseVue.nextTick(function() { vm.$el.textContent === 'new message' // true})
在组件内使用 vm.$nextTick()
实例方法特别方便,因为它不需要全局 Vue
,并且回调函数中的 this
将自动绑定到当前的组件实例上:
Vue.component('example', { template: '<span>{{ message }}</span>', data: function() { return { message: 'not updated' } }, methods: { updateMessage: function() { this.message = 'updated' console.log(this.$el.textContent) // => 'not updated' this.$nextTick(function() { console.log(this.$el.textContent) // => 'updated' }) } }})
因为 $nextTick()
返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: { updateMessage: async function () { this.message = 'updated' console.log(this.$el.textContent) // => 'not updated' await this.$nextTick() console.log(this.$el.textContent) // => 'updated' } }
在很多 Vue 项目中,我们使用 app.component
来定义全局组件,紧接着用 app.mount('#app')
在每个页面内指定一个容器元素。
这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:
所有这些都可以通过扩展名为 .vue
的 single-file components (单文件组件) 来解决,并且还可以使用 webpack 或 Browserify 等构建工具。
这是一个文件名为 Hello.vue
的简单实例:
现在我们获得:
正如我们说过的,我们可以使用预处理器来构建简洁和功能更丰富的组件,比如 Pug,Babel (with ES2015 modules),和 Stylus。
这些特定的语言只是例子,你可以只是简单地使用 Babel,TypeScript,SCSS,PostCSS 或者其他任何能够帮助你提高生产力的预处理器。如果搭配 vue-loader
使用 webpack,它也能为 CSS Modules 提供头等支持。
一个重要的事情值得注意,关注点分离不等于文件类型分离。在现代 UI 开发中,我们已经发现相比于把代码库分离成三个大的层次并将其相互交织起来,把它们划分为松散耦合的组件再将其组合起来更合理一些。在一个组件里,其模板、逻辑和样式是内部耦合的,并且把他们搭配在一起实际上使得组件更加内聚且更可维护。
即便你不喜欢单文件组件,你仍然可以把 JavaScript、CSS 分离成独立的文件然后做到热重载和预编译。
<!-- my-component.vue --><template> <div>This will be pre-compiled</div></template><script src="./my-component.js"></script><style src="./my-component.css"></style>
如果你希望深入了解并开始使用单文件组件,请来 CodeSandbox 看看这个简单的 todo 应用。
有了 .vue
组件,我们就进入了高阶 JavaScript 应用领域。如果你没有准备好的话,意味着还需要学会使用一些附加的工具:
在你花一天时间了解这些资源之后,我们建议你参考 Vue CLI。只要遵循指示,你就能很快地运行一个带有 .vue
组件、ES2015、webpack 和热重载 (hot-reloading) 的 Vue 项目!
CLI 会为你搞定大多数工具的配置问题,同时也支持细粒度自定义配置项。
有时你会想从零搭建你自己的构建工具,这时你需要通过 Vue Loader 手动配置 webpack。关于学习更多 webpack 的内容,请查阅其官方文档和 Webpack Academy。
当构建可靠的应用时,测试在个人或团队构建新特性、重构代码、修复 bug 等工作中扮演了关键的角色。尽管测试的流派有很多,它们在 web 应用这个领域里主要有三大类:
本章节致力于引导大家了解测试的生态系统的并为 Vue 应用或组件库选择适合的工具。
单元测试允许你将独立单元的代码进行隔离测试,其目的是为开发者提供对代码的信心。通过编写细致且有意义的测试,你能够有信心在构建新特性或重构已有代码的同时,保持应用的功能和稳定。
为一个 Vue 应用做单元测试并没有和为其它类型的应用做测试有什么明显的区别。
因为单元测试的建议通常是框架无关的,所以下面只是当你在评估应用的单元测试工具时需要的一些基本指引。
当测试失败时,提供有用的错误信息对于单元测试框架来说至关重要。这是断言库应尽的职责。一个具有高质量错误信息的断言能够最小化调试问题所需的时间。除了简单地告诉你什么测试失败了,断言库还应额外提供上下文以及测试失败的原因,例如预期结果 vs 实际得到的结果。
一些诸如 Jest 这样的单元测试框架会包含断言库。另一些诸如 Mocha 需要你单独安装断言库 (通常会用 Chai)。
因为主流的单元测试框架都是开源的,所以对于一些旨在长期维护其测试且确保项目本身保持活跃的团队来说,拥有一个活跃的社区是至关重要的。额外的好处是,在任何时候遇到问题时,一个活跃的社区会为你提供更多的支持。
尽管生态系统里有很多工具,这里我们列出一些在 Vue 生态系统中常用的单元测试工具。
Jest 是一个专注于简易性的 JavaScript 测试框架。一个其独特的功能是可以为测试生成快照 (snapshot),以提供另一种验证应用单元的方法。
资料:
Mocha 是一个专注于灵活性的 JavaScript 测试框架。因为其灵活性,它允许你选择不同的库来满足诸如侦听 (如 Sinon) 和断言 (如 Chai) 等其它常见的功能。另一个 Mocha 独特的功能是它不止可以在 Node.js 里运行测试,还可以在浏览器里运行测试。
资料:
测试大多数 Vue 组件时都必须将它们挂载到 DOM (虚拟或真实) 上,才能完全断言它们正在工作。这是另一个与框架无关的概念。因此组件测试框架的诞生,是为了让用户能够以可靠的方式完成这项工作,同时还提供了 Vue 特有的诸如对 Vuex、Vue Router 和其他 Vue 插件的集成的便利性。
以下章节提供了在评估最适合你的应用的组件测试框架时需要记住的事项。
毋容置疑,最重要的标准之一就是组件测试库应该尽可能与 Vue 生态系统兼容。虽然这看起来很全面,但需要记住的一些关键集成领域包括单文件组件 (SFC)、Vuex、Vue Router 以及应用所依赖的任何其他特定于 Vue 的插件。
当测试失败时,提供有用的错误日志以最小化调试问题所需的时间对于组件测试框架来说至关重要。除了简单地告诉你什么测试失败了,他们还应额外提供上下文以及测试失败的原因,例如预期结果 vs 实际得到的结果。
Vue Testing Library 是一组专注于测试组件而不依赖实现细节的工具。由于在设计时就充分考虑了可访问性,它采用的方案也使重构变得轻而易举。
它的指导原则是,与软件使用方式相似的测试越多,它们提供的可信度就越高。
资料:
Vue Test Utils 是官方的偏底层的组件测试库,它是为用户提供对 Vue 特定 API 的访问而编写的。如果你对测试 Vue 应用不熟悉,我们建议你使用 Vue Testing Library,它是 Vue Test Utils 的抽象。
资源:
虽然单元测试为开发者提供了一定程度的信心,但是单元测试和组件测试在部署到生产环境时提供应用整体覆盖的能力是有限的。因此,端到端测试可以说从应用最重要的方面进行测试覆盖:当用户实际使用应用时会发生什么。
换句话说,端到端测试验证应用中的所有层。这不仅包括你的前端代码,还包括所有相关的后端服务和基础设施,它们更能代表你的用户所处的环境。通过测试用户操作如何影响应用,端到端测试通常是提高应用是否正常运行的信心的关键。
虽然 web 上的端到端测试因不可信赖 (片面的) 测试和减慢开发过程而得到负面的声誉,但现代端到端工具在创建更可靠的、交互的和实用的测试方面取得了长足进步。在选择端到端测试框架时,以下章节在你为应用选择测试框架时提供了一些指导。
端到端测试的一个主要优点是它能够跨多个浏览器测试应用。尽管 100% 的跨浏览器覆盖看上去很诱人,但需要注意的是,因为持续运行这些跨浏览器测试需要额外的时间和机器消耗,它会降低团队的资源回报。因此,在选择应用需要的跨浏览器测试数量时,必须注意这种权衡。
TIP
针对浏览器特定问题的一个最新进展是,针对不常用的浏览器 (如:< IE11、旧版 Safari 等) 使用应用监视和错误报告工具 (如:Sentry、LogRocket 等)。
端到端测试和开发的主要问题之一是运行整个套件需要很长时间。通常,这只在持续集成和部署 (CI/CD) 管道中完成。现代的端到端测试框架通过添加类似并行化的特性来帮助解决这个问题,这使得 CI/CD 管道的运行速度通常比以前快。此外,在本地开发时,有选择地为正在处理的页面运行单个测试的能力,同时还提供测试的热重载,将有助于提高开发者的工作流程和工作效率。
虽然开发者传统上依赖于在终端窗口中扫描日志来帮助确定测试中出了什么问题,但现代端到端测试框架允许开发者利用他们已经熟悉的工具,例如浏览器开发工具。
虽然生态系统中有许多工具,但以下是一些 Vue.js 生态系统中常用的端到端测试框架。
Cypress.io 是一个测试框架,旨在通过使开发者能够可靠地测试他们的应用,同时提供一流的开发者体验,来提高开发者的生产率。
资料:
Nightwatch.js 是一个端到端测试框架,可用于测试 web 应用和网站,以及 Node.js 单元测试和集成测试。
资料:
Puppeteer 是一个 Node.js 库,它提供高阶 API 来控制浏览器,并可以与其他测试运行程序 (例如 Jest) 配对来测试应用。
资料:
TestCafe 是一个基于端到端的 Node.js 框架,旨在提供简单的设置,以便开发者能够专注于创建易于编写和可靠的测试。
资料:
Vue CLI 提供内置的 TypeScript 工具支持。
随着应用的增长,静态类型系统可以帮助防止许多潜在的运行时错误,这就是为什么 Vue 3 是用 TypeScript 编写的。这意味着在 Vue 中使用 TypeScript 不需要任何其他工具——它具有一流的公民支持。
// tsconfig.json{ "compilerOptions": { "target": "esnext", "module": "esnext", // 这样就可以对 `this` 上的数据属性进行更严格的推断` "strict": true, "jsx": "preserve", "moduleResolution": "node" }}
请注意,必须包含 strict: true
(或至少包含 noImplicitThis: true
,它是 strict
标志的一部分) 才能在组件方法中利用 this
的类型检查,否则它总是被视为 any
类型。
参见 TypeScript 编译选项文档查看更多细节。
Vue CLI 可以生成使用 TypeScript 的新项目,开始:
# 1. Install Vue CLI, 如果尚未安装npm install --global @vue/cli@next# 2. 创建一个新项目, 选择 "Manually select features" 选项vue create my-project-name# 3. 如果已经有一个不存在TypeScript的 Vue CLI项目,请添加适当的 Vue CLI插件:vue add typescript
请确保组件的 script
部分已将语言设置为 TypeScript:
<script lang="ts"> ...</script>
对于使用 TypeScript 开发 Vue 应用程序,我们强烈建议使用 Visual Studio Code,它为 TypeScript 提供了很好的开箱即用支持。如果你使用的是单文件组件 (SFCs),那么可以使用很棒的 Vetur extension,它在 SFCs 中提供了 TypeScript 推理和许多其他优秀的特性。
WebStorm 还为 TypeScript 和 Vue 提供现成的支持。
要让 TypeScript 正确推断 Vue 组件选项中的类型,需要使用 defineComponent
全局方法定义组件:
import { defineComponent } from 'vue'const Component = defineComponent({ // 已启用类型推断})
TypeScript 应该能够在不显式定义类型的情况下推断大多数类型。例如,如果有一个具有数字 count
property 的组件,如果试图对其调用特定于字符串的方法,则会出现错误:
const Component = defineComponent({ data() { return { count: 0 } }, mounted() { const result = this.count.split('') // => Property 'split' does not exist on type 'number' }})
如果你有一个复杂的类型或接口,你可以使用 type assertion 对其进行强制转换:
interface Book { title: string author: string year: number}const Component = defineComponent({ data() { return { book: { title: 'Vue 3 Guide', author: 'Vue Team', year: 2020 } as Book } }})
由于 Vue 声明文件的循环特性,TypeScript 可能难以推断 computed 的类型。因此,你可能需要注释返回类型的计算属性。
import { defineComponent } from 'vue'const Component = defineComponent({ data() { return { message: 'Hello!' } }, computed: { // 需要注释 greeting(): string { return this.message + '!' } // 在使用setter进行计算时,需要对getter进行注释 greetingUppercased: { get(): string { return this.greeting.toUpperCase(); }, set(newValue: string) { this.message = newValue.toUpperCase(); }, }, }})
Vue 对定义了 type
的 prop 执行运行时验证。要将这些类型提供给 TypeScript,我们需要使用 PropType
强制转换构造函数:
import { defineComponent, PropType } from 'vue'interface ComplexMessage { title: string okMessage: string cancelMessage: string}const Component = defineComponent({ props: { name: String, success: { type: String }, callback: { type: Function as PropType<() => void> }, message: { type: Object as PropType<ComplexMessage>, required: true, validator(message: ComplexMessage) { return !!message.title } } }})
如果你发现验证器没有得到类型推断或者成员完成不起作用,那么用期望的类型注释参数可能有助于解决这些问题。
在 setup()
函数中,不需要将类型传递给 props
参数,因为它将从 props
组件选项推断类型。
import { defineComponent } from 'vue'const Component = defineComponent({ props: { message: { type: String, required: true } }, setup(props) { const result = props.message.split('') // 正确, 'message' 被声明为字符串 const filtered = props.message.filter(p => p.value) // 将引发错误: Property 'filter' does not exist on type 'string' }})
ref
Refs 根据初始值推断类型:
import { defineComponent, ref } from 'vue'const Component = defineComponent({ setup() { const year = ref(2020) const result = year.value.split('') // => Property 'split' does not exist on type 'number' }})
有时我们可能需要为 ref 的内部值指定复杂类型。我们可以在调用 ref 重写默认推理时简单地传递一个泛型参数:
const year = ref<string | number>('2020') // year's type: Ref<string | number>year.value = 2020 // ok!
TIP
如果泛型的类型未知,建议将 ref
转换为 Ref<T>
。
reactive
当声明类型 reactive
property,我们可以使用接口:
import { defineComponent, reactive } from 'vue'interface Book { title: string year?: number}export default defineComponent({ name: 'HelloWorld', setup() { const book = reactive<Book>({ title: 'Vue 3 Guide' }) // or const book: Book = reactive({ title: 'Vue 3 Guide' }) // or const book = reactive({ title: 'Vue 3 Guide' }) as Book }})
computed
计算值将根据返回值自动推断类型
import { defineComponent, ref, computed } from 'vue'export default defineComponent({ name: 'CounterButton', setup() { let count = ref(0) // 只读 const doubleCount = computed(() => count.value * 2) const result = doubleCount.value.split('') // => Property 'split' does not exist on type 'number' }})
虽然 Vue.js 本身并不支持移动应用开发,但是有很多解决方案可以用 Vue.js 创建原生 iOS 和 Android 应用。
Capacitor 是一个来自 Ionic Team 的项目,通过提供跨多个平台运行的 API,开发者可以使用单个代码库构建原生 iOS、Android 和 PWA 应用。
资源
NativeScript 使用已熟悉的 Web 技能为跨平台(真正的原生)移动应用提供支持。两者结合在一起是开发沉浸式移动体验的绝佳选择。
资源
对于大多数单页面应用,都推荐使用官方支持的 vue-router 库。更多细节可以移步 vue-router 文档。
如果你只需要非常简单的路由而不想引入一个功能完整的路由库,可以像这样动态渲染一个页面级的组件:
const NotFoundComponent = { template: '<p>Page not found</p>' }const HomeComponent = { template: '<p>Home page</p>' }const AboutComponent = { template: '<p>About page</p>' }const routes = { '/': HomeComponent, '/about': AboutComponent}const SimpleRouter = { data: () => ({ currentRoute: window.location.pathname }), computed: { CurrentComponent() { return routes[this.currentRoute] || NotFoundComponent } }, render() { return Vue.h(this.CurrentComponent) }}Vue.createApp(SimpleRouter).mount('#app')
结合 HTML5 History API,你可以建立一个麻雀虽小但是五脏俱全的客户端路由器。可以直接看实例应用。
如果你有更偏爱的第三方路由,如 Page.js 或者 Director,整合起来也一样简单。这里有一个使用了 Page.js 的完整示例。
由于状态零散地分布在许多组件和组件之间的交互中,大型应用复杂度也经常逐渐增长。为了解决这个问题,Vue 提供 vuex:我们有受到 Elm 启发的状态管理库。vuex 甚至集成到 vue-devtools,无需配置即可进行时光旅行调试 (time travel debugging)。
如果你是来自 React 的开发者,你可能会对 Vuex 和 Redux 间的差异表示关注,Redux 是 React 生态环境中最流行的 Flux 实现。Redux 事实上无法感知视图层,所以它能够轻松的通过一些[简单绑定_blank_nofollow](https://classic.yarnpkg.com/en/packages?q=redux vue&p=1)和 Vue 一起使用。Vuex 区别在于它是一个专门为 Vue 应用所设计。这使得它能够更好地和 Vue 进行整合,同时提供简洁的 API 和改善过的开发体验。
经常被忽略的是,Vue 应用中响应式 data
对象的实际来源——当访问数据对象时,一个组件实例只是简单的代理访问。所以,如果你有一处需要被多个实例间共享的状态,你可以使用一个 reactive 方法让对象作为响应式对象。
const sourceOfTruth = Vue.reactive({ message: 'Hello'})const appA = Vue.createApp({ data() { return sourceOfTruth }}).mount('#app-a')const appB = Vue.createApp({ data() { return sourceOfTruth }}).mount('#app-b')
<div id="app-a">App A: {{ message }}</div><div id="app-b">App B: {{ message }}</div>
现在当 sourceOfTruth
发生变更,appA
和 appB
都将自动地更新它们的视图。我们现在只有一个真实来源,但调试将是一场噩梦。我们应用的任何部分都可以随时更改任何数据,而不会留下变更过的记录。
const appB = Vue.createApp({ data() { return sourceOfTruth }, mounted() { sourceOfTruth.message = 'Goodbye' // both apps will render 'Goodbye' message now }}).mount('#app-b')
为了解决这个问题,我们采用一个简单的 store 模式:
const store = { debug: true, state: Vue.reactive({ message: 'Hello!' }), setMessageAction(newValue) { if (this.debug) { console.log('setMessageAction triggered with', newValue) } this.state.message = newValue }, clearMessageAction() { if (this.debug) { console.log('clearMessageAction triggered') } this.state.message = '' }}
需要注意,所有 store 中 state 的变更,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的变更将会发生,以及它们是如何被触发。当错误出现时,我们现在也会有一个 log 记录 bug 之前发生了什么。
此外,每个实例/组件仍然可以拥有和管理自己的私有状态:
<div id="app-a">{{sharedState.message}}</div><div id="app-b">{{sharedState.message}}</div>
const appA = Vue.createApp({ data() { return { privateState: {}, sharedState: store.state } }, mounted() { store.setMessageAction('Goodbye!') }}).mount('#app-a')const appB = Vue.createApp({ data() { return { privateState: {}, sharedState: store.state } }}).mount('#app-b')
TIP
重要的是,注意你不应该在 action 中替换原始的状态对象——组件和 store 需要引用同一个共享对象,变更才能够被观察到。
接着我们继续延伸约定,组件不允许直接变更属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,我们最终达成了 Flux 架构。这样约定的好处是,我们能够记录所有 store 中发生的 state 变更,同时实现能做到记录变更、保存状态快照、历史回滚/时光旅行的先进的调试工具。
说了一圈其实又回到了 Vuex,如果你已经读到这儿,或许可以去尝试一下!
我们创建了一份完整的构建 Vue 服务端渲染应用的指南。这份指南非常深入,适合已经熟悉 Vue、webpack 和 Node.js 开发的开发者阅读。请移步 ssr.vuejs.org。
从头搭建一个服务端渲染的应用是相当复杂的。幸运的是,我们有一个优秀的社区项目 Nuxt.js 让这一切变得非常简单。Nuxt 是一个基于 Vue 生态的更高层的框架,为开发服务端渲染的 Vue 应用提供了极其便利的开发体验。更酷的是,你甚至可以用它来做为静态站生成器。推荐尝试。
Quasar Framework 可以通过其一流的构建系统、合理的配置和开发者扩展性生成 (可选地和 PWA 互通的) SSR 应用,让你的想法的设计和构建变得轻而易举。你可以在服务端挑选执行超过上百款遵循“Material Design 2.0”的组件,并在浏览器端可用。你甚至可以管理网站的 <meta>
标签。Quasar 是一个基于 Node.js 和 webpack 的开发环境,它可以通过一套代码完成 SPA、PWA、SSR、Electron、Capacitor 和 Cordova 应用的快速开发。
Web 可访问性 (也称为 a11y) 是指创建可供任何人使用的网站的实践方式——无论是身患某种障碍、通过慢速的网络连接访问、使用老旧或损坏的硬件,还是仅仅是处于不利环境中的人。例如,在视频中添加字幕可以帮助失聪、重听或在嘈杂的环境中听不到手机的用户。同样,请确保文字对比度不要太低,这对低视力用户和那些试图在强光下使用手机的用户都有帮助。
你是否已经准备开始却又无从下手?
可以先看看由万维网联盟 (W3C) 提供的规划和管理 web 可访问性。
你应该在每个页面的顶部添加一个直接指向主内容区域的链接,这样用户就可以跳过在多个网页上重复的内容。
通常这个链接会放在 App.vue
的顶部,这样它就会是所有页面上的第一个可聚焦元素:
<ul class="skip-links"> <li> <a href="#main" ref="skipLink">跳到主内容</a> </li></ul>
若想在非聚焦状态下隐藏该链接,可以添加以下样式:
.skipLink { white-space: nowrap; margin: 1em auto; top: 0; position: fixed; left: 50%; margin-left: -72px; opacity: 0;}.skipLink:focus { opacity: 1; background-color: white; padding: .5em; border: 1px solid black;}
一旦用户改变路由,请将焦点放回到这个跳过链接。通过用如下方式聚焦 ref
即可实现:
<script>export default { watch: { $route() { this.$refs.skipLink.focus(); } }};</script>
可访问性最重要的部分之一是确保设计本身是可访问的。设计不仅要考虑颜色对比度、字体选择、文本大小和语言,还要考虑应用程序中内容的结构。
用户可以通过标题在应用程序中进行导航。为应用程序的每个部分设置描述性标题可以让用户更容易地预测每个部分的内容。说到标题,有几个推荐的可访问性实践:
<h1>
- <h6>
<main role="main" aria-labelledby="main-title"> <h1 id="main-title">Main title</h1> <section aria-labelledby="section-title"> <h2 id="section-title"> Section Title </h2> <h3>Section Subtitle</h3> <!-- 内容 --> </section> <section aria-labelledby="section-title"> <h2 id="section-title"> Section Title </h2> <h3>Section Subtitle</h3> <!-- 内容 --> <h3>Section Subtitle</h3> <!-- 内容 --> </section></main>
地标 (landmark) 会为应用中的章节提供访问规划。依赖辅助技术的用户可以跳过内容直接导航到应用程序的每个部分。你可以使用 ARIA role 帮助你实现这个目标。
HTML | ARIA Role | 地标的目的 |
---|---|---|
header | role="banner" | 主标题:页面的标题 |
nav | role="navigation" | 适合用作文档或相关文档导航的链接集合 |
main | role="main" | 文档的主体或中心内容 |
footer | role="contentinfo" | 关于父级文档的信息:脚注/版权/隐私声明链接 |
aside | role="complementary" | 用来支持主内容,同时其自身的内容是相对独立且有意义的 |
无对应元素 | role="search" | 该章节包含整个应用的搜索功能 |
form | role="form" | 表单相关元素的集合 |
section | role="region" | 相关的且用户可能会导航到的内容。必须为该元素提供 label |
Tip:
在使用地标 HTML 元素时,建议加上冗余的地标 role attribute,以最大限度地与传统不支持 HTML5 语义元素的浏览器兼容。
当创建一个表单,你可能使用到以下几个元素:<form>
、<label>
、<input>
、<textarea>
和 <button>
。
标签通常放置在表单字段的顶部或左侧:
<form action="/dataCollectionLocation" method="post" autocomplete="on"> <div v-for="item in formItems" :key="item.id" class="form-item"> <label :for="item.id">{{ item.label }}: </label> <input :type="item.type" :id="item.id" :name="item.id" v-model="item.value" /> </div> <button type="submit">Submit</button></form>
注意如何在表单元素中包含 autocomplete='on'
,它将应用于表单中的所有输入。你也可以为每个输入设置不同的自动完成属性的值。
提供标签以描述所有表单控件的用途;链接 for
和 id
:
<label for="name">Name</label><input type="text" name="name" id="name" v-model="name" />
如果你在 chrome 开发工具中检查这个元素,并打开 Elements 选项卡中的 Accessibility 选项卡,你将看到输入是如何从标签中获取其名称的:
警告:
虽然你可能已经看到这样包装输入字段的标签:
<label> Name: <input type="text" name="name" id="name" v-model="name" /></label>
辅助技术更好地支持用匹配的 id 显式设置标签。
你也可以给输入一个带有aria-label
的可访问名称。
<label for="name">Name</label><input type="text" name="name" id="name" v-model="name" :aria-label="nameLabel"/>
请随意在 Chrome DevTools 中检查此元素,以查看可访问名称是如何更改的:
使用 aria-labelledby
类似于 aria-label
,除非标签文本在屏幕上可见。它通过 id
与其他元素配对,你可以链接多个 id
:
<form class="demo" action="/dataCollectionLocation" method="post" autocomplete="on"> <h1 id="billing">Billing</h1> <div class="form-item"> <label for="name">Name:</label> <input type="text" name="name" id="name" v-model="name" aria-labelledby="billing name" /> </div> <button type="submit">Submit</button></form>
aria-describedby 的用法与 aria-labelledby
相同,预期提供了用户可能需要的附加信息的描述。这可用于描述任何输入的标准:
<form class="demo" action="/dataCollectionLocation" method="post" autocomplete="on"> <h1 id="billing">Billing</h1> <div class="form-item"> <label for="name">Full Name:</label> <input type="text" name="name" id="name" v-model="name" aria-labelledby="billing name" aria-describedby="nameDescription" /> <p id="nameDescription">Please provide first and last name.</p> </div> <button type="submit">Submit</button></form>
你可以通过使用 Chrome 开发工具来查看说明:
避免使用占位符,因为它们可能会混淆许多用户。
占位符的一个问题是默认情况下它们不符合颜色对比标准;修复颜色对比度会使占位符看起来像输入字段中预填充的数据。查看以下示例,可以看到满足颜色对比度条件的姓氏占位符看起来像预填充的数据:
最好提供用户在任何输入之外填写表单所需的所有信息。
为输入字段添加说明时,请确保将其正确链接到输入。你可以提供附加指令并在 aria-labelledby
内绑定多个 id。这使得设计更加灵活。
<fieldset> <legend>Using aria-labelledby</legend> <label id="date-label" for="date">Current Date:</label> <input type="date" name="date" id="date" aria-labelledby="date-label date-instructions" /> <p id="date-instructions">MM/DD/YYYY</p></fieldset>
或者,你可以用 aria-describedby
将指令附加到输入。
<fieldset> <legend>Using aria-describedby</legend> <label id="dob" for="dob">Date of Birth:</label> <input type="date" name="dob" id="dob" aria-describedby="dob-instructions" /> <p id="dob-instructions">MM/DD/YYYY</p></fieldset>
通常不建议直观地隐藏标签,即使输入具有可访问的名称。但是,如果输入的功能可以与周围的内容一起理解,那么我们可以隐藏视觉标签。
让我们看看这个搜索字段:
<form role="search"> <label for="search" class="hidden-visually">Search: </label> <input type="text" name="search" id="search" v-model="search" /> <button type="submit">Search</button></form>
我们可以这样做,因为搜索按钮将帮助可视化用户识别输入字段的用途。
我们可以使用 CSS 直观地隐藏元素,但可以将它们用于辅助技术:
.hidden-visually { position: absolute; overflow: hidden; white-space: nowrap; margin: 0; padding: 0; height: 1px; width: 1px; clip: rect(0 0 0 0); clip-path: inset(100%);}
添加 aria hidden="true"
将隐藏辅助技术中的元素,但使其在视觉上对其他用户可用。不要把它用在可聚焦的元素上,纯粹用于装饰性的、复制的或屏幕外的内容上。
<p>This is not hidden from screen readers.</p><p aria-hidden="true">This is hidden from screen readers.</p>
在表单中使用按钮时,必须设置类型以防止提交表单。
也可以使用输入创建按钮:
<form action="/dataCollectionLocation" method="post" autocomplete="on"> <!-- Buttons --> <button type="button">Cancel</button> <button type="submit">Submit</button> <!-- Input buttons --> <input type="button" value="Cancel" /> <input type="submit" value="Submit" /></form>
你可以使用此技术创建功能图像。
<form role="search"> <label for="search" class="hidden-visually">Search: </label> <input type="text" name="search" id="search" v-model="search" /> <input type="image" class="btnImg" src="https://img.icons8.com/search" rel="external nofollow" alt="Search" /> </form>
<form role="search"> <label for="searchIcon" class="hidden-visually">Search: </label> <input type="text" name="searchIcon" id="searchIcon" v-model="searchIcon" /> <button type="submit"> <i class="fas fa-search" aria-hidden="true"></i> <span class="hidden-visually">Search</span> </button></form>
万维网联盟 (W3C) 网络可访问性倡议 (WAI) 为不同的组件制定了 Web 可访问性标准:
WCAG 2.1 在 WCAG 2.0 上进行了扩展,允许通过处理 web 的变化来实现新技术。W3C 鼓励在开发或更新 Web 可访问性策略时使用 WCAG 的最新版本。
W3C 的 WAI-ARIA 为如何构建动态内容和高阶用户界面控件提供了指导。
世界卫生组织估计,世界 15%的人口患有某种形式的残疾,其中 2 - 4%的人严重残疾。估计全世界有 10 亿人,使残疾人成为世界上最大的少数群体。
残疾的种类繁多,大致可分为四类:
请从 WebAim 查看以下链接,以便用户了解:
INFO
刚接触 Vue.js?先从基础指南开始吧。
本指南主要是为有 Vue 2 经验的用户希望了解 Vue 3 的新功能和更改而提供的。在试用 Vue 3 之前,你不必从头阅读这些内容。虽然看起来有很多变化,但很多你已经了解和喜欢 Vue 的部分仍是一样的。不过我们希望尽可能全面,并为每处变化提供详细的例子。
开始学习 Vue 3 Vue Mastery。
<script src="https://unpkg.com/vue@next" rel="external nofollow" ></script>
npm init vite-app hello-vue3 # OR yarn create vite-app hello-vue3
npm install -g @vue/cli # OR yarn global add @vue/cli vue create hello-vue3 # select vue 3 preset
Vue 3 中需要关注的一些新功能包括:
createRenderer
API 来自 @vue/runtime-core
创建自定义渲染器提示
我们仍在开发 Vue 3 的专用迁移版本,该版本的行为与 Vue 2 兼容,运行时警告不兼容。如果你计划迁移一个非常重要的 Vue 2 应用程序,我们强烈建议你等待迁移版本完成以获得更流畅的体验。
下面列出了从 2.x 开始的重大更改:
v-model
用法已更改<template>
v-for和非 - v-for 节点上 key 用法已更改v-if
和 v-for
优先级已更改v-bind="object"
现在排序敏感v-for
中的 ref
不再注册 ref 数组$scopedSlots
property 已删除,所有插槽都通过 $slots
作为函数暴露v-enter
-> v-enter-from
v-leave
-> v-leave-from
$watch
不再支持点分隔字符串路径,请改用计算函数作为参数outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x 现在使用应用程序容器的 innerHTML
。destroyed
生命周期选项被重命名为 unmounted
beforeDestroy
生命周期选项被重命名为 beforeUnmount
default
工厂函数不再有权访问 this
是上下文data
应始终声明为函数data
选项现在可简单地合并$watch
不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。<template>
没有特殊指令的标记 (v-if/else-if/else
、v-for
或 v-slot
) 现在被视为普通元素,并将生成原生的 <template>
元素,而不是渲染其内部内容。outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器的 innerHTML
,这意味着容器本身不再被视为模板的一部分。keyCode
支持作为 v-on
的修饰符$destroy
实例方法。用户不应再手动管理单个 Vue 组件的生命周期。我们所有的官方库和工具现在都支持 Vue 3,但大多数仍然处于 beta 状态,并在 npm 的 next
dist 标签下发布。我们正计划在 2020 年底前稳定所有项目,并将其转换为使用 latest
的 dist 标签。
从 v4.5.0 开始,vue-cli
现在提供了内置选项,可在创建新项目时选择 Vue 3 预设。现在可以升级 vue-cli
并运行 vue create
来创建 Vue 3 项目。
Vue Router 4.0 提供了 Vue 3 支持,并有许多突破性的变化,查看 README 中完整的细节,
Vuex 4.0 提供了 Vue 3 支持,其 API 与 3.x 基本相同。唯一的突破性变化是插件的安装方式。
我们正在开发一个新版本的 Devtools,它有一个新的 UI 和经过重构的内部结构,以支持多个 Vue 版本。新版本目前处于测试阶段,目前只支持 Vue 3。Vuex 和路由器的集成也在进行中。
.xpi
文件)推荐使用 VSCode 和我们官方拓展 Vetur,它为 Vue 3 提供了全面的 IDE 支持
项目 | npm | 仓库 |
---|---|---|
@vue/babel-plugin-jsx | [Github] | |
eslint-plugin-vue | [Github] | |
@vue/test-utils | [Github] | |
vue-class-component | [Github] | |
vue-loader | [Github] | |
rollup-plugin-vue | [Github] |
在 Vue 2 中,在 v-for
里使用的 ref
attribute 会用 ref 数组填充相应的 $refs
property。当存在嵌套的 v-for
时,这种行为会变得不明确且效率低下。
在 Vue 3 中,这样的用法将不再在 $ref
中自动创建数组。要从单个绑定获取多个 ref,请将 ref
绑定到一个更灵活的函数上 (这是一个新特性):
<div v-for="item in list" :ref="setItemRef"></div>
结合选项式 API:
export default { data() { return { itemRefs: [] } }, methods: { setItemRef(el) { this.itemRefs.push(el) } }, beforeUpdate() { this.itemRefs = [] }, updated() { console.log(this.itemRefs) }}
结合组合式 API:
import { ref, onBeforeUpdate, onUpdated } from 'vue'export default { setup() { let itemRefs = [] const setItemRef = el => { itemRefs.push(el) } onBeforeUpdate(() => { itemRefs = [] }) onUpdated(() => { console.log(itemRefs) }) return { itemRefs, setItemRef } }}
注意:
itemRefs
不必是数组:它也可以是一个对象,其 ref 会通过迭代的 key 被设置。itemRef
也可以是响应式的且可以被监听。以下是对变化的高层次概述:
defineAsyncComponent
助手方法,用于显式地定义异步组件component
选项重命名为 loader
resolve
和 reject
参数,且必须返回一个 Promise如需更深入的解释,请继续阅读!
以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,例如:
const asyncPage = () => import('./NextPage.vue')
或者,对于带有选项的更高阶的组件语法:
const asyncPage = { component: () => import('./NextPage.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 asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))// 带选项的异步组件const asyncPageWithOptions = defineAsyncComponent({ loader: () => import('./NextPage.vue'), delay: 200, timeout: 3000, errorComponent: ErrorComponent, loadingComponent: LoadingComponent})
对 2.x 所做的另一个更改是,component
选项现在被重命名为 loader
,以便准确地传达不能直接提供组件定义的信息。
import { defineAsyncComponent } from 'vue'const asyncPageWithOptions = defineAsyncComponent({ loader: () => import('./NextPage.vue'), delay: 200, timeout: 3000, error: ErrorComponent, loading: LoadingComponent})
此外,与 2.x 不同,loader 函数不再接收 resolve
和 reject
参数,且必须始终返回 Promise。
// 2.x 版本const oldAsyncComponent = (resolve, reject) => { /* ... */}// 3.x 版本const asyncComponent = defineAsyncComponent( () => new Promise((resolve, reject) => { /* ... */ }))
有关异步组件用法的详细信息,请参阅:
信息
这是一个低级的内部 API 更改,不会影响大多数开发人员。
下面是对这些变化的高层次总结:
false
。相反,它被设置为 attr=“false”。移除 attribute,使用 null
或者 undefined
。如需更深入的解释,请继续阅读!
在 2.x,我们有以下策略来强制 v-bind
的值:
value
的 ,
,``,等等。falsy
的,Vue 会移除它们 (undefined
,null
or false
) 另外加上它们 (见这里和这里)。contenteditable
,draggable
和 spellcheck
),Vue 会尝试强制将它们串起来 (目前对 contenteditable
做了特殊处理,修复 vuejs/vue#9397)。falsy
值 (undefined
,null
,or false
) 并按原样设置其他值 (见这里)。下表描述了 Vue 如何使用普通非布尔 attribute 强制“枚举 attribute”:
绑定表达式 | foo 正常 | draggable 枚举 |
---|---|---|
:attr="null" | / | draggable="false" |
:attr="undefined" | / | / |
:attr="true" | foo="true" | draggable="true" |
:attr="false" | / | draggable="false" |
:attr="0" | foo="0" | draggable="true" |
attr="" | foo="" | draggable="true" |
attr="foo" | foo="foo" | draggable="true" |
attr | foo="" | draggable="true" |
从上表可以看出,当前实现 true
强制为 'true'
但如果 attribute 为 false
,则移除该 attribute。这也导致了不一致性,并要求用户在非常常见的用例中手动强制布尔值为字符串,例如 aria-*
attribute 像 aria-selected
,aria-hidden
,等等。
我们打算放弃“枚举 attribute”的内部概念,并将它们视为普通的非布尔 HTML attribute。
'true'
和 'false'
以外的值,甚至可以使用 contenteditable
等 attribute 的关键字`对于非布尔 attribute,如果 attribute 为 false
,Vue 将停止删除它们,相反强制它们为 'false'
。
true
和 false
之间的不一致性,并使输出 aria-*
attributes 更容易下表描述了新行为:
绑定表达式 | foo 正常 | draggable 枚举 |
---|---|---|
:attr="null" | / | / † |
:attr="undefined" | / | / |
:attr="true" | foo="true" | draggable="true" |
:attr="false" | foo="false" † | draggable="false" |
:attr="0" | foo="0" | draggable="0" † |
attr="" | foo="" | draggable="" † |
attr="foo" | foo="foo" | draggable="foo" † |
attr | foo="" | draggable="" † |
†: 变更
布尔 attributes 的强制保持不变。
缺少枚举 attribute 和 attr="false"
可能会产生不同的 IDL attribute 值 (将反映实际状态),描述如下:
缺少枚举attr | IDL attr & 值 |
---|---|
contenteditable | contentEditable → 'inherit' |
draggable | draggable → false |
spellcheck | spellcheck → true |
为了保持原有的行为,并且我们将强制使用 false
为 'false'
,在 3.x Vue 中,开发人员需要将 v-bind
表达式解析为 false
或 'false'
,表示 contenteditable
和 spellcheck
。
在 2.x 中,枚举 attribute 的无效值被强制为 'true'
。这通常是无意的,不太可能大规模依赖。在 3.x 中,应显式指定 true
或 'true'
。
false
强制为 'false'
而不是删除 attribute在 3.x,null
或 undefined
应用于显式删除 attribute。
Attributes | v-bind value 2.x | v-bind value 3.x | HTML 输出 |
---|---|---|---|
2.x “枚举attribute” i.e. contenteditable , draggable and spellcheck . | undefined , false | undefined , null | removed |
true , 'true' , '' , 1 , 'foo' | true , 'true' | "true" | |
null , 'false' | false , 'false' | "false" | |
其他非布尔attribute eg. aria-checked , tabindex , alt , etc. | undefined , null , false | undefined , null | removed |
'false' | false , 'false' | "false" |
下面是对变更的简要总结:
v-bind="$attrs"
更多信息,请继续阅读!
在 Vue 2,自定义指令是通过使用下面列出的钩子来创建的,这些钩子都是可选的
下面是一个例子:
<p v-highlight="yellow">高亮显示此文本亮黄色</p>
Vue.directive('highlight', { bind(el, binding, vnode) { el.style.background = binding.value }})
在这里,在这个元素的初始设置中,指令通过传递一个值来绑定样式,该值可以通过应用程序更新为不同的值。
然而,在 Vue 3 中,我们为自定义指令创建了一个更具凝聚力的 API。正如你所看到的,它们与我们的组件生命周期方法有很大的不同,即使我们正与类似的事件钩子,我们现在把它们统一起来了:
updated
。最终 API 如下:
const MyDirective = { beforeMount(el, binding, vnode, prevVnode) {}, mounted() {}, beforeUpdate() {}, updated() {}, beforeUnmount() {}, // 新 unmounted() {}}
生成的 API 可以这样使用,与前面的示例相同:
<p v-highlight="yellow">高亮显示此文本亮黄色</p>
const app = Vue.createApp({})app.directive('highlight', { beforeMount(el, binding, vnode) { el.style.background = binding.value }})
既然定制指令生命周期钩子映射了组件本身的那些,那么它们就更容易推理和记住了!
通常建议保持指令独立于它们所使用的组件实例。从自定义指令中访问实例通常表明该指令本身应该是一个组件。然而,在某些情况下,这实际上是有意义的。
在Vue 2中,必须通过vnode
参数访问组件实例:
bind(el, binding, vnode) { const vm = vnode.context}
在Vue 3中,实例现在是bind
的一部分:
mounted(el, binding, vnode) { const vm = binding.instance}
在 Vue 3 中,我们现在支持片段,这允许我们为每个组件返回多个 DOM 节点。你可以想象,对于具有多个 <li>
的组件或一个表的子元素这样的组件有多方便:
<template> <li>Hello</li> <li>Vue</li> <li>Devs!</li></template>
如此灵活,我们可能会遇到一个定制指令的问题,它可能有多个根节点。
因此,自定义指令现在作为虚拟 DOM 节点数据的一部分包含在内。当在组件上使用自定义指令时,钩子作为无关的 prop 传递到组件,并以 this.$attrs
结束。
这也意味着可以像这样在模板中直接挂接到元素的生命周期中,这在涉及到自定义指令时非常方便:
<div @vnodeMounted="myHook" />
这与属性 fallthrough 行为是一致的,因此,当子组件在内部元素上使用 v-bind="$attrs"
时,它也将应用对其使用的任何自定义指令。
is
prop 用法仅限于保留的 <component>
标记。v-is
指令来支持 2.x 用例,其中在原生元素上使用了 v-is
来处理原生 HTML 解析限制。如果我们想添加在 Vue 外部定义的自定义元素 (例如使用 Web 组件 API),我们需要“指示”Vue 将其视为自定义元素。让我们以下面的模板为例。
<plastic-button></plastic-button>
在 Vue 2.x 中,将标记作为自定义元素白名单是通过 Vue.config.ignoredElements
:
// 这将使Vue忽略在Vue外部定义的自定义元素// (例如:使用 Web Components API)Vue.config.ignoredElements = ['plastic-button']
在 Vue 3.0 中,此检查在模板编译期间执行指示编译器将 <plastic-button>
视为自定义元素:
isCustomElement
传递给 Vue 模板编译器,如果使用 vue-loader
,则应通过 vue-loader
的 compilerOptions
选项传递: // webpack 中的配置 rules: [ { test: /.vue$/, use: 'vue-loader', options: { compilerOptions: { isCustomElement: tag => tag === 'plastic-button' } } } // ... ]
app.config.isCustomElement
传递: const app = Vue.createApp({}) app.config.isCustomElement = tag => tag === 'plastic-button'
需要注意的是,运行时配置只会影响运行时模板编译——它不会影响预编译的模板。
自定义元素规范提供了一种将自定义元素用作自定义内置模板的方法,方法是向内置元素添加 is
属性:
<button is="plastic-button">点击我!</button>
Vue 对 is
特殊 prop 的使用是在模拟 native attribute 在浏览器中普遍可用之前的作用。但是,在 2.x 中,它被解释为渲染一个名为 plastic-button
的 Vue 组件,这将阻止上面提到的自定义内置元素的原生使用。
在 3.0 中,我们仅将 Vue 对 is
属性的特殊处理限制到 <component>
tag。
<component>
tag 上使用时,它的行为将与 2.x 中完全相同; <foo is="bar" />
bar
组件。is
prop 渲染 foo
组件。is
选项传递给 createElement
调用,并作为原生 attribute 渲染,这支持使用自定义的内置元素。 <button is="plastic-button">点击我!</button>
plastic-button
组件。 document.createElement('button', { is: 'plastic-button' })
v-is
用于 DOM 内模板解析解决方案提示:本节仅影响直接在页面的 HTML 中写入 Vue 模板的情况。 在 DOM 模板中使用时,模板受原生 HTML 解析规则的约束。一些 HTML 元素,例如
<ul&
,<ol&
,<table&
和<select&
对它们内部可以出现的元素有限制,和一些像<li&
,<tr&
,和<option&
只能出现在某些其他元素中。
在 Vue 2 中,我们建议通过在原生 tag 上使用 is
prop 来解决这些限制:
<table> <tr is="blog-post-row"></tr></table>
随着 is
的行为变化,我们引入了一个新的指令 v-is
,用于解决这些情况:
<table> <tr v-is="'blog-post-row'"></tr></table>
WARNING
v-is
函数像一个动态的 2.x :is
绑定——因此,要按注册名称渲染组件,其值应为 JavaScript 字符串文本:
<!-- 不正确,不会渲染任何内容 --><tr v-is="blog-post-row"></tr><!-- 正确 --><tr v-is="'blog-post-row'"></tr>
config.ignoredElements
与 vue-loader
的 compilerOptions
(使用 build 步骤) 或 app.config.isCustomElement
(使用动态模板编译)<component>
tags 与 is
用法更改为 <component is="...">
(对于 SFC 模板) 或 v-is
(对于 DOM 模板)。data
组件选项声明不再接收纯 JavaScript object
,而需要 function
声明。当合并来自 mixin 或 extend 的多个 data
返回值时,现在是浅层次合并的而不是深层次合并的(只合并根级属性)。
在 2.x 中,开发者可以定义 data
选项是 object
或者是 function
。
例如:
<!-- 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
。
使用上面的示例,代码只有一个可能的实现:
<script> import { createApp } from 'vue' createApp({ data() { return { apiKey: 'a1b2c3' } } }).mount('#app')</script>
此外,当来自组件的 data()
及其 mixin 或 extends 基类被合并时,现在将浅层次执行合并:
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 }}
对于依赖对象声明的用户,我们建议:
data
中的 property对于依赖 mixin 的深度合并行为的用户,我们建议重构代码以完全避免这种依赖,因为 mixin 的深度合并非常隐式,这让代码逻辑更难理解和调试。
$on
,$off
和 $once
实例方法已被移除,应用实例不再实现事件触发接口。
在 2.x 中,Vue 实例可用于触发通过事件触发 API 强制附加的处理程序 ($on
,$off
和 $once
),这用于创建 event hub,以创建在整个应用程序中使用的全局事件侦听器:
// eventHub.jsconst eventHub = new Vue()export default eventHub
// ChildComponent.vueimport eventHub from './eventHub'export default { mounted() { // 添加 eventHub listener eventHub.$on('custom-event', () => { console.log('Custom event triggered!') }) }, beforeDestroy() { // 移除 eventHub listener eventHub.$off('custom-event') }}
// ParentComponent.vueimport eventHub from './eventHub'export default { methods: { callGlobalCustomEvent() { eventHub.$emit('custom-event') // 如果ChildComponent mounted,控制台中将显示一条消息 } }}
我们整个从实例中移除了 $on
,$off
和 $once
方法,$emit
仍然是现有 API 的一部分,因为它用于触发由父组件以声明方式附加的事件处理程序
例如,可以通过使用实现事件发射器接口的外部库来替换现有的 event hub mitt。
在兼容性构建中也可以支持这些方法。
从 Vue 3.0 开始,过滤器已删除,不再支持。
在 2.x,开发者可以使用过滤器来处理通用文本格式。
例如:
<template> <h1>Bank Account Balance</h1> <p>{{ accountBalance | currencyUSD }}</p></template><script> export default { props: { accountBalance: { type: Number, required: true } }, filters: { currencyUSD(value) { return '$' + value } } }</script>
虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是“只是 JavaScript”的假设,这不仅有学习成本,而且有实现成本。
在 3.x 中,过滤器已删除,不再支持。相反地,我们建议用方法调用或计算属性替换它们。
使用上面的例子,这里是一个如何实现它的例子。
<template> <h1>Bank Account Balance</h1> <p>{{ accountInUSD }}</p></template><script> export default { props: { accountBalance: { type: Number, required: true } }, computed: { accountInUSD() { return '$' + this.accountBalance } } }</script>
我们建议用计算属性或方法代替过滤器,而不是使用过滤器。
如果在应用中全局注册了过滤器,那么在每个组件中用计算属性或方法调用来替换它可能就没那么方便了。
相反地,你可以通过全局属性在所有组件中使用它:
// main.jsconst app = createApp(App)app.config.globalProperties.$filters = { currencyUSD(value) { return '$' + value }}
然后,你可以通过 $filters
对象修改所有的模板,像下面这样:
<template> <h1>Bank Account Balance</h1> <p>{{ $filters.currencyUSD(accountBalance) }}</p></template>
注意,这种方式只能用于方法中,不可以在计算属性中使用,因为后者只有在单个组件的上下文中定义时才有意义。
在 Vue 3 中,组件现在正式支持多根节点组件,即片段!
在 2.x 中,不支持多根组件,当用户意外创建多根组件时会发出警告,因此,为了修复此错误,许多组件被包装在一个 <div>
中。
<!-- Layout.vue --><template> <div> <header>...</header> <main>...</main> <footer>...</footer> </div></template>
在 3.x 中,组件现在可以有多个根节点!但是,这确实要求开发者明确定义属性应该分布在哪里。
<!-- Layout.vue --><template> <header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer></template>
有关 attribute 继承如何工作的详细信息,见非 Prop Attributes。
就变化而言,属于高等级内容:
props
和 context
的普通函数创建 (即:slots
,attrs
,emit
)。functional
attribute 在单文件组件 (SFC) <template>
已被移除{ functional: true }
选项在通过函数创建组件已被移除更多信息,请继续阅读!
在 Vue 2 中,函数式组件有两个主要用例:
然而,在 Vue 3 中,有状态组件的性能已经提高到可以忽略不计的程度。此外,有状态组件现在还包括返回多个根节点的能力。
因此,函数式组件剩下的唯一用例就是简单组件,比如创建动态标题的组件。否则,建议你像平常一样使用有状态组件。
使用 <dynamic-heading>
组件,负责提供适当的标题 (即:h1
,h2
,h3
,等等),在 2.x 中,这可能是作为单个文件组件编写的:
// Vue 2 函数式组件示例export default { functional: true, props: ['level'], render(h, { props, data, children }) { return h(`h${props.level}`, data, children) }}
或者,对于喜欢在单个文件组件中使用 <template>
的用户:
// Vue 2 函数式组件示例使用 <template><template functional> <component :is="`h${props.level}`" v-bind="attrs" v-on="listeners" /></template><script>export default { props: ['level']}</script>
现在在 Vue 3 中,所有的函数式组件都是用普通函数创建的,换句话说,不需要定义 { functional: true }
组件选项。
他们将接收两个参数:props
和 context
。context
参数是一个对象,包含组件的 attrs
,slots
,和 emit
property。
此外,现在不是在 render
函数中隐式提供 h
,而是全局导入 h
。
使用前面提到的 <dynamic-heading>
组件的示例,下面是它现在的样子。
import { h } from 'vue'const DynamicHeading = (props, context) => { return h(`h${props.level}`, context.attrs, context.slots)}DynamicHeading.props = ['level']export default DynamicHeading
在 3.x 中,有状态组件和函数式组件之间的性能差异已经大大减少,并且在大多数用例中是微不足道的。因此,在 SFCs 上使用 functional
的开发人员的迁移路径是删除该 attribute,并将 props
的所有引用重命名为 $props
,将 attrs
重命名为 $attrs
。
使用之前的 <dynamic-heading>
示例,下面是它现在的样子。
<template> <component v-bind:is="`h${$props.level}`" v-bind="$attrs" /></template><script>export default { props: ['level']}</script>
主要的区别在于:
functional
attribute 在 <template>
中移除listeners
现在作为 $attrs
的一部分传递,可以将其删除有关新函数式组件的用法和对渲染函数的更改的详细信息,见:
Vue 2.x 有许多全局 API 和配置,这些 API 和配置可以全局改变 Vue 的行为。例如,要创建全局组件,可以使用 Vue.component
这样的 API:
Vue.component('button-counter', { data: () => ({ count: 0 }), template: '<button @click="count++">Clicked {{ count }} times.</button>'})
类似地,使用全局指令的声明方式如下:
Vue.directive('focus', { inserted: el => el.focus()})
虽然这种声明方式很方便,但它也会导致一些问题。从技术上讲,Vue 2 没有“app”的概念,我们定义的应用只是通过 new Vue()
创建的根 Vue 实例。从同一个 Vue 构造函数创建的每个根实例共享相同的全局配置,因此:
Vue.config.errorHandler
)。有些 API 像 Vue.use
以及 Vue.mixin
甚至连恢复效果的方法都没有,这使得涉及插件的测试特别棘手。实际上,vue-test-utils 必须实现一个特殊的 API createLocalVue
来处理此问题:import { createLocalVue, mount } from '@vue/test-utils'// 建扩展的 `Vue` 构造函数const localVue = createLocalVue()// 在 “local” Vue构造函数上 “全局” 安装插件localVue.use(MyPlugin)// 通过 `localVue` 来挂载选项mount(Component, { localVue })
// 这会影响两个根实例 Vue.mixin({ /* ... */ }) const app1 = new Vue({ el: '#app-1' }) const app2 = new Vue({ el: '#app-2' })
为了避免这些问题,在 Vue 3 中我们引入...
createApp
调用 createApp
返回一个应用实例,这是 Vue 3 中的新概念:
import { createApp } from 'vue'const app = createApp({})
应用实例暴露当前全局 API 的子集,经验法则是,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上,以下是当前全局 API 及其相应实例 API 的表:
2.x 全局 API | 3.x 实例 API (app ) |
---|---|
Vue.config | app.config |
Vue.config.productionTip | removed (见下方) |
Vue.config.ignoredElements | app.config.isCustomElement (见下方) |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use (见下方) |
所有其他不全局改变行为的全局 API 现在被命名为 exports,文档见全局 API Treeshaking。
config.productionTip
移除在 Vue 3.x 中,“使用生产版本”提示仅在使用“dev + full build”(包含运行时编译器并有警告的构建) 时才会显示。
对于 ES 模块构建,由于它们是与 bundler 一起使用的,而且在大多数情况下,CLI 或样板已经正确地配置了生产环境,所以本技巧将不再出现。
config.ignoredElements
替换为 config.isCustomElement
引入此配置选项的目的是支持原生自定义元素,因此重命名可以更好地传达它的功能,新选项还需要一个比旧的 string/RegExp 方法提供更多灵活性的函数:
// beforeVue.config.ignoredElements = ['my-el', /^ion-/]// afterconst app = Vue.createApp({})app.config.isCustomElement = tag => tag.startsWith('ion-')
重要
在 3.0 中,元素是否是组件的检查已转移到模板编译阶段,因此只有在使用运行时编译器时才考虑此配置选项。如果你使用的是 runtime-only 版本 isCustomElement
必须通过 @vue/compiler-dom
在构建步骤替换——比如,通过 compilerOptions
option in vue-loader。
config.isCustomElement
当使用仅运行时构建时时,将发出警告,指示用户在生成设置中传递该选项;插件开发者通常使用 Vue.use
。例如,官方的 vue-router
插件是如何在浏览器环境中自行安装的:
var inBrowser = typeof window !== 'undefined'/* … */if (inBrowser && window.Vue) { window.Vue.use(VueRouter)}
由于 use
全局 API 在 Vue 3 中不再使用,此方法将停止工作并停止调用 Vue.use()
现在将触发警告,于是,开发者必须在应用程序实例上显式指定使用此插件:
const app = createApp(MyApp)app.use(VueRouter)
使用 createApp(/* options */)
初始化后,应用实例 app
可用于挂载具有 app.mount(domTarget)
:
import { createApp } from 'vue'import MyApp from './MyApp.vue'const app = createApp(MyApp)app.mount('#app')
经过所有这些更改,我们在指南开头的组件和指令将被改写为如下内容:
const app = createApp(MyApp)app.component('button-counter', { data: () => ({ count: 0 }), template: '<button @click="count++">Clicked {{ count }} times.</button>'})app.directive('focus', { mounted: el => el.focus()})// 现在所有应用实例都挂载了,与其组件树一起,将具有相同的 “button-counter” 组件 和 “focus” 指令不污染全局环境app.mount('#app')
与在 2.x 根实例中使用 provide
选项类似,Vue 3 应用实例还可以提供可由应用内的任何组件注入的依赖项:
// 在入口app.provide('guide', 'Vue 3 Guide')// 在子组件export default { inject: { book: { from: 'guide' } }, template: `<div>{{ book }}</div>`}
在应用之间共享配置 (如组件或指令) 的一种方法是创建工厂功能,如下所示:
import { createApp } from 'vue'import Foo from './Foo.vue'import Bar from './Bar.vue'const createMyApp = options => { const app = createApp(options) app.directive('focus' /* ... */) return app}createMyApp(Foo).mount('#foo')createMyApp(Bar).mount('#bar')
现在,Foo 和 Bar 实例及其后代中都可以使用 focus
指令。
如果你曾经在 Vue 中手动操作过 DOM,你可能会遇到以下模式:
import Vue from 'vue'Vue.nextTick(() => { // 一些和DOM有关的东西})
或者,如果你一直在对涉及 async components 的应用程序进行单元测试,那么很可能你编写了以下内容:
import { shallowMount } from '@vue/test-utils'import { MyComponent } from './MyComponent.vue'test('an async feature', async () => { const wrapper = shallowMount(MyComponent) // 执行一些DOM相关的任务 await wrapper.vm.$nextTick() // 运行你的断言})
Vue.nextTick()
是一个全局的 API 直接暴露在单个 Vue 对象上——事实上,实例方法 $nextTick()
只是一个方便的包装 Vue.nextTick()
为方便起见,回调的 this
上下文自动绑定到当前实例。
模块捆绑程序,如 webpack 支持 tree-shaking,这是“死代码消除”的一个花哨术语。不幸的是,由于代码是如何在以前的 Vue 版本中编写的,全局 API Vue.nextTick()
不可摇动,将包含在最终捆绑中不管它们实际在哪里使用。
在 Vue 3 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。例如,我们之前的片段现在应该如下所示:
import { nextTick } from 'vue'nextTick(() => { // 一些和DOM有关的东西})
and
import { shallowMount } from '@vue/test-utils'import { MyComponent } from './MyComponent.vue'import { nextTick } from 'vue'test('an async feature', async () => { const wrapper = shallowMount(MyComponent) // 执行一些DOM相关的任务 await nextTick() // 运行你的断言})
直接调用 Vue.nextTick()
将导致臭名昭著的 undefined is not a function
错误。
通过这一更改,如果模块绑定器支持 tree-shaking,则 Vue 应用程序中未使用的全局 api 将从最终捆绑包中消除,从而获得最佳的文件大小。
Vue 2.x 中的这些全局 API 受此更改的影响:
Vue.nextTick
Vue.observable
(用 Vue.reactive
替换)Vue.version
Vue.compile
(仅全构建)Vue.set
(仅兼容构建)Vue.delete
(仅兼容构建)除了公共 api,许多内部组件/帮助器现在也被导出为命名导出,只有当编译器的输出是这些特性时,才允许编译器导入这些特性,例如以下模板:
<transition> <div v-show="ok">hello</div></transition>
被编译为类似于以下的内容:
import { h, Transition, withDirectives, vShow } from 'vue'export function render() { return h(Transition, [withDirectives(h('div', 'hello'), [[vShow, this.ok]])])}
这实际上意味着只有在应用程序实际使用了 Transition
组件时才会导入它。换句话说,如果应用程序没有任何 Transition
组件,那么支持此功能的代码将不会出现在最终的捆绑包中。
随着全局 tree-shaking,用户只需为他们实际使用的功能“付费”,更好的是,知道了可选特性不会增加不使用它们的应用程序的捆绑包大小,框架大小在将来已经不再是其他核心功能的考虑因素了,如果有的话。
重要
以上仅适用于 ES Modules builds,用于支持 tree-shaking 的绑定器——UMD 构建仍然包括所有特性,并暴露 Vue 全局变量上的所有内容 (编译器将生成适当的输出,以使用全局外的 api 而不是导入)。
如果你的插件依赖受影响的 Vue 2.x 全局 API,例如:
const plugin = { install: Vue => { Vue.nextTick(() => { // ... }) }}
在 Vue 3 中,必须显式导入:
import { nextTick } from 'vue'const plugin = { install: app => { nextTick(() => { // ... }) }}
如果使用 webpack 这样的模块捆绑包,这可能会导致 Vue 的源代码绑定到插件中,而且通常情况下,这并不是你所期望的。防止这种情况发生的一种常见做法是配置模块绑定器以将 Vue 从最终捆绑中排除。对于 webpack,你可以使用 externals
配置选项:
// webpack.config.jsmodule.exports = { /*...*/ externals: { vue: 'Vue' }}
这将告诉 webpack 将 Vue 模块视为一个外部库,而不是捆绑它。
如果你选择的模块绑定器恰好是 Rollup,你基本上可以免费获得相同的效果,因为默认情况下,Rollup 会将绝对模块 id (在我们的例子中为 'vue'
) 作为外部依赖项,而不会将它们包含在最终的 bundle 中。但是在绑定期间,它可能会发出一个“将 vue 作为外部依赖” 警告,可使用 external
选项抑制该警告:
// rollup.config.jsexport default { /*...*/ external: ['vue']}
对内联特性的支持已被移除。
在 2.x 中,Vue 为子组件提供了 inline-template
attribute,以便将其内部内容用作模板,而不是将其作为分发内容。
<my-component inline-template> <div> <p>它们被编译为组件自己的模板</p> <p>不是父级所包含的内容。</p> </div></my-component>
将不再支持此功能。
inline-template
的大多数用例都假设没有构建工具设置,所有模板都直接写在 HTML 页面中
<script>
标签在这种情况下,最简单的解决方法是将 <script>
与其他类型一起使用:
<script type="text/html" id="my-comp-template"> <div>{{ hello }}</div></script>
在组件中,使用选择器将模板作为目标:
const MyComp = { template: '#my-comp-template' // ...}
这不需要任何构建设置,可以在所有浏览器中工作,不受任何 DOM HTML 解析警告的约束 (例如,你可以使用 camelCase prop 名称),并且在大多数 ide 中提供了正确的语法高亮显示。在传统的服务器端框架中,可以将这些模板拆分为服务器模板部分 (包括在主 HTML 模板中),以获得更好的可维护性。
以前使用 inline-template
的组件也可以使用默认 slot——进行重构,这使得数据范围更加明确,同时保留了内联编写子内容的便利性:
<!-- 2.x 语法 --><my-comp inline-template :msg="parentMsg"> {{ msg }} {{ childState }}</my-comp><!-- 默认 Slot 版本 --><my-comp v-slot="{ childState }"> {{ parentMsg }} {{ childState }}</my-comp>
子级现在应该渲染默认 slot*,而不是不提供模板:
<!-- 在子模板中,在传递时渲染默认slot 在必要的private状态下。--><template> <slot :childState="childState" /></template>
- 提示:在 3.x,slot 可以渲染为具有原生 fragments 支持的根目录!
v-if
/v-else
/v-else-if
的各分支项key
将不再是必须的,因为现在 Vue 会自动生成唯一的key
。key
,那么每个分支必须使用唯一的 key
。你不能通过故意使用相同的 key
来强制重用分支。<template v-for>
的 key
应该设置在 <template>
标签上 (而不是设置在它的子节点上)。特殊的 key
attribute 被用于提示 Vue 的虚拟 DOM 算法来保持对节点身份的持续跟踪。这样 Vue 可以知道何时能够重用和修补现有节点,以及何时需要对它们重新排序或重新创建。关于其它更多信息,可以查看以下章节:
Vue 2.x 建议在 v-if
/v-else
/v-else-if
的分支中使用 key
。
<!-- Vue 2.x --><div v-if="condition" key="yes">Yes</div><div v-else key="no">No</div>
这个示例在 Vue 3.x 中仍能正常工作。但是我们不再建议在 v-if
/v-else
/v-else-if
的分支中继续使用 key
attribute,因为没有为条件分支提供 key
时,也会自动生成唯一的 key
。
<!-- Vue 3.x --><div v-if="condition">Yes</div><div v-else>No</div>
非兼容变更体现在如果你手动提供了 key
,那么每个分支都必须使用一个唯一的 key
。因此大多数情况下都不需要设置这些 key
。
<!-- Vue 2.x --><div v-if="condition" key="a">Yes</div><div v-else key="a">No</div><!-- Vue 3.x (recommended solution: remove keys) --><div v-if="condition">Yes</div><div v-else>No</div><!-- Vue 3.x (alternate solution: make sure the keys are always unique) --><div v-if="condition" key="a">Yes</div><div v-else key="b">No</div>
<template v-for>
在 Vue 2.x 中 <template>
标签不能拥有 key
。不过你可以为其每个子节点分别设置 key
。
<!-- Vue 2.x --><template v-for="item in list"> <div :key="item.id">...</div> <span :key="item.id">...</span></template>
在 Vue 3.x 中 key
则应该被设置在 <template>
标签上。
<!-- Vue 3.x --><template v-for="item in list" :key="item.id"> <div>...</div> <span>...</span></template>
类似地,当使用 <template v-for>
时存在使用 v-if
的子节点,key
应改为设置在 <template>
标签上。
<!-- Vue 2.x --><template v-for="item in list"> <div v-if="item.isVisible" :key="item.id">...</div> <span v-else :key="item.id">...</span></template><!-- Vue 3.x --><template v-for="item in list" :key="item.id"> <div v-if="item.isVisible">...</div> <span v-else>...</span></template>
以下是变更的简要总结:
v-on
修饰符config.keyCodes
在 Vue 2 中,支持 keyCodes
作为修改 v-on
方法的方法。
<!-- 键码版本 --><input v-on:keyup.13="submit" /><!-- 别名版本 --><input v-on:keyup.enter="submit" />
此外,你可以通过全局 config.keyCodes
选项。
Vue.config.keyCodes = { f1: 112}
<!-- 键码版本 --><input v-on:keyup.112="showHelpText" /><!-- 自定别名版本 --><input v-on:keyup.f1="showHelpText" />
从KeyboardEvent.keyCode
has been deprecated 开始,Vue 3 继续支持这一点就不再有意义了。因此,现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 大小写名称。
<!-- Vue 3 在 v-on 上使用 按键修饰符 --><input v-on:keyup.delete="confirmDelete" />
因此,这意味着 config.keyCodes
现在也已弃用,不再受支持。
对于那些在代码库中使用 keyCode
的用户,我们建议将它们转换为它们的 kebab-cased (短横线) 命名对齐。
生成 prop 默认值的工厂函数不再能访问 this
。
替代方案:
import { inject } from 'vue'export default { props: { theme: { default (props) { // `props` 是传递给组件的原始值。 // 在任何类型/默认强制转换之前 // 也可以使用 `inject` 来访问注入的 property return inject('theme', 'default-theme') } } }}
此更改不会影响 <template>
用户。
以下是更改的简要总结:
h
现在全局导入,而不是作为参数传递给渲染函数更多信息,请继续阅读!
在 2.x 中,e render
函数将自动接收 h
函数 (它是 createElement
的常规别名) 作为参数:
// Vue 2 渲染函数示例export default { render(h) { return h('div') }}
在 3.x 中,h
现在是全局导入的,而不是作为参数自动传递。
// Vue 3 渲染函数示例import { h } from 'vue'export default { render() { return h('div') }}
在 2.x 中,render
函数自动接收诸如 h
之类的参数。
// Vue 2 渲染函数示例export default { render(h) { return h('div') }}
在 3.x 中,由于 render
函数不再接收任何参数,它将主要用于 setup()
函数内部。这还有一个好处:可以访问作用域中声明的响应式状态和函数,以及传递给 setup()
的参数。
import { h, reactive } from 'vue'export default { setup(props, { slots, attrs, emit }) { const state = reactive({ count: 0 }) function increment() { state.count++ } // 返回render函数 return () => h( 'div', { onClick: increment }, state.count ) }}
有关 setup()
如何工作的详细信息,参考组合式 API 指南。
在 2.x 中,domProps
包含 VNode props 中的嵌套列表:
// 2.x{ class: ['button', 'is-outlined'], style: { color: '#34495E' }, attrs: { id: 'submit' }, domProps: { innerHTML: '' }, on: { click: submitForm }, key: 'submit-button'}
在 3.x 中,整个 VNode props 结构是扁平的,使用上面的例子,下面是它现在的样子
// 3.x 语法{ class: ['button', 'is-outlined'], style: { color: '#34495E' }, id: 'submit', innerHTML: '', onClick: submitForm, key: 'submit-button'}
全局导入 h
意味着任何包含 Vue 组件的库都将在某处包含 import { h } from 'vue'
,因此,这会带来一些开销,因为它需要库作者在其构建设置中正确配置 Vue 的外部化:
见 Render 函数指南更详细的文档!
此更改统一了 3.x 中的普通 slot 和作用域 slot。
以下是变化的变更总结:
this.$slots
现在将 slots 作为函数公开this.$scopedSlots
更多信息,请继续阅读!
当使用渲染函数时,即 h
,2.x 用于在内容节点上定义 slot
data property。
// 2.x 语法h(LayoutComponent, [ h('div', { slot: 'header' }, this.header), h('div', { slot: 'content' }, this.content)])
此外,在引用作用域 slot 时,可以使用以下方法引用它们:
// 2.x 语法this.$scopedSlots.header
在 3.x 中,插槽被定义为当前节点的子对象:
// 3.x Syntaxh(LayoutComponent, {}, { header: () => h('div', this.header), content: () => h('div', this.content)})
当你需要以编程方式引用作用域 slot 时,它们现在被统一到 $slots
选项中。
// 2.x 语法this.$scopedSlots.header// 3.x 语法this.$slots.header
大部分更改已经在 2.6 中发布。因此,迁移可以一步到位:
this.$scopedSlots
替换为 this.$slots
。过渡类名 v-enter
修改为 v-enter-from
、过渡类名 v-leave
修改为 v-leave-from
。
在v2.1.8版本之前, 为过渡指令提供了两个过渡类名对应初始和激活状态。
在 v2.1.8 版本中, 引入 v-enter-to
来定义 enter 或 leave 变换之间的过渡动画插帧, 为了向下兼容, 并没有变动 v-enter
类名:
.v-enter,.v-leave-to { opacity: 0;}.v-leave,.v-enter-to { opacity: 1;}
这样做会带来很多困惑, 类似 enter 和 leave 含义过于宽泛并且没有遵循类名钩子的命名约定。
为了更加明确易读,我们现在将这些初始状态重命名为:
.v-enter-from,.v-leave-to { opacity: 0;}.v-leave-from,.v-enter-to { opacity: 1;}
现在,这些状态之间的区别就清晰多了。
<transition>
组件相关属性名也发生了变化:
leave-class
已经被重命名为 leave-from-class
(在渲染函数或 JSX 中可以写为:leaveFromClass
)enter-class
已经被重命名为 enter-from-class
(在渲染函数或 JSX 中可以写为:enterFromClass
).v-enter
字符串实例替换为 .v-enter-from
.v-leave
字符串实例替换为 .v-leave-from
就变化内容而言,此部分属于高阶内容:
v-model
prop 和事件默认名称已更改:value
-> modelValue
;input
-> update:modelValue
;v-bind
的 .sync
修饰符和组件的 model
选项已移除,可用 v-model
作为代替;v-model
进行双向绑定;v-model
修饰符。更多信息,请见下文。
在 Vue 2.0 发布后,开发者使用 v-model
指令必须使用为 value
的 prop。如果开发者出于不同的目的需要使用其他的 prop,他们就不得不使用 v-bind.sync
。此外,由于v-model
和 value
之间的这种硬编码关系的原因,产生了如何处理原生元素和自定义元素的问题。
在 Vue 2.2 中,我们引入了 model
组件选项,允许组件自定义用于 v-model
的 prop 和事件。但是,这仍然只允许在组件上使用一个 model
。
在 Vue 3 中,双向数据绑定的 API 已经标准化,减少了开发者在使用 v-model
指令时的混淆并且在使用 v-model
指令时可以更加灵活。
在 2.x 中,在组件上使用 v-model
相当于绑定 value
prop 和 input
事件:
<ChildComponent v-model="pageTitle" /><!-- 简写: --><ChildComponent :value="pageTitle" @input="pageTitle = $event" />
如果要将属性或事件名称更改为其他名称,则需要在 ChildComponent
组件中添加 model
选项:
<!-- ParentComponent.vue --><ChildComponent v-model="pageTitle" />
// ChildComponent.vueexport default { model: { prop: 'title', event: 'change' }, props: { // 这将允许 `value` 属性用于其他用途 value: String, // 使用 `title` 代替 `value` 作为 model 的 prop title: { type: String, default: 'Default title' } }}
所以,在这个例子中 v-model
的简写如下:
<ChildComponent :title="pageTitle" @change="pageTitle = $event" />
v-bind.sync
在某些情况下,我们可能需要对某一个 prop 进行“双向绑定”(除了前面用 v-model
绑定 prop 的情况)。为此,我们建议使用 update:myPropName
抛出事件。例如,对于在上一个示例中带有 title
prop 的 ChildComponent
,我们可以通过下面的方式将分配新 value 的意图传达给父级:
this.$emit('update:title', newValue)
如果需要的话,父级可以监听该事件并更新本地 data property。例如:
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
为了方便起见,我们可以使用 .sync
修饰符来缩写,如下所示:
<ChildComponent :title.sync="pageTitle" />
在 3.x 中,自定义组件上的 v-model
相当于传递了 modelValue
prop 并接收抛出的 update:modelValue
事件:
<ChildComponent v-model="pageTitle" /><!-- 简写: --><ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event"/>
v-model
参数若需要更改 model
名称,而不是更改组件内的 model
选项,那么现在我们可以将一个 argument 传递给 model
:
<ChildComponent v-model:title="pageTitle" /><!-- 简写: --><ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
这也可以作为 .sync
修饰符的替代,而且允许我们在自定义组件上使用多个 v-model
。
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" /><!-- 简写: --><ChildComponent :title="pageTitle" @update:title="pageTitle = $event" :content="pageContent" @update:content="pageContent = $event"/>
v-model
修饰符除了像 .trim
这样的 2.x 硬编码的 v-model
修饰符外,现在 3.x 还支持自定义修饰符:
<ChildComponent v-model.capitalize="pageTitle" />
我们可以在 Custom Events 部分中了解有关自定义 v-model
修饰符的更多信息。
我们推荐:
.sync
的部分并将其替换为 v-model
: <ChildComponent :title.sync="pageTitle" /> <!-- 替换为 --> <ChildComponent v-model:title="pageTitle" />
v-model
,请确保分别将 prop 和 event 命名更改为 modelValue
和 update:modelValue
<ChildComponent v-model="pageTitle" />
// ChildComponent.vue export default { props: { modelValue: String // 以前是`value:String` }, methods: { changePageTitle(title) { this.$emit('update:modelValue', title) // 以前是 `this.$emit('input', title)` } } }
更多新的 v-model
语法相关信息,请参考:
在元素上动态绑定 attribute 时,常见的场景是在一个元素中同时使用 v-bind="object"
语法和单独的 property。然而,这就引出了关于合并的优先级的问题。
在 2.x,如果一个元素同时定义了 v-bind="object"
和一个相同的单独的 property,那么这个单独的 property 总是会覆盖 object
中的绑定。
<!-- template --><div id="red" v-bind="{ id: 'blue' }"></div><!-- result --><div id="red"></div>
在 3.x,如果一个元素同时定义了 v-bind="object"
和一个相同的单独的 property,那么声明绑定的顺序决定了它们如何合并。换句话说,相对于假设开发者总是希望单独的 property 覆盖 object
中定义的内容,现在开发者对自己所希望的合并行为有了更好的控制。
<!-- template --><div id="red" v-bind="{ id: 'blue' }"></div><!-- result --><div id="blue"></div><!-- template --><div v-bind="{ id: 'blue' }" id="red"></div><!-- result --><div id="red"></div>
如果你依赖 v-bind
的覆盖功能,目前的建议是确保在单独的 property 之前定义 v-bind
attribute。
译者:本章节大部分内容是针对母语是英文的读者,中文用户可略读,除非你想以英文文档编写者的身份参与 Vue docs 的编写,
编写文档是一种换位思考的练习。我们并不是在描述客观现实——源代码已经做到了。我们的工作是帮助塑造用户与 Vue 生态系统之间的关系。这份不断发展的指南提供了一些规则和建议,说明如何在 Vue 生态系统中始终如一地做到这一点。
<BlogPost>
组件例子比 <ComponentA>
更好。<BlogPost>
组件例子比 <CurrencyExchangeSettings>
更好。attribute
优于 attr
,message
优于 msg
),除非你在 API 中明确引用了缩写 (例如 $attrs
)。标准键盘上包含的缩写符号 (例如,@
,#
,&
) 可以。:
) 结束句子,而不是句点 (.
)。比如:
我们有一些专用的样式来表示需要以特定方式突出显示的内容。这些被捕获为在这个页面请谨慎使用。
滥用这些样式是有一定诱惑力的,因为你可以简单地在标注中添加更改。但是,这会破坏用户的阅读流程,因此,只能在特殊情况下使用。在可能的情况下,我们应该尝试在页面内创建一个叙述和流程,以尊重读者的认知负荷。
在任何情况下都不应该相邻使用两个警告,这表明我们无法很好地解释上下文。
我们欣赏小型、集中的 PR。如果你想进行非常大的更改,请在发起请求之前与团队成员沟通。这是一份详细说明为什么这一点如此重要的书面材料让我们在这个团队里工作得很好。请理解,尽管我们总是很感激你的贡献,但最终我们必须优先考虑哪些对整个项目最有效。
本指南将概述可用于创建文档的不同设计元素。
VuePress 提供了一个自定义容器插件来创建警稿框。有四种类型:
Markdown 范例
::: infoYou can find more information at this site.:::::: tipThis is a great tip to remember!:::::: warningThis is something to be cautious of.:::::: danger DANGERThis is something we do not recommend. Use at your own risk.:::
渲染 Markdown
INFO
你可以在这个网站上找到更多信息。
TIP
这是一个值得记住的好提示!
WARNING
这是需要谨慎的。
DANGER
这是我们不推荐的。使用风险自负。
VuePress 使用 Prism 提供语言语法高亮显示,方法是将语言附加到代码块的起始反撇号:
Markdown 示例
```jsexport default { name: 'MyComponent'}```
渲染输出
export default { name: 'MyComponent'}
向代码块添加行高亮显示,需要在大括号中附加行号。
Markdown 示例
```js{2}export default { name: 'MyComponent', props: { type: String, item: Object }}```
渲染 Markdown
export default { name: 'MyComponent', props: { type: String, item: Object }}
```js{4-5}export default { name: 'MyComponent', props: { type: String, item: Object }}```
export default { name: 'MyComponent', props: { type: String, item: Object }}
```js{2,4-5}export default { name: 'MyComponent', props: { type: String, item: Object }}```
export default { name: 'MyComponent', props: { type: String, item: Object }}
Vue 已经遍布全球,核心团队至少在 6 个不同的时区。论坛包括 7 种语言和计数,我们的许多文档积极维护翻译。我们为 Vue 的国际影响力感到骄傲,但我们可以做得更好。
目前,VUE3 文档仍处于测试阶段,随时可能更改。因此,我们会谨慎对待任何重要的工作,因为我们仍在收集反馈,并根据需要重新编写。当文档处于发布候选阶段时,我们将确保发布公告,以便你可以开始使用!
开始的最好方法是检查此处固定 issues Vuejs.org 其中包含了对社区内各种倡议的积极讨论。
config
是一个包含了 Vue 应用全局配置的对象。你可以在应用挂载前修改其以下 property:
const app = Vue.createApp({})app.config = {...}
Function
undefined
app.config.errorHandler = (err, vm, info) => { // 处理错误 // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子}
指定一个处理函数,来处理组件渲染方法执行期间以及侦听器抛出的未捕获错误。这个处理函数被调用时,可获取错误信息和应用实例。
Function
undefined
app.config.warnHandler = function(msg, vm, trace) { // `trace` 是组件的继承关系追踪}
为 Vue 的运行时警告指定一个自定义处理函数。注意这只会在开发环境下生效,在生产环境下它会被忽略。
[key: string]: any
undefined
app.config.globalProperties.foo = 'bar'app.component('child-component', { mounted() { console.log(this.foo) // 'bar' }})
添加可以在应用程序内的任何组件实例中访问的全局 property。属性名冲突时,组件的 property 将具有优先权。
这可以代替 Vue 2.x Vue.prototype
扩展:
// 之前(Vue 2.x)Vue.prototype.$http = () => {}// 之后(Vue 3.x)const app = Vue.createApp({})app.config.globalProperties.$http = () => {}
(tag: string) => boolean
undefined
// 任何以“ion-”开头的元素都将被识别为自定义元素app.config.isCustomElement = tag => tag.startsWith('ion-')
指定一个方法,用来识别在 Vue 之外定义的自定义元素(例如,使用 Web Components API)。如果组件符合此条件,则不需要本地或全局注册,并且 Vue 不会抛出关于 Unknown custom element
的警告。
注意,所有原生 HTML 和 SVG 标记不需要在此函数中匹配——Vue 解析器自动执行此检查。
{ [key: string]: Function }
{}
const app = Vue.createApp({ mounted() { console.log(this.$options.hello) }})app.config.optionMergeStrategies.hello = (parent, child, vm) => { return `Hello, ${child}`}app.mixin({ hello: 'Vue'})// 'Hello, Vue
为自定义选项定义合并策略。
合并策略选项分别接收在父实例和子实例上定义的该选项的值作为第一个和第二个参数,引用上下文实例被作为第三个参数传入。
boolean
false
设置为 true
以在浏览器开发工具的 performance/timeline 面板中启用对组件初始化、编译、渲染和更新的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器。
在 Vue 3 中,改变全局 Vue 行为的 API 现在被移动到了由新的 createApp
方法所创建的应用实例上。此外,现在它们的影响仅限于该特定应用实例:
import { createApp } from 'vue'const app = createApp({})
调用 createApp
返回一个应用实例。该实例提供了一个应用上下文。应用实例挂载的整个组件树共享相同的上下文,该上下文提供了之前在 Vue 2.x 中“全局”的配置。
另外,由于 createApp
方法返回应用实例本身,因此可以在其后链式调用其它方法,这些方法可以在以下部分中找到。
{string} name
{Function | Object} [definition]
definition
参数,返回应用实例。definition
参数,返回组件定义。注册或检索全局组件。注册还会使用给定的 name
参数自动设置组件的 name
。
import { createApp } from 'vue'const app = createApp({})// 注册一个名为my-component的组件app.component('my-component', { /* ... */})// 检索注册的组件(始终返回构造函数)const MyComponent = app.component('my-component', {})
包含应用配置的对象。
import { createApp } from 'vue'const app = createApp({})app.config = {...}
{string} name
{Function | Object} [definition]
definition
参数,返回应用实例。definition
参数,返回指令定义。注册或检索全局指令。
import { createApp } from 'vue'const app = createApp({})// 注册app.directive('my-directive', { // 指令是具有一组生命周期的钩子: // 在绑定元素的父组件挂载之前调用 beforeMount() {}, // 绑定元素的父组件挂载时调用 mounted() {}, // 在包含组件的 VNode 更新之前调用 beforeUpdate() {}, // 在包含组件的 VNode 及其子组件的 VNode 更新之后调用 updated() {}, // 在绑定元素的父组件卸载之前调用 beforeUnmount() {}, // 卸载绑定元素的父组件时调用 unmounted() {}})// 注册 (功能指令)app.directive('my-directive', () => { // 这将被作为 `mounted` 和 `updated` 调用})// getter, 如果已注册,则返回指令定义const myDirective = app.directive('my-directive')
指令钩子传递了这些参数:
指令绑定到的元素。这可用于直接操作 DOM。
包含以下 property 的对象。
instance
:使用指令的组件实例。value
:传递给指令的值。例如,在 v-my-directive="1 + 1"
中,该值为 2
。oldValue
:先前的值,仅在 beforeUpdate
和 updated
中可用。值是否已更改都可用。arg
:参数传递给指令 (如果有)。例如在 v-my-directive:foo
中,arg 为 "foo"
。modifiers
:包含修饰符 (如果有) 的对象。例如在 v-my-directive.foo.bar
中,修饰符对象为 {foo: true,bar: true}
。dir
:一个对象,在注册指令时作为参数传递。例如,在以下指令中app.directive('focus', { mounted(el) { el.focus() }})
dir
将会是以下对象:
{ mounted(el) { el.focus() }}
上面作为 el 参数收到的真实 DOM 元素的蓝图。
上一个虚拟节点,仅在 beforeUpdate
和 updated
钩子中可用。
Note
除了 el
之外,你应该将这些参数视为只读,并且永远不要修改它们。如果你需要跨钩子共享信息,建议通过元素的自定义数据属性集进行共享。
{Object} mixin
在整个应用范围内应用混入。一旦注册,它们就可以在当前的应用中任何组件模板内使用它。插件作者可以使用此方法将自定义行为注入组件。不建议在应用代码中使用。
{Element | string} rootContainer
{boolean} isHydrate
将应用实例的根组件挂载在提供的 DOM 元素上。
<body> <div id="my-app"></div></body>
import { createApp } from 'vue'const app = createApp({})// 做一些必要的准备app.mount('#my-app')
{string | Symbol} key
value
设置一个可以被注入到应用范围内所有组件中的值。组件应该使用 inject
来接收提供的值。
从 provide
/inject
的角度来看,可以将应用程序视为根级别的祖先,而根组件是其唯一的子级。
该方法不应该与 provide 组件选项或组合式 API 中的 provide 方法混淆。虽然它们也是相同的 provide
/inject
机制的一部分,但是是用来配置组件提供的值而不是应用提供的值。
通过应用提供值在写插件时尤其有用,因为插件一般不能使用组件提供值。这是使用 globalProperties 的替代选择。
Note
provide
和 inject
绑定不是响应式的。这是有意为之。不过,如果你向下传递一个响应式对象,这个对象上的 property 会保持响应式。
向根组件中注入一个 property,值由应用提供。
import { createApp } from 'vue'const app = createApp({ inject: ['user'], template: ` <div> {{ user }} </div> `})app.provide('user', 'administrator')
{Element | string} rootContainer
在提供的 DOM 元素上卸载应用实例的根组件。
<body> <div id="my-app"></div></body>
import { createApp } from 'vue'const app = createApp({})// 做一些必要的准备app.mount('#my-app')// 挂载5秒后,应用将被卸载setTimeout(() => app.unmount('#my-app'), 5000)
{Object | Function} plugin
...options (可选)
安装 Vue.js 插件。如果插件是一个对象,它必须暴露一个 install
方法。如果它本身是一个函数,它将被视为安装方法。
该安装方法将以应用实例作为第一个参数被调用。传给 use
的其他 options
参数将作为后续参数传入该安装方法。
当在同一个插件上多次调用此方法时,该插件将仅安装一次。
返回一个提供应用上下文的应用实例。应用实例挂载的整个组件树共享同一个上下文。
const app = Vue.createApp({})
你可以在 createApp
之后链式调用其它方法,这些方法可以在应用 API 中找到。
该函数接收一个根组件选项对象作为第一个参数:
const app = Vue.createApp({ data() { return { ... } }, methods: {...}, computed: {...} ...})
使用第二个参数,我们可以将根 prop 传递给应用程序:
const app = Vue.createApp( { props: ['username'] }, { username: 'Evan' })
<div id="app"> <!-- 会显示 'Evan' --> {{ username }}</div>
interface Data { [key: string]: unknown}export type CreateAppFunction<HostElement> = ( rootComponent: PublicAPIComponent, rootProps?: Data | null) => App<HostElement>
返回一个”虚拟节点“,通常缩写为 VNode:一个普通对象,其中包含向 Vue 描述它应在页面上渲染哪种节点的信息,包括所有子节点的描述。它的目的是用于手动编写的渲染函数:
render() { return Vue.h('h1', {}, 'Some title')}
接收三个参数:type
,props
和 children
String | Object | Function
HTML 标签名、组件或异步组件。使用返回 null 的函数将渲染一个注释。此参数是必需的。
Object
一个对象,与我们将在模板中使用的 attribute、prop 和事件相对应。可选。
String | Array | Object
子代 VNode,使用 h()
生成,或者使用字符串来获取“文本 VNode”,或带有插槽的对象。可选。
h('div', {}, [ 'Some text comes first.', h('h1', 'A headline'), h(MyComponent, { someProp: 'foobar' }) ])
从实现上看,defineComponent
只返回传递给它的对象。但是,就类型而言,返回的值有一个合成类型的构造函数,用于手动渲染函数、TSX 和 IDE 工具支持。
具有组件选项的对象
import { defineComponent } from 'vue'const MyComponent = defineComponent({ data() { return { count: 1 } }, methods: { increment() { this.count++ } }})
或者是一个 setup
函数,函数名称将作为组件名称来使用
import { defineComponent, ref } from 'vue'const HelloWorld = defineComponent(function HelloWorld() { const count = ref(0) return { count }})
创建一个只有在需要时才会加载的异步组件。
对于基本用法,defineAsyncComponent
可以接受一个返回 Promise
的工厂函数。Promise 的 resolve
回调应该在服务端返回组件定义后被调用。你也可以调用 reject(reason)
来表示加载失败。
import { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent(() => import('./components/AsyncComponent.vue'))app.component('async-component', AsyncComp)
当使用局部注册时,你也可以直接提供一个返回 Promise
的函数:
import { createApp, defineAsyncComponent } from 'vue'createApp({ // ... components: { AsyncComponent: defineAsyncComponent(() => import('./components/AsyncComponent.vue') ) }})
对于高阶用法,defineAsyncComponent
可以接受一个对象:
defineAsyncComponent
方法还可以返回以下格式的对象:
import { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent({ // 工厂函数 loader: () => import('./Foo.vue') // 加载异步组件时要使用的组件 loadingComponent: LoadingComponent, // 加载失败时要使用的组件 errorComponent: ErrorComponent, // 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms) delay: 200, // 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件 // 默认值:Infinity(即永不超时,单位 ms) timeout: 3000, // 定义组件是否可挂起 | 默认值:true suspensible: false, /** * * @param {*} error 错误信息对象 * @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试 * @param {*} fail 一个函数,指示加载程序结束退出 * @param {*} attempts 允许的最大重试次数 */ onError(error, retry, fail, attempts) { if (error.message.match(/fetch/) && attempts <= 3) { // 请求发生错误时重试,最多可尝试 3 次 retry() } else { // 注意,retry/fail 就像 promise 的 resolve/reject 一样: // 必须调用其中一个才能继续错误处理。 fail() } }})
参考:动态和异步组件
WARNING
resolveComponent
只能在 render
或 setup
函数中使用。
如果在当前应用实例中可用,则允许按名称解析 component
。
返回一个 Component
。如果没有找到,则返回 undefined
。
const app = Vue.createApp({})app.component('MyComponent', { /* ... */})
import { resolveComponent } from 'vue'render() { const MyComponent = resolveComponent('MyComponent')}
接受一个参数:name
String
已加载的组件的名称。
WARNING
resolveDynamicComponent
只能在 render
或 setup
函数中使用。
允许使用与 <component :is="">
相同的机制来解析一个 component
。
返回已解析的 Component
或新创建的 VNode
,其中组件名称作为节点标签。如果找不到 Component
,将发出警告。
import { resolveDynamicComponent } from 'vue'render () { const MyComponent = resolveDynamicComponent('MyComponent')}
接受一个参数:component
String | Object (组件的选项对象)
有关详细信息,请参阅动态组件上的文档。
WARNING
resolveDirective
只能在 render
或 setup
函数中使用。
如果在当前应用实例中可用,则允许通过其名称解析一个 directive
。
返回一个 Directive
。如果没有找到,则返回 undefined
。
const app = Vue.createApp({})app.directive('highlight', {})
import { resolveDirective } from 'vue'render () { const highlightDirective = resolveDirective('highlight')}
接受一个参数:name
String
已加载的指令的名称。
WARNING
withDirectives
只能在 render
或 setup
函数中使用。
允许将指令应用于 VNode。返回一个包含应用指令的 VNode。
import { withDirectives, resolveDirective } from 'vue'const foo = resolveDirective('foo')const bar = resolveDirective('bar')return withDirectives(h('div'), [ [foo, this.x], [bar, this.y]])
接受两个参数:vnode
和 directives
。
vnode
一个虚拟节点,通常使用 h()
创建。
Array
一个指令数组。
每个指令本身都是一个数组,最多可以定义 4 个索引,如以下示例所示。
[directive]
- 该指令本身。必选。 const MyDirective = resolveDirective('MyDirective') const nodeWithDirectives = withDirectives(h('div'), [[MyDirective]])
[directive, value]
- 上述内容,再加上分配给指令的类型为 any
的值。 const MyDirective = resolveDirective('MyDirective') const nodeWithDirectives = withDirectives(h('div'), [[MyDirective, 100]])
[directive, value, arg]
- 上述内容,再加上一个 string
参数,比如:在 v-on:click
中的 click
。 const MyDirective = resolveDirective('MyDirective') const nodeWithDirectives = withDirectives(h('div'), [ [MyDirective, 100, 'click'] ])
[directive, value, arg, modifiers]
- 上述内容,再加上定义任何修饰符的 key: value
键值对 Object
。 const MyDirective = resolveDirective('MyDirective') const nodeWithDirectives = withDirectives(h('div'), [ [MyDirective, 100, 'click', { prevent: true }] ])
createRenderer 函数接受两个泛型参数: HostNode
和 HostElement
,对应于宿主环境中的 Node 和 Element 类型。
例如,对于 runtime-dom,HostNode 将是 DOM Node
接口,HostElement 将是 DOM Element
接口。
自定义渲染器可以传入特定于平台的类型,如下所示:
import { createRenderer } from 'vue'const { render, createApp } = createRenderer<Node, Element>({ patchProp, ...nodeOps})
接受两个参数:HostNode
和 HostElement
。
Node
宿主环境中的节点。
Element
宿主环境中的元素。
将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。
import { createApp, nextTick } from 'vue'const app = createApp({ setup() { const message = ref('Hello!') const changeMessage = async newMessage => { message.value = newMessage await nextTick() console.log('Now DOM is updated') } }})
Function
返回组件实例的 data 对象的函数。在 data
中,我们不建议观察具有自身状态行为的对象,如浏览器 API 对象和原型 property。一个好主意是这里只有一个表示组件 data 的普通对象。
一旦观察过,你就无法在根数据对象上添加响应式 property。因此推荐在创建实例之前,就声明所有的根级响应式 property。
实例创建之后,可以通过 vm.$data
访问原始数据对象。组件实例也代理了 data 对象上所有的 property,因此访问 vm.a
等价于访问 vm.$data.a
。
以 _
或 $
开头的 property 不会被组件实例代理,因为它们可能和 Vue 内置的 property、API 方法冲突。你可以使用例如 vm.$data._property
的方式访问这些 property。
// 直接创建一个实例 const data = { a: 1 } // 这个对象将添加到组件实例中 const vm = Vue.createApp({ data() { return data } }).mount('#app') console.log(vm.a) // => 1
注意,如果你为 data property 使用了箭头函数,则 this
不会指向这个组件的实例,不过你仍然可以将其实例作为函数的第一个参数来访问。
data: vm => ({ a: vm.myProp })
Array<string> | Object
props 可以是数组或对象,用于接收来自父组件的数据。props 可以是简单的数组,或者使用对象作为替代,对象允许配置高阶选项,如类型检测、自定义验证和设置默认值。
你可以基于对象的语法使用以下选项:
type
:可以是下列原生构造函数中的一种:String
、Number
、Boolean
、Array
、Object
、Date
、Function
、Symbol
、任何自定义构造函数、或上述内容组成的数组。会检查一个 prop 是否是给定的类型,否则抛出警告。Prop 类型的更多信息在此。default
:any
为该 prop 指定一个默认值。如果该 prop 没有被传入,则换做用这个值。对象或数组的默认值必须从一个工厂函数返回required
:Boolean
义该 prop 是否是必填项。在非生产环境中,如果这个值为 truthy 且该 prop 没有被传入的,则一个控制台警告将会被抛出。validator
:Function
自定义验证函数会将该 prop 的值作为唯一的参数代入。在非生产环境下,如果该函数返回一个 falsy 的值 (也就是验证失败),一个控制台警告将会被抛出。你可以在这里查阅更多 prop 验证的相关信息。 const app = Vue.createApp({}) // 简单语法 app.component('props-demo-simple', { props: ['size', 'myMessage'] }) // 对象语法,提供验证 app.component('props-demo-advanced', { props: { // 类型检查 height: Number, // 类型检查 + 其他验证 age: { type: Number, default: 0, required: true, validator: value => { return value >= 0 } } } })
{ [key: string]: Function | { get: Function, set: Function } }
计算属性将被混入到组件实例中。所有 getter 和 setter 的 this
上下文自动地绑定为组件实例。
注意,如果你为一个计算属性使用了箭头函数,则 this
不会指向这个组件的实例,不过你仍然可以将其实例作为函数的第一个参数来访问。
computed: { aDouble: vm => vm.a * 2 }
计算属性的结果会被缓存,除非依赖的响应式 property 变化才会重新计算。注意,如果某个依赖 (比如非响应式 property) 在该实例范畴之外,则计算属性是不会被更新的。
const app = Vue.createApp({ data() { return { a: 1 } }, computed: { // 仅读取 aDouble() { return this.a * 2 }, // 读取和设置 aPlus: { get() { return this.a + 1 }, set(v) { this.a = v - 1 } } } }) const vm = app.mount('#app') console.log(vm.aPlus) // => 2 vm.aPlus = 3 console.log(vm.a) // => 2 console.log(vm.aDouble) // => 4
{ [key: string]: Function }
methods 将被混入到组件实例中。可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用。方法中的 this
自动绑定为组件实例。
注意
注意,不应该使用箭头函数来定义 method 函数 (例如 plus:() => this.a++)。理由是箭头函数绑定了父级作用域的上下文,所以 this
将不会按照期望指向组件实例,this.a
将是 undefined。
const app = Vue.createApp({ data() { return { a: 1 } }, methods: { plus() { this.a++ } } }) const vm = app.mount('#app') vm.plus() console.log(vm.a) // => 2
{ [key: string]: string | Function | Object | Array}
一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。组件实例将会在实例化时调用 $watch()
,参阅 $watch,了解更多关于 deep
、immediate
和 flush
选项的信息。
const app = Vue.createApp({ data() { return { a: 1, b: 2, c: { d: 4 }, e: 'test', f: 5 } }, watch: { a(val, oldVal) { console.log(`new: ${val}, old: ${oldVal}`) }, // 字符串方法名 b: 'someMethod', // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深 c: { handler(val, oldVal) { console.log('c changed') }, deep: true }, // 该回调将会在侦听开始之后被立即调用 e: { handler(val, oldVal) { console.log('e changed') }, immediate: true }, // 你可以传入回调数组,它们会被逐一调用 f: [ 'handle1', function handle2(val, oldVal) { console.log('handle2 triggered') }, { handler: function handle3(val, oldVal) { console.log('handle3 triggered') } /* ... */ } ] }, methods: { someMethod() { console.log('b changed') }, handle1() { console.log('handle 1 triggered') } } }) const vm = app.mount('#app') vm.a = 3 // => new: 3, old: 1
注意
注意,不应该使用箭头函数来定义 watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue)
)。理由是箭头函数绑定了父级作用域的上下文,所以 this
将不会按照期望指向组件实例,this.updateAutocomplete
将是 undefined。
Array<string> | Object
emits 可以是数组或对象,从组件触发自定义事件,emits 可以是简单的数组,或者对象作为替代,允许配置和事件验证。
在对象语法中,每个 property 的值可以为 null
或验证函数。验证函数将接收传递给 $emit
调用的其他参数。如果 this.$emit('foo',1)
被调用,foo
的相应验证函数将接收参数 1
。验证函数应返回布尔值,以表示事件参数是否有效。
const app = Vue.createApp({}) // 数组语法 app.component('todo-item', { emits: ['check'], created() { this.$emit('check') } }) // 对象语法 app.component('reply-form', { emits: { // 没有验证函数 click: null, // 带有验证函数 submit: payload => { if (payload.email && payload.password) { return true } else { console.warn(`Invalid submit event payload!`) return false } } } })
注意
emits
选项中列出的事件不会从组件的根元素继承,也将从 $attrs
property 中移除。
string
一个字符串模板作为 component 实例的标识使用。模板将会替换挂载的元素。挂载元素的内容都将被忽略,除非模板的内容有分发插槽。
如果值以 #
开始,则它将被用作 querySelector
,并使用匹配元素的 innerHTML 作为模板。常用的技巧是用 <script type="x-template">
包含模板。
注意
出于安全考虑,你应该只使用你信任的 Vue 模板。避免使用其他人生成的内容作为你的模板。
注意
如果 Vue 选项中包含渲染函数,该模板将被忽略。
Function
字符串模板的另一种选择,允许你充分利用 JavaScript 的编程功能。
<div id="app" class="demo"> <my-title blog-title="A Perfect Vue"></my-title> </div>
const app = Vue.createApp({}) app.component('my-title', { render() { return Vue.h( 'h1', // 标签名称 this.blogTitle // 标签内容 ) }, props: { blogTitle: { type: String, required: true } } }) app.mount('#app')
注意
render
函数的优先级高于从挂载元素 template
选项或内置 DOM 提取出的 HTML 模板编译渲染函数。
注意
所有的生命周期钩子自动绑定 this
上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos()
) 。这是因为箭头函数绑定了父上下文,因此 this
与你期待的组件实例不同,this.fetchTodos
的行为未定义。
Function
在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
Function
在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el
property 目前尚不可用。
Function
在挂载开始之前被调用:相关的 render
函数首次被调用。
该钩子在服务器端渲染期间不被调用。
Function
实例被挂载后调用,这时 Vue.createApp({}).mount()
被新创建的 vm.$el
替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el
也在文档内。
注意 mounted
不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted
内部使用 vm。$nextTick:
mounted() { this.$nextTick(function () { // 仅在渲染整个视图之后运行的代码 }) }
该钩子在服务器端渲染期间不被调用。
Function
数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务端进行。
Function
由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或侦听器取而代之。
注意,updated
不会保证所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以在 updated
里使用 vm.$nextTick:
updated() { this.$nextTick(function () { // 仅在渲染整个视图之后运行的代码 }) }
该钩子在服务器端渲染期间不被调用。
Function
被 keep-alive 缓存的组件激活时调用。
该钩子在服务器端渲染期间不被调用。
Function
被 keep-alive 缓存的组件停用时调用。
该钩子在服务器端渲染期间不被调用。
Function
在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
该钩子在服务器端渲染期间不被调用。
Function
卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
该钩子在服务器端渲染期间不被调用。
(err: Error, instance: Component, info: string) => ?boolean
当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false
以阻止该错误继续向上传播。
TIP
你可以在此钩子中修改组件的状态。因此在捕获错误时,在模板或渲染函数中有一个条件判断来绕过其它内容就很重要;不然该组件可能会进入一个无限的渲染循环。
错误传播规则
config.errorHandler
被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。errorCaptured
钩子,则它们将会被相同的错误逐个唤起。errorCaptured
钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的 config.errorHandler
。errorCaptured
钩子能够返回 false
以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的 errorCaptured
钩子和全局的 config.errorHandler
。(e: DebuggerEvent) => void
跟踪虚拟 DOM 重新渲染时调用。钩子接收 debugger event
作为参数。此事件告诉你哪个操作跟踪了组件以及该操作的目标对象和键。
<div id="app"> <button v-on:click="addToCart">Add to cart</button> <p>Cart({{ cart }})</p> </div>
const app = Vue.createApp({ data() { return { cart: 0 } }, renderTracked({ key, target, type }) { console.log({ key, target, type }) /* 当组件第一次渲染时,这将被记录下来: { key: "cart", target: { cart: 0 }, type: "get" } */ }, methods: { addToCart() { this.cart += 1 } } }) app.mount('#app')
(e: DebuggerEvent) => void
当虚拟 DOM 重新渲染为 triggered.Similarly 为renderTracked
,接收 debugger event
作为参数。此事件告诉你是什么操作触发了重新渲染,以及该操作的目标对象和键。
<div id="app"> <button v-on:click="addToCart">Add to cart</button> <p>Cart({{ cart }})</p> </div>
const app = Vue.createApp({ data() { return { cart: 0 } }, renderTriggered({ key, target, type }) { console.log({ key, target, type }) }, methods: { addToCart() { this.cart += 1 /* 这将导致renderTriggered调用 { key: "cart", target: { cart: 1 }, type: "set" } */ } } }) app.mount('#app')
Array<Object>
mixins
选项接收一个混入对象的数组。这些混入对象可以像正常的实例对象一样包含实例选项,这些选项将会被合并到最终的选项中,使用特定的选项合并逻辑。例如,如果 mixin 包含一个 created
钩子,而创建组件本身也有一个,那么两个函数都会被调用。
Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
const mixin = { created: function() { console.log(1) } } Vue.createApp({ created() { console.log(2) }, mixins: [mixin] }) // => 1 // => 2
Object | Function
允许声明扩展另一个组件 (可以是一个简单的选项对象或构造函数)。这主要是为了便于扩展单文件组件。
这和 mixins
类似。
const CompA = { ... } // 在没有调用 `Vue.extend` 时候继承 CompA const CompB = { extends: CompA, ... }
Object | () => Object
Array<string> | { [key: string]: string | Symbol | Object }
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的 context
特性很相似。
provide
选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol
和 Reflect.ownKeys
的环境下可工作。
inject
选项应该是:
from
property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)default
property 是降级情况下使用的 value提示:提示:
provide
和inject
绑定并不是响应式的。这是刻意为之的。然而,如果你传入了一个响应式的对象,那么其对象的 property 仍是响应式的。
// 父级组件提供 'foo' const Provider = { provide: { foo: 'bar' } // ... } // 子组件注入 'foo' const Child = { inject: ['foo'], created() { console.log(this.foo) // => "bar" } // ... }
利用 ES2015 Symbols、函数 provide
和对象 inject
:
const s = Symbol() const Provider = { provide() { return { [s]: 'foo' } } } const Child = { inject: { s } // ... }
使用一个注入的值作为一个 property 的默认值:
const Child = { inject: ['foo'], props: { bar: { default() { return this.foo } } } }
使用一个注入的值作为数据入口:
const Child = { inject: ['foo'], data() { return { bar: this.foo } } }
注入可以通过设置默认值使其变成可选项:
const Child = { inject: { foo: { default: 'foo' } } }
如果它需要从一个不同名字的 property 注入,则使用 from
来表示其源 property:
const Child = { inject: { foo: { from: 'bar', default: 'foo' } } }
与 prop 的默认值类似,你需要对非原始值使用一个工厂方法:
const Child = { inject: { foo: { from: 'bar', default: () => [1, 2, 3] } } }
Function
setup
函数是一个新的组件选项。它作为在组件内部使用组合式 API 的入口点。
在创建组件实例时,在初始 prop 解析之后立即调用 setup
。在生命周期方面,它是在 beforeCreate 钩子之前调用的。
如果 setup
返回一个对象,则该对象的属性将合并到组件模板的渲染上下文中:
<template> <div>{{ count }} {{ object.foo }}</div> </template> <script> import { ref, reactive } from 'vue' export default { setup() { const count = ref(0) const object = reactive({ foo: 'bar' }) // 暴露到template中 return { count, object } } } </script>
请注意,从 setup
返回的 refs 在模板中访问时会自动展开,因此模板中不需要 .value
。
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:
import { h, ref, reactive } from 'vue' export default { setup() { const count = ref(0) const object = reactive({ foo: 'bar' }) return () => h('div', [count.value, object.foo]) } }
该函数将接收到的 prop 作为其第一个参数:
export default { props: { name: String }, setup(props) { console.log(props.name) } }
请注意,此 props
对象是响应式的——即在传入新的 props 时会对其进行更新,并且可以通过使用 watchEffect
或 watch
进行观测和响应:
export default { props: { name: String }, setup(props) { watchEffect(() => { console.log(`name is: ` + props.name) }) } }
但是,请不要解构 props
对象,因为它会失去响应式:
export default { props: { name: String }, setup({ name }) { watchEffect(() => { console.log(`name is: ` + name) // 没有响应式 }) } }
props
对象在开发过程中对于用户区代码是不可变的 (如果用户代码尝试对其进行更改,则会发出警告)。
第二个参数提供了一个上下文对象,该对象暴露了以前在 this
上暴露的 property 的选择列表:
const MyComponent = { setup(props, context) { context.attrs context.slots context.emit } }
attrs
和 slots
是内部组件实例上相应值的代理。这样可以确保它们即使在更新后也始终会显示最新值,以便我们可以对它们进行结构分解,而不必担心访问老的引用:
const MyComponent = { setup(props, { attrs }) { // 稍后可能会调用的函数 function onClick() { console.log(attrs.foo) // 保证是最新引用 } } }
有很多理由将 props
作为单独的第一个参数而不是将其包含在上下文中:
props
比其他 property 更常见,并且很多情况下组件仅使用 props
。props
作为单独的参数可以使单独键入更容易,而不会弄乱上下文中其他 property 的类型。这也使得在具有 TSX 支持的 setup
、render
和普通功能组件之间保持一致的签名成为可能。string
允许组件模板递归地调用自身。注意,组件在全局用 Vue.createApp({}).component({})
注册时,全局 ID 自动作为组件的 name。
指定 name
选项的另一个好处是便于调试。有名字的组件有更友好的警告信息。另外,当在有 vue-devtools,未命名组件将显示成 <AnonymousComponent>
,这很没有语义。通过提供 name
选项,可以获得更有语义信息的组件树。
Array<string>
['{{', '}}']
Vue.createApp({ // Delimiters changed to ES6 template string style delimiters: ['${', '}'] })
boolean
true
默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs
到 false
,这些默认行为将会被去掉。而通过实例 property $attrs
可以让这些 attribute 生效,且可以通过 v-bind
显性的绑定到非根元素上。
app.component('base-input', { inheritAttrs: false, props: ['label', 'value'], emits: ['input'], template: ` <label> {{ label }} <input v-bind="$attrs" v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > </label> ` })
Object
组件实例观察的数据对象。组件实例代理了对其 data 对象 property 的访问。
Object
当前组件接收到的 props 对象。组件实例代理了对其 props 对象 property 的访问。
any
组件实例使用的根 DOM 元素。对于使用片段的组件,$el
将是Vue用于跟踪组件在DOM中位置的占位符DOM节点。建议使用模板引用直接访问DOM元素,而不是依赖$el
。
Object
用于当前组件实例的初始化选项。需要在选项中包含自定义 property 时会有用处:
const app = Vue.createApp({ customOption: 'foo', created() { console.log(this.$options.customOption) // => 'foo' } })
Vue instance
父实例,如果当前实例有的话。
Vue instance
当前组件树的根组件实例。如果当前实例没有父实例,此实例将会是其自己。
{ [name: string]: (...args: any[]) => Array<VNode> | undefined }
用来访问被插槽分发的内容。每个具名插槽有其相应的 property (例如:v-slot:foo
中的内容将会在 this.$slots.foo
中被找到)。default
property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default
的内容。
在使用渲染函数书写一个组件时,访问 this.$slots
最有帮助。
<blog-post> <template v-slot:header> <h1>About Me</h1> </template> <template v-slot:default> <p> Here's some page content, which will be included in $slots.default. </p> </template> <template v-slot:footer> <p>Copyright 2020 Evan You</p> </template> </blog-post>
const app = Vue.createApp({}) app.component('blog-post', { render() { return Vue.h('div', [ Vue.h('header', this.$slots.header()), Vue.h('main', this.$slots.default()), Vue.h('footer', this.$slots.footer()) ]) } })
Object
一个对象,持有注册过 ref
attribute 的所有 DOM 元素和组件实例。
Object
包含了父作用域中不作为组件 props 或自定义事件。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs"
传入内部组件——在创建高阶的组件时非常有用。
{string | Function} source
{Function | Object} callback
{Object} [options]
{boolean} deep
{boolean} immediate
{string} flush
{Function} unwatch
侦听组件实例上的响应式 property 或函数计算结果的变化。回调函数得到的参数为新值和旧值。我们只能将顶层的 data
、prop
或 computed
property 名作为字符串传递。对于更复杂的表达式,用一个函数取代。
const app = Vue.createApp({ data() { return { a: 1, b: 2, c: { d: 3, e: 4 } } }, created() { // 顶层property 名 this.$watch('a', (newVal, oldVal) => { // 做点什么 }) // 用于监视单个嵌套property 的函数 this.$watch( () => this.c.d, (newVal, oldVal) => { // 做点什么 } ) // 用于监视复杂表达式的函数 this.$watch( // 表达式 `this.a + this.b` 每次得出一个不同的结果时 // 处理函数都会被调用。 // 这就像监听一个未被定义的计算属性 () => this.a + this.b, (newVal, oldVal) => { // 做点什么 } ) } })
当侦听的值是一个对象或者数组时,对其属性或元素的任何更改都不会触发侦听器,因为它们引用相同的对象/数组:
const app = Vue.createApp({ data() { return { article: { text: 'Vue is awesome!' }, comments: ['Indeed!', 'I agree'] } }, created() { this.$watch('article', () => { console.log('Article changed!') }) this.$watch('comments', () => { console.log('Comments changed!') }) }, methods: { // 这些方法不会触发侦听器,因为我们只更改了Object/Array的一个property, // 不是对象/数组本身 changeArticleText() { this.article.text = 'Vue 3 is awesome' }, addComment() { this.comments.push('New comment') }, // 这些方法将触发侦听器,因为我们完全替换了对象/数组 changeWholeArticle() { this.article = { text: 'Vue 3 is awesome' } }, clearComments() { this.comments = [] } } })
$watch
返回一个取消侦听函数,用来停止触发回调:
const app = Vue.createApp({ data() { return { a: 1 } } }) const vm = app.mount('#app') const unwatch = vm.$watch('a', cb) // later, teardown the watcher unwatch()
为了发现对象内部值的变化,可以在选项参数中指定 deep: true
。注意监听数组的变更不需要这么做。
vm.$watch('someObject', callback, { deep: true }) vm.someObject.nestedValue = 123 // callback is fired
在选项参数中指定 immediate: true
将立即以表达式的当前值触发回调:
vm.$watch('a', callback, { immediate: true }) // 立即以 `a` 的当前值触发 `callback`
注意,在带有 immediate
选项时,你不能在第一次回调时取消侦听给定的 property。
// 这会导致报错 const unwatch = vm.$watch( 'value', function() { doSomething() unwatch() }, { immediate: true } )
如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:
let unwatch = null unwatch = vm.$watch( 'value', function() { doSomething() if (unwatch) { unwatch() } }, { immediate: true } )
flush
选项允许更好地控制回调的时间。它可以设置为pre
、post
或sync
。
默认值为pre
,它指定在呈现之前应调用回调。这允许回调在模板运行之前更新其他值。
值post
可用于将回调延迟到呈现之后。如果回调需要通过$refs
访问更新的DOM或子组件,则应使用此选项。
如果flush
设置为sync
,则只要值发生更改,就会同步调用回调。
对于pre
和post
,回调都使用队列进行缓冲。回调只会添加到队列一次,即使关注的值更改多次。临时值将被跳过,不会传递给回调。
缓冲回调不仅可以提高性能,而且有助于确保数据一致性。在执行数据更新的代码完成之前,不会触发监视程序。
sync
观察者应该谨慎使用,因为他们没有这些好处。
有关`flush`的详细信息,请参见[效果刷新计时](https://www.51coolma.cn/vuejs3/vuejs3-35qs3f4h.html)。
{string} eventName
[...args]
触发当前实例上的事件。附加参数都会传给监听器回调。
只配合一个事件名使用 $emit:
<div id="emit-example-simple"> <welcome-button v-on:welcome="sayHi"></welcome-button> </div>
const app = Vue.createApp({ methods: { sayHi() { console.log('Hi!') } } }) app.component('welcome-button', { template: ` <button v-on:click="$emit('welcome')"> Click me to be welcomed </button> ` }) app.mount('#emit-example-simple')
配合额外的参数使用 $emit
:
<div id="emit-example-argument"> <advice-component v-on:give-advice="showAdvice"></advice-component> </div>
const app = Vue.createApp({ methods: { showAdvice(advice) { alert(advice) } } }) app.component('advice-component', { data() { return { adviceText: 'Some advice' } }, template: ` <div> <input type="text" v-model="adviceText"> <button v-on:click="$emit('give-advice', adviceText)"> Click me for sending advice </button> </div> ` })
迫使组件实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
{Function} [callback]
将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 nextTick
一样,不同的是回调的 this
自动绑定到调用它的实例上。
Vue.createApp({ // ... methods: { // ... example() { // modify data this.message = 'changed' // DOM is not updated yet this.$nextTick(function() { // DOM is now updated // `this` is bound to the current instance this.doSomethingElse() }) } }})
string
更新元素的 textContent。如果要更新部分的 textContent
,需要使用 Mustache 插值。
<span v-text="msg"></span> <!-- 等价于 --> <span>{{msg}}</span>
string
更新元素的 innerHTML。注意:内容按普通 HTML 插入 - 不会作为 Vue 模板进行编译。如果试图使用 v-html
组合模板,可以重新考虑是否通过使用组件来替代。
WARNING
在网站上动态渲染任意 HTML 是非常危险的,因为容易导致 XSS 攻击。只在可信内容上使用 v-html
,永不用在用户提交的内容上。
在单文件组件里,scoped
的样式不会应用在 v-html
内部,因为那部分 HTML 没有被 Vue 的模板编译器处理。如果你希望针对 v-html
的内容设置带作用域的 CSS,你可以替换为 CSS modules 或用一个额外的全局 <style>
元素手动设置类似 BEM 的作用域策略。
<div v-html="html"></div>
any
根据表达式的真假值,切换元素的 display
CSS property。
当条件变化时该指令触发过渡效果。
any
根据表达式的真假值来有条件地渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。如果元素是 <template>
,将提取它的内容作为条件块。
当条件变化时该指令触发过渡效果。
当和 v-if
一起使用时,v-for
的优先级比 v-if
更高。详见列表渲染教程
v-if
或 v-else-if
。为 v-if
或者 v-else-if
添加“else 块”。
<div v-if="Math.random() > 0.5"> Now you see me </div> <div v-else> Now you don't </div>
any
v-if
或 v-else-if
。表示 v-if
的“else if 块”。可以链式调用。
<div v-if="type === 'A'"> A </div> <div v-else-if="type === 'B'"> B </div> <div v-else-if="type === 'C'"> C </div> <div v-else> Not A/B/C </div>
Array | Object | number | string | Iterable
基于源数据多次渲染元素或模板块。此指令之值,必须使用特定语法 alias in expression
,为当前遍历的元素提供别名:
<div v-for="item in items"> {{ item.text }} </div>
另外也可以为数组索引指定别名 (或者用于对象的键):
<div v-for="(item, index) in items"></div> <div v-for="(value, key) in object"></div> <div v-for="(value, name, index) in object"></div>
v-for
的默认行为会尝试原地修改元素而不是移动它们。要强制其重新排序元素,你需要用特殊 attribute key
来提供一个排序提示:
<div v-for="item in items" :key="item.id"> {{ item.text }} </div>
v-for
也可以在实现了可迭代协议的值上使用,包括原生的 Map
和 Set
。
v-for
的详细用法可以通过以下链接查看教程详细说明。
@
Function | Inline Statement | Object
event
.stop
- 调用 event.stopPropagation()
。.prevent
- 调用 event.preventDefault()
。.capture
- 添加事件侦听器时使用 capture 模式。.self
- 只当事件是从侦听器绑定的元素本身触发时才触发回调。.{keyAlias}
- 仅当事件是从特定键触发时才触发回调。.once
- 只触发一次回调。.left
- 只当点击鼠标左键时触发。.right
- 只当点击鼠标右键时触发。.middle
- 只当点击鼠标中键时触发。.passive
- { passive: true }
模式添加侦听器绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略。
用在普通元素上时,只能监听原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件。
监听原生 DOM 事件时,方法以事件为唯一的参数。如果使用内联语句,语句可以访问一个 $event
property:v-on:click="handle('ok', $event)"
。
v-on
同样支持不带参数绑定一个事件/监听器键值对的对象。注意当使用对象语法时,是不支持任何修饰器的。
<!-- 方法处理器 --> <button v-on:click="doThis"></button> <!-- 动态事件 --> <button v-on:[event]="doThis"></button> <!-- 内联语句 --> <button v-on:click="doThat('hello', $event)"></button> <!-- 缩写 --> <button @click="doThis"></button> <!-- 动态事件缩写 --> <button @[event]="doThis"></button> <!-- 停止冒泡 --> <button @click.stop="doThis"></button> <!-- 阻止默认行为 --> <button @click.prevent="doThis"></button> <!-- 阻止默认行为,没有表达式 --> <form @submit.prevent></form> <!-- 串联修饰符 --> <button @click.stop.prevent="doThis"></button> <!-- 键修饰符,键别名 --> <input @keyup.enter="onEnter" /> <!-- 点击回调只会触发一次 --> <button v-on:click.once="doThis"></button> <!-- 对象语法 --> <button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
在子组件上监听自定义事件 (当子组件触发“my-event”时将调用事件处理器):
<my-component @my-event="handleThis"></my-component> <!-- 内联语句 --> <my-component @my-event="handleThis(123, $event)"></my-component>
:
any (with argument) | Object (without argument)
attrOrProp (optional)
.camel
- 将 kebab-case attribute 名转换为 camelCase。动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。
在绑定 class
或 style
attribute 时,支持其它类型的值,如数组或对象。可以通过下面的教程链接查看详情。
在绑定 prop 时,prop 必须在子组件中声明。可以用修饰符指定不同的绑定类型。
没有参数时,可以绑定到一个包含键值对的对象。注意此时 class
和 style
绑定不支持数组和对象。
<!-- 绑定 attribute --> <img v-bind:src="imageSrc" /> <!-- 动态 attribute 名 --> <button v-bind:[key]="value"></button> <!-- 缩写 --> <img :src="imageSrc" /> <!-- 动态 attribute 名缩写 --> <button :[key]="value"></button> <!-- 内联字符串拼接 --> <img :src="'/path/to/images/' + fileName" /> <!-- class 绑定 --> <div :class="{ red: isRed }"></div> <div :class="[classA, classB]"></div> <div :class="[classA, { classB: isB, classC: isC }]"> <!-- style 绑定 --> <div :style="{ fontSize: size + 'px' }"></div> <div :style="[styleObjectA, styleObjectB]"></div> <!-- 绑定一个全是 attribute 的对象 --> <div v-bind="{ id: someProp, 'other-attr': otherProp }"></div> <!-- prop 绑定。"prop" 必须在 my-component 声明 --> <my-component :prop="someThing"></my-component> <!-- 通过 $props 将父组件的 props 一起传给子组件 --> <child-component v-bind="$props"></child-component> <!-- XLink --> <svg><a :xlink:special="foo"></a></svg> </div>
.camel
修饰符允许在使用 DOM 模板时将 v-bind
property 名称驼峰化,例如 SVG 的 viewBox
property:
<svg :view-box.camel="viewBox"></svg>
在使用字符串模板或通过 vue-loader
/ vueify
编译时,无需使用 .camel
。
<input>
<select>
<textarea>
在表单控件或者组件上创建双向绑定。细节请看下面的教程链接。
#
default
)<template>
提供具名插槽或需要接收 prop 的插槽。
<!-- 具名插槽 --> <base-layout> <template v-slot:header> Header content </template> <template v-slot:default> Default slot content </template> <template v-slot:footer> Footer content </template> </base-layout> <!-- 接收 prop 的具名插槽 --> <infinite-scroll> <template v-slot:item="slotProps"> <div class="item"> {{ slotProps.item.text }} </div> </template> </infinite-scroll> <!-- 接收 prop 的默认插槽,使用了解构 --> <mouse-position v-slot="{ x, y }"> Mouse position: {{ x }}, {{ y }} </mouse-position>
更多细节请查阅以下链接。
跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
<span v-pre>{{ this will not be compiled }}</span>
这个指令保持在元素上直到关联组件实例结束编译。和 CSS 规则如 [v-cloak] { display: none }
一起用时,这个指令可以隐藏未编译的 Mustache 标签直到组件实例准备完毕。
[v-cloak] { display: none; }
<div v-cloak> {{ message }} </div>
<div> 不会显示,直到编译结束。
只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
<!-- 单个元素 --> <span v-once>This will never change: {{msg}}</span> <!-- 有子元素 --> <div v-once> <h1>comment</h1> <p>{{msg}}</p> </div> <!-- 组件 --> <my-component v-once :comment="msg"></my-component> <!-- `v-for` 指令 --> <ul> <li v-for="i in list" v-once>{{i}}</li> </ul>
注意:本节仅影响直接在页面的 HTML 中写入 Vue 模板的情况。
<ul>
、<ol>
、<table>
和 <select>
等,对哪些元素可以出现在它们内部有限制,而某些元素 (如:<li>
、<tr>
和 <option>
只能出现在某些其他元素中。作为解决方法,我们可以对以下元素使用 v-is
指令:<table> <tr v-is="'blog-post-row'"></tr></table>
WARNING
v-is
函数类似于动态 2.x :is
绑定——因此要按组件的注册名称渲染组件,其值应为 JavaScript 字符串文本:
<!-- 不正确,不会渲染任何内容 --><tr v-is="blog-post-row"></tr><!-- 正确 --><tr v-is="'blog-post-row'"></tr>
number | string
key
的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除/销毁 key 不存在的元素。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
最常见的用例是结合 v-for
:
<ul> <li v-for="item in items" :key="item.id">...</li> </ul>
它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:
例如:
<transition> <span :key="text">{{ text }}</span> </transition>
当 text
发生改变时,<span>
总是会被替换而不是被修改,因此会触发过渡。
string | Function
ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:
<!-- vm.$refs.p 会是 DOM 节点 --> <p ref="p">hello</p> <!-- vm.$refs.child 会是子组件实例 --> <child-component ref="child"></child-component> <!-- 当动态绑定时,我们可以将ref定义为回调函数,显式地传递元素或组件实例 --> <child-component :ref="(el) => child = el"></child-component>
当 v-for
用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。
关于 ref 注册时间的重要说明:因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs
也是非响应式的,因此你不应该试图用它在模板中做数据绑定。
string | Object (component’s options object)
使用动态组件。
例如:
<!-- component changes when currentView changes --><component :is="currentView"></component>
更多的使用细节,请移步至下面的链接。
is
- string | Component
渲染一个“元组件”为动态组件。依 is
的值,来决定哪个组件被渲染。is
的值是一个字符串,它既可以是 HTML 标签名称也可以是组件名称。
<!-- 动态组件由 vm 实例的 `componentId` property 控制 --> <component :is="componentId"></component> <!-- 也能够渲染注册过的组件或 prop 传入的组件--> <component :is="$options.components.child"></component> <!-- 可以通过字符串引用组件 --> <component :is="condition ? 'FooComponent' : 'BarComponent'"></component> <!-- 可以用来渲染原生 HTML 元素 --> <component :is="href ? 'a' : 'span'"></component>
name
- string
用于自动生成 CSS 过渡类名。例如:name: 'fade'
将自动拓展为 .fade-enter
,.fade-enter-active
等。appear
- boolean
,是否在初始渲染时使用过渡。默认为 false
。persisted
- boolean
。如果是 true,表示这是一个不真实插入/删除元素的转换,而是切换显示/隐藏状态。过渡钩子被注入,但渲染器将跳过。相反,自定义指令可以通过调用注入的钩子 (例如 v-show
) 来控制转换。css
- boolean
。是否使用 CSS 过渡类。默认为 true
。如果设置为 false
,将只通过组件事件触发注册的 JavaScript 钩子。type
- string
。指定过渡事件类型,侦听过渡何时结束。有效值为 "transition"
和 "animation"
。默认 Vue.js 将自动检测出持续时间长的为过渡事件类型。mode
- string
控制离开/进入过渡的时间序列。有效的模式有 "out-in"
和 "in-out"
;默认同时进行。duration
- number | {
enter : number,
leave : number }
。指定过渡的持续时间。默认情况下,Vue 会等待过渡所在根元素的第一个 transitionend
或 animationend
事件。enter-from-class
- string
leave-from-class
- string
appear-class
- string
enter-to-class
- string
leave-to-class
- string
appear-to-class
- string
enter-active-class
- string
leave-active-class
- string
appear-active-class
- string
before-enter
before-leave
enter
leave
appear
after-enter
after-leave
after-appear
enter-cancelled
leave-cancelled
(仅 v-show
)appear-cancelled
<transition>
元素作为单个元素/组件的过渡效果。<transition>
只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。
<!-- 单个元素 --> <transition> <div v-if="ok">toggled content</div> </transition> <!-- 动态组件 --> <transition name="fade" mode="out-in" appear> <component :is="view"></component> </transition> <!-- 事件钩子 --> <div id="transition-demo"> <transition @after-enter="transitionComplete"> <div v-show="ok">toggled content</div> </transition> </div>
const app = Vue.createApp({ ... methods: { transitionComplete (el) { // 因为传递了'el'的DOM元素作为参数 } } ... }) app.mount('#transition-demo')
tag
- string
,默认为 span
。move-class
- 覆盖移动过渡期间应用的 CSS 类。mode
,其他 attribute 和 <transition>
相同。<transition>
相同。<transition-group>
元素作为多个元素/组件的过渡效果。<transition-group>
渲染一个真实的 DOM 元素。默认渲染 <span>
,可以通过 tag
attribute 配置哪个元素应该被渲染。
注意,每个 <transition-group>
的子节点必须有独立的 key,动画才能正常工作
<transition-group>
支持通过 CSS transform 过渡移动。当一个子节点被更新,从屏幕上的位置发生变化,它会被应用一个移动中的 CSS 类 (通过 name
attribute 或配置 move-class
attribute 自动生成)。如果 CSS transform
property 是“可过渡”property,当应用移动类时,将会使用 FLIP 技术使元素流畅地到达动画终点。
<transition-group tag="ul" name="slide"> <li v-for="item in items" :key="item.id"> {{ item.text }} </li> </transition-group>
include
- string | RegExp | Array
。只有名称匹配的组件会被缓存。exclude
- string | RegExp | Array
。任何名称匹配的组件都不会被缓存。max
- number | string
。最多可以缓存多少组件实例。<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition>
相似,<keep-alive>
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
当组件在 <keep-alive>
内被切换,它的 activated
和 deactivated
这两个生命周期钩子函数将会被对应执行。
主要用于保留组件状态或避免重新渲染。
<!-- 基本 --> <keep-alive> <component :is="view"></component> </keep-alive> <!-- 多个条件判断的子组件 --> <keep-alive> <comp-a v-if="a > 1"></comp-a> <comp-b v-else></comp-b> </keep-alive> <!-- 和 `<transition>` 一起使用 --> <transition> <keep-alive> <component :is="view"></component> </keep-alive> </transition>
注意,<keep-alive>
是用在其一个直属的子组件被切换的情形。如果你在其中有 v-for
则不会工作。如果有上述的多个条件性的子元素,<keep-alive>
要求同时只有一个子元素被渲染。
include
和 exclude
The include
和 exclude
prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:
<!-- 逗号分隔字符串 --> <keep-alive include="a,b"> <component :is="view"></component> </keep-alive> <!-- regex (使用 `v-bind`) --> <keep-alive :include="/a|b/"> <component :is="view"></component> </keep-alive> <!-- Array (使用 `v-bind`) --> <keep-alive :include="['a', 'b']"> <component :is="view"></component> </keep-alive>
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值)。匿名组件不能被匹配。
max
最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
<keep-alive :max="10"> <component :is="view"></component> </keep-alive>
WARNING
<keep-alive>
不会在函数式组件中正常工作,因为它们没有缓存实例。
name
- string
,用于具名插槽<slot>
元素作为组件模板之中的内容分发插槽。<slot>
元素自身将被替换。
详细用法,请参考下面教程的链接。
to
- string
。需要 prop,必须是有效的查询选择器或 HTMLElement (如果在浏览器环境中使用)。指定将在其中移动 <teleport>
内容的目标元素 <!-- 正确 --> <teleport to="#some-id" /> <teleport to=".some-class" /> <teleport to="[data-teleport]" /> <!-- 错误 --> <teleport to="h1" /> <teleport to="some-string" />
disabled
- boolean
。此可选属性可用于禁用 <teleport>
的功能,这意味着其插槽内容将不会移动到任何位置,而是在您在周围父组件中指定了 <teleport>
的位置渲染。 <teleport to="#popup" :disabled="displayVideoInline"> <video src="./my-movie.mp4"> </teleport>
请注意,这将移动实际的 DOM 节点,而不是被销毁和重新创建,并且它还将保持任何组件实例的活动状态。所有有状态的 HTML 元素 (即播放的视频) 都将保持其状态。
本节例子中代码使用的单文件组件语法
reactive
返回对象的响应式副本
const obj = reactive({ count: 0 })
响应式转换是“深层”的——它影响所有嵌套 property。在基于 ES2015 Proxy 的实现中,返回的代理是不等于原始对象。建议只使用响应式代理,避免依赖原始对象。
类型声明:
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
readonly
获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。只读代理是深层的:访问的任何嵌套 property 也是只读的。
const original = reactive({ count: 0 })const copy = readonly(original)watchEffect(() => { // 适用于响应性追踪 console.log(copy.count)})// 变更original 会触发侦听器依赖副本original.count++// 变更副本将失败并导致警告copy.count++ // 警告!
isProxy
检查对象是 reactive
还是 readonly
创建的代理
isReactive
检查对象是否是 reactive
创建的响应式 proxy。
import { reactive, isReactive } from 'vue'export default { setup() { const state = reactive({ name: 'John' }) console.log(isReactive(state)) // -> true }}
如果 proxy 是 readonly
创建的,但还包装了由 reactive
创建的另一个 proxy,它也会返回 true
。
import { reactive, isReactive, readonly } from 'vue'export default { setup() { const state = reactive({ name: 'John' }) // 从普通对象创建的只读代理 const plain = readonly({ name: 'Mary' }) console.log(isReactive(plain)) // -> false // 从响应式代理创建的只读代理 const stateCopy = readonly(state) console.log(isReactive(stateCopy)) // -> true }}
isReadonly
检查对象是否是由readonly
创建的只读代理。
toRaw
返回 reactive
或 readonly
代理的原始对象。这是一个转义口,可用于临时读取而不会引起代理访问/跟踪开销,也可用于写入而不会触发更改。不建议保留对原始对象的持久引用。请谨慎使用。
const foo = {}const reactiveFoo = reactive(foo)console.log(toRaw(reactiveFoo) === foo) // true
markRaw
标记一个对象,使其永远不会转换为代理。返回对象本身。
const foo = markRaw({})console.log(isReactive(reactive(foo))) // false// 嵌套在其他响应式对象中时也可以使用const bar = reactive({ foo })console.log(isReactive(bar.foo)) // false
WARNING
下方的 markRaw
和 shallowXXX API 使你可以有选择地选择退出默认的深度响应式/只读转换,并将原始的,非代理的对象嵌入状态图中。它们可以在各种情况下使用:
它们被认为是高阶的,因为原始选择退出仅在根级别,因此,如果将嵌套的、未标记的原始对象设置为响应式对象,然后再次访问它,则可以得到代理版本。这可能会导致本源危害——即执行依赖于对象本身但同时使用同一对象的原始版本和代理版本的操作:
const foo = markRaw({ nested: {}})const bar = reactive({ // 虽然 `foo` 被标记为原始,foo.nested 不是。 nested: foo.nested})console.log(foo.nested === bar.nested) // false
本源危害通常很少见。然而,为了在安全地避免本源危害的同时正确地使用这些 API,需要对响应性系统的工作原理有一个坚实的理解。
shallowReactive
创建一个响应式代理,该代理跟踪其自身 property 的响应性,但不执行嵌套对象的深度响应式转换 (暴露原始值)。
const state = shallowReactive({ foo: 1, nested: { bar: 2 }})// 改变状态本身的性质是响应式的state.foo++// ...但是不转换嵌套对象isReactive(state.nested) // falsestate.nested.bar++ // 非响应式
shallowReadonly
创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。
const state = shallowReadonly({ foo: 1, nested: { bar: 2 }})// 改变状态本身的property将失败state.foo++// ...但适用于嵌套对象isReadonly(state.nested) // falsestate.nested.bar++ // 适用
本节例子中代码使用的单文件组件语法
ref
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value
。
示例:
const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
如果将对象分配为 ref 值,则可以通过 reactive 方法使该对象具有高度的响应式。
类型声明:
interface Ref<T> { value: T}function ref<T>(value: T): Ref<T>
有时我们可能需要为 ref 的内部值指定复杂类型。我们可以通过在调用 ref
来覆盖默认推断时传递一个泛型参数来简洁地做到这一点:
const foo = ref<string | number>('foo') // foo's type: Ref<string | number>foo.value = 123 // ok!
如果泛型的类型未知,建议将 ref
转换为 Ref<T>
:
function useState<State extends string>(initial: State) { const state = ref(initial) as Ref<State> // state.value -> State extends string return state}
unref
如果参数为 ref
,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val
。
function useFoo(x: number | Ref<number>) { const unwrapped = unref(x) // unwrapped 确保现在是数字类型}
toRef
可以用来为源响应式对象上的 property 性创建一个 ref
。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。
const state = reactive({ foo: 1, bar: 2})const fooRef = toRef(state, 'foo')fooRef.value++console.log(state.foo) // 2state.foo++console.log(fooRef.value) // 3
当您要将 prop 的 ref 传递给复合函数时,toRef
很有用:
export default { setup(props) { useSomeFeature(toRef(props, 'foo')) }}
toRefs
将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref
。
const state = reactive({ foo: 1, bar: 2})const stateAsRefs = toRefs(state)/*Type of stateAsRefs:{ foo: Ref<number>, bar: Ref<number>}*/// ref 和 原始property “链接”state.foo++console.log(stateAsRefs.foo.value) // 2stateAsRefs.foo.value++console.log(state.foo) // 3
当从合成函数返回响应式对象时,toRefs
非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:
function useFeatureX() { const state = reactive({ foo: 1, bar: 2 }) // 逻辑运行状态 // 返回时转换为ref return toRefs(state)}export default { setup() { // 可以在不失去响应性的情况下破坏结构 const { foo, bar } = useFeatureX() return { foo, bar } }}
isRef
检查值是否为ref对象。
customRef
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track
和 trigger
函数作为参数,并应返回一个带有 get
和 set
的对象。
v-model
使用自定义 ref 实现 debounce
的示例: <input v-model="text" />
function useDebouncedRef(value, delay = 200) { let timeout return customRef((track, trigger) => { return { get() { track() return value }, set(newValue) { clearTimeout(timeout) timeout = setTimeout(() => { value = newValue trigger() }, delay) } } }) } export default { setup() { return { text: useDebouncedRef('hello') } } }
Typing:
function customRef<T>(factory: CustomRefFactory<T>): Ref<T>type CustomRefFactory<T> = ( track: () => void, trigger: () => void) => { get: () => T set: (value: T) => void}
shallowRef
创建一个 ref,它跟踪自己的 .value
更改,但不会使其值成为响应式的。
const foo = shallowRef({})// 改变 ref 的值是响应式的foo.value = {}// 但是这个值不会被转换。isReactive(foo.value) // false
triggerRef
手动执行与 shallowRef
](#shallowref) 关联的任何效果。
const shallow = shallowRef({ greet: 'Hello, world'})// 第一次运行时记录一次 "Hello, world"watchEffect(() => { console.log(shallow.value.greet)})// 这不会触发作用,因为 ref 很浅层shallow.value.greet = 'Hello, universe'// 记录 "Hello, universe"triggerRef(shallow)
本节例子中代码使用的单文件组件语法
computed
使用 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。
const count = ref(1)const plusOne = computed(() => count.value + 1)console.log(plusOne.value) // 2plusOne.value++ // error
或者,它可以使用具有 get
和 set
函数的对象来创建可写的 ref 对象。
const count = ref(1)const plusOne = computed({ get: () => count.value + 1, set: val => { count.value = val - 1 }})plusOne.value = 1console.log(count.value) // 0
类型声明:
// read-onlyfunction computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>// writablefunction computed<T>(options: { get: () => T; set: (value: T) => void }): Ref<T>
watchEffect
在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它。
const count = ref(0)watchEffect(() => console.log(count.value))// -> logs 0setTimeout(() => { count.value++ // -> logs 1}, 100)
类型声明:
function watchEffect( effect: (onInvalidate: InvalidateCbRegistrator) => void, options?: WatchEffectOptions): StopHandleinterface WatchEffectOptions { flush?: 'pre' | 'post' | 'sync' // default: 'pre' onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void}interface DebuggerEvent { effect: ReactiveEffect target: any type: OperationTypes key: string | symbol | undefined}type InvalidateCbRegistrator = (invalidate: () => void) => voidtype StopHandle = () => void
watch
watch
API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效。watch
需要侦听特定的 data 源,并在单独的回调函数中副作用。默认情况下,它也是惰性的——即,回调是仅在侦听源发生更改时调用。
watch
允许我们:侦听器 data 源可以是返回值的 getter 函数,也可以是 ref:
// 侦听一个getterconst state = reactive({ count: 0 })watch( () => state.count, (count, prevCount) => { /* ... */ })// 直接侦听一个refconst count = ref(0)watch(count, (count, prevCount) => { /* ... */})
侦听器还可以使用数组同时侦听多个源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */})
watchEffect
共享行为watch
与 watchEffect
在手动停止,副作用无效 (将 onInvalidate
作为第三个参数传递给回调),flush timing 和 debugging 有共享行为。
类型声明:
// 侦听单一源function watch<T>( source: WatcherSource<T>, callback: ( value: T, oldValue: T, onInvalidate: InvalidateCbRegistrator ) => void, options?: WatchOptions): StopHandle// 侦听多个源function watch<T extends WatcherSource<unknown>[]>( sources: T callback: ( values: MapSources<T>, oldValues: MapSources<T>, onInvalidate: InvalidateCbRegistrator ) => void, options? : WatchOptions): StopHandletype WatcherSource<T> = Ref<T> | (() => T)type MapSources<T> = { [K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never}// 参见 `watchEffect` 类型声明共享选项interface WatchOptions extends WatchEffectOptions { immediate?: boolean // default: false deep?: boolean}
参考:watch
指南
本节例子中代码使用的单文件组件语法
setup
一个组件选项,在创建组件之前执行,一旦 props
被解析,并作为组合式 API 的入口点
{Data} props
{SetupContext} context
interface Data { [key: string]: unknown}interface SetupContext { attrs: Data slots: Slots emit: (event: string, ...args: unknown[]) => void}function setup(props: Data, context: SetupContext): Data
TIP
若要获取传递给 setup()
的参数的类型推断,请使用 defineComponent 是需要的。
使用模板:
<!-- MyBook.vue --> <template> <div>{{ readersNumber }} {{ book.title }}</div> </template> <script> import { ref, reactive } from 'vue' export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // expose to template return { readersNumber, book } } } </script>
使用渲染函数:
// MyBook.vue import { h, ref, reactive } from 'vue' export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // 请注意,我们需要在这里显式地暴露ref值 return () => h('div', [readersNumber.value, book.title]) } }
可以使用直接导入的 onX
函数注册生命周期钩子:
import { onMounted, onUpdated, onUnmounted } from 'vue'const MyComponent = { setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) onUnmounted(() => { console.log('unmounted!') }) }}
这些生命周期钩子注册函数只能在 setup()
期间同步使用,因为它们依赖于内部全局状态来定位当前活动实例 (此时正在调用其 setup()
的组件实例)。在没有当前活动实例的情况下调用它们将导致错误。
组件实例上下文也是在生命周期钩子的同步执行期间设置的,因此在生命周期钩子内同步创建的侦听器和计算属性也会在组件卸载时自动删除。
选项 API 生命周期选项和组合式 API 之间的映射
beforeCreate
-> use setup()
created
-> use setup()
beforeMount
-> onBeforeMount
mounted
-> onMounted
beforeUpdate
-> onBeforeUpdate
updated
-> onUpdated
beforeUnmount
-> onBeforeUnmount
unmounted
-> onUnmounted
errorCaptured
-> onErrorCaptured
renderTracked
-> onRenderTracked
renderTriggered
-> onRenderTriggered
provide
和 inject
启用依赖注入。只有在使用当前活动实例的 setup()
期间才能调用这两者。
interface InjectionKey<T> extends Symbol {}function provide<T>(key: InjectionKey<T> | string, value: T): void// without default valuefunction inject<T>(key: InjectionKey<T> | string): T | undefined// with default valuefunction inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
Vue 提供了一个 InjectionKey
接口,该接口是扩展 Symbol
的泛型类型。它可用于在提供者和消费者之间同步注入值的类型:
import { InjectionKey, provide, inject } from 'vue'const key: InjectionKey<string> = Symbol()provide(key, 'foo') // 提供非字符串值将导致错误const foo = inject(key) // foo 的类型: string | undefined
如果使用字符串 key 或非类型化 symbols,则需要显式声明注入值的类型:
const foo = inject<string>('foo') // string | undefined
:
getCurrentInstance
getCurrentInstance
允许访问对高级使用或库创建者有用的内部组件实例。
import { getCurrentInstance } from 'vue'const MyComponent = { setup() { const internalInstance = getCurrentInstance() internalInstance.appContext.config.globalProperties // access to globalProperties }}
getCurrentInstance
仅在安装或生命周期挂钩期间有效。
在安装程序或生命周期挂钩之外使用时,请在
setup
上调用getCurrentInstance()
,并改用该实例。
const MyComponent = { setup() { const internalInstance = getCurrentInstance() // works const id = useComponentId() // works const handleClick = () => { getCurrentInstance() // doesn't work useComponentId() // doesn't work internalInstance // works } onMounted(() => { getCurrentInstance() // works }) return () => h( 'button', { onClick: handleClick }, `uid: ${id}` ) }}// also works if called on a composablefunction useComponentId() { return getCurrentInstance().uid}
这里是官方的 Vue 特有代码的风格指南。如果在工程中使用 Vue,为了回避错误、小纠结和反模式,该指南是份不错的参考。不过我们也不确信风格指南的所有内容对于所有的团队或工程都是理想的。所以根据过去的经验、周边的技术栈、个人价值观做出有意义的偏差是可取的。
对于其绝大部分,我们也总体上避免就 JavaScript 或 HTML 的本身提出建议。我们不介意你是否使用分号或结尾的逗号。我们不介意你在 HTML attribute 中使用单引号还是双引号。不过当我们发现在 Vue 的情景下有帮助的特定模式时,也会存在例外。
最终,我们把所有的规则归为了四个大类:
这些规则会帮你规避错误,所以学习并接受它们带来的全部代价吧。这里面可能存在例外,但应该非常少,且只有你同时精通 JavaScript 和 Vue 才可以这样做。
这些规则能够在绝大多数工程中改善可读性和开发体验。即使你违反了,代码还是能照常运行,但例外应该尽可能少且有合理的理由。
当存在多个同样好的选项,选任意一个都可以确保一致性。在这些规则里,我们描述了每个选项并建议一个默认的选择。也就是说只要保持一致且理由充分,你可以随意在你的代码库中做出不同的选择。请务必给出一个好的理由!通过接受社区的标准,你将会:
有些 Vue 特性的存在是为了照顾极端情况或帮助老代码的平稳迁移。当被过度使用时,这些特性会让你的代码难于维护甚至变成 bug 的来源。这些规则是为了给有潜在风险的特性敲个警钟,并说明它们什么时候不应该使用以及为什么。
组件名应该始终是多个单词的,根组件 App
以及 <transition>
、<component>
之类的 Vue 内置组件除外。
这样做可以避免跟现有的以及未来的 HTML 元素相冲突,因为所有的 HTML 元素名称都是单个单词的。
app.component('todo', { // ...})
export default { name: 'Todo', // ...}
app.component('todo-item', { // ...})
export default { name: 'TodoItem', // ...}
Prop 定义应尽量详细
在你提交的代码中,prop 的定义应该尽量详细,至少需要指定其类型。
详解
细致的 prop 定义有两个好处:
它们写明了组件的 API,所以很容易看懂组件的用法;
在开发环境下,如果向一个组件提供格式不正确的 prop,Vue 将会告警,以帮助你捕获潜在的错误来源。
// 这样做只有开发原型系统时可以接受props: ['status']
props: { status: String}
// 更好的例子props: { status: { type: String, required: true, validator: value => { return [ 'syncing', 'synced', 'version-conflict', 'error' ].includes(value) } }}
v-for
设置 key 值必要总是用 key
配合 v-for
在组件上总是必须用 key
配合 v-for
,以便维护内部组件及其子树的状态。甚至在元素上维护可预测的行为,比如动画中的对象固化 (object constancy) ,也是一种好的做法。
详解假设你有一个待办事项列表:
data() { return { todos: [ { id: 1, text: 'Learn to use v-for' }, { id: 2, text: 'Learn to use key' } ] }}
然后你把它们按照字母顺序排序。在更新 DOM 的时候,Vue 将会优化渲染把可能的 DOM 变更降到最低。即可能删掉第一个待办事项元素,然后把它重新加回到列表的最末尾。这里的问题在于,不要删除仍然会留在 DOM 中的元素。比如你想使用 <transition-group> 给列表加过渡动画,或想在被渲染元素是 <input> 时保持聚焦。在这些情况下,为每一个项目添加一个唯一的键值 (比如 :key="todo.id") 将会让 Vue 知道如何使行为更容易预测。
详解
假设你有一个待办事项列表:JavaScript
data() {
return {
todos: [
{
id: 1,
text: 'Learn to use v-for'
},
{
id: 2,
text: 'Learn to use key'
}
]
}
}
然后你把它们按照字母顺序排序。在更新 DOM 的时候,Vue 将会优化渲染把可能的 DOM 变更降到最低。即可能删掉第一个待办事项元素,然后把它重新加回到列表的最末尾。
这里的问题在于,不要删除仍然会留在 DOM 中的元素。比如你想使用 <transition-group> 给列表加过渡动画,或想在被渲染元素是 <input> 时保持聚焦。在这些情况下,为每一个项目添加一个唯一的键值 (比如 :key="todo.id") 将会让 Vue 知道如何使行为更容易预测。
根据我们的经验,最好始终添加一个唯一的键值,以便你和你的团队永远不必担心这些极端情况。也在少数对性能有严格要求的情况下,为了避免对象固化,你可以刻意做一些非常规的处理。根据我们的经验,最好始终添加一个唯一的键值,以便你和你的团队永远不必担心这些极端情况。也在少数对性能有严格要求的情况下,为了避免对象固化,你可以刻意做一些非常规的处理。
<ul> <li v-for="todo in todos"> {{ todo.text }} </li></ul>
<ul> <li v-for="todo in todos" :key="todo.id" > {{ todo.text }} </li></ul>
v-if
和 v-for
一起使用必要永远不要把 v-if
和 v-for
同时用在同一个元素上。
一般我们在两种常见的情况下会倾向于这样做:
v-for="user in users" v-if="user.isActive"
)。在这种情形下,请将 users
替换为一个计算属性 (比如 activeUsers
),让其返回过滤后的列表。v-for="user in users" v-if="shouldShowUsers"
)。这种情形下,请将 v-if
移动至容器元素上 (比如 ul
、ol
详解当 Vue 处理指令时,v-for
比 v-if
具有更高的优先级,所以这个模板:
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id" > {{ user.name }} </li></ul>
将会经过如下运算:
this.users.map(user => { if (user.isActive) { return user.name }})
因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。通过将其更换为在如下的一个计算属性上遍历:
computed: { activeUsers() { return this.users.filter(user => user.isActive) }}
<ul> <li v-for="user in activeUsers" :key="user.id" > {{ user.name }} </li></ul>
我们将会获得如下好处:
users
数组发生相关变化时才被重新运算,过滤更高效。v-for="user in activeUsers"
之后,我们在渲染的时候只遍历活跃用户,渲染更高效。<ul><liv-for="user in users"v-if="shouldShowUsers":key="user.id">{{ user.name }}</li></ul>
更新为:
<ul v-if="shouldShowUsers"><liv-for="user in users":key="user.id">{{ user.name }}</li></ul>
详解
当 Vue 处理指令时,v-for
比v-if
具有更高的优先级,所以这个模板:html
lt;ul&
lt;li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
{{ user.name }}
lt;/li&
lt;/ul&
将会经过如下运算:js
this.users.map(user => {
if (user.isActive) {
return user.name
}
})
因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。
通过将其更换为在如下的一个计算属性上遍历:js
computed: {
activeUsers() {
return this.users.filter(user => user.isActive)
}
}html
lt;ul&
lt;li
v-for="user in activeUsers"
:key="user.id"
{{ user.name }}
lt;/li&
lt;/ul&
我们将会获得如下好处:
users
数组发生相关变化时才被重新运算,过滤更高效。v-for="user in activeUsers"
之后,我们在渲染的时候只遍历活跃用户,渲染更高效。<ul><liv-for="user in users"v-if="shouldShowUsers":key="user.id">{{ user.name }}</li></ul>
更新为:
<ul v-if="shouldShowUsers"><liv-for="user in users":key="user.id">{{ user.name }}</li></ul>
通过将 v-if
移动到容器元素,我们不会再对列表中的每个用户检查 shouldShowUsers
。取而代之的是,我们只检查它一次,且不会在 shouldShowUsers
为否的时候运算 v-for
。通过将 v-if
移动到容器元素,我们不会再对列表中的每个用户检查 shouldShowUsers
。取而代之的是,我们只检查它一次,且不会在 shouldShowUsers
为否的时候运算 v-for
。
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id" > {{ user.name }} </li></ul>
<ul> <li v-for="user in users" v-if="shouldShowUsers" :key="user.id" > {{ user.name }} </li></ul>
<ul> <li v-for="user in activeUsers" :key="user.id" > {{ user.name }} </li></ul>
<ul v-if="shouldShowUsers"> <li v-for="user in users" :key="user.id" > {{ user.name }} </li></ul>
对于应用来说,顶级 App
组件和布局组件中的样式可以是全局的,但是其它所有组件都应该是有作用域的。
这条规则只和单文件组件有关。你不一定要使用 scoped
attribute。设置作用域也可以通过 CSS Modules,那是一个基于 class 的类似 BEM 的策略,当然你也可以使用其它的库或约定。
不管怎样,对于组件库,我们应该更倾向于选用基于 class 的策略而不是 scoped
attribute。
这让覆写内部样式更容易:使用了人类可理解的 class 名称且没有太高的选择器优先级,而且不太会导致冲突。
详解
如果你和其他开发者一起开发一个大型工程,或有时引入三方 HTML/CSS (比如来自 Auth0),设置一致的作用域会确保你的样式只会运用在它们想要作用的组件上。
不止要使用scoped
attribute,使用唯一的 class 名可以帮你确保那些三方库的 CSS 不会运用在你自己的 HTML 上。比如许多工程都使用了button
、btn
或icon
class 名,所以即便你不使用类似 BEM 的策略,添加一个 app 专属或组件专属的前缀 (比如ButtonClose-icon
) 也可以提供很多保护。
<template> <button class="btn btn-close">×</button></template><style>.btn-close { background-color: red;}</style>
<template> <button class="button button-close">×</button></template><!-- 使用 `scoped` attribute --><style scoped>.button { border: none; border-radius: 2px;}.button-close { background-color: red;}</style>
<template> <button :class="[$style.button, $style.buttonClose]">×</button></template><!-- 使用 CSS modules --><style module>.button { border: none; border-radius: 2px;}.buttonClose { background-color: red;}</style>
<template> <button class="c-Button c-Button--close">×</button></template><!-- 使用 BEM 约定 --><style>.c-Button { border: none; border-radius: 2px;}.c-Button--close { background-color: red;}</style>
使用模块作用域保持不允许外部访问的函数的私有性。如果无法做到这一点,就始终为插件、混入等不考虑作为对外公共 API 的自定义私有 property 使用 $_
前缀。并附带一个命名空间以回避和其它作者的冲突 (比如 $_yourPluginName_
)。
::: 详细
Vue 使用 _
前缀来定义其自身的私有 property,所以使用相同的前缀 (比如 _update
) 有覆写实例 property 的风险。即便你检查确认 Vue 当前版本没有用到这个 property 名,也不能保证和将来的版本没有冲突。
对于 $
前缀来说,其在 Vue 生态系统中的目的是暴露给用户的一个特殊的实例 property,所以把它用于私有 property 并不合适。
不过,我们推荐把这两个前缀结合为 $_
,作为一个用户定义的私有 property 的约定,以确保不会和 Vue 自身相冲突。
:::
const myGreatMixin = { // ... methods: { update() { // ... } }}
const myGreatMixin = { // ... methods: { _update() { // ... } }}
const myGreatMixin = { // ... methods: { $update() { // ... } }}
const myGreatMixin = { // ... methods: { $_update() { // ... } }}
const myGreatMixin = { // ... methods: { $_myGreatMixin_update() { // ... } }}
// Even better!const myGreatMixin = { // ... methods: { publicMethod() { // ... myPrivateFunction() } }}function myPrivateFunction() { // ...}export default myGreatMixin
只要有能够拼接文件的构建系统,就把每个组件单独分成文件。
当你需要编辑一个组件或查阅一个组件的用法时,可以更快速的找到它。
app.component('TodoList', { // ...})app.component('TodoItem', { // ...})
components/|- TodoList.js|- TodoItem.js
components/|- TodoList.vue|- TodoItem.vue
单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)。
单词大写开头对于代码编辑器的自动补全最为友好,因为这使得我们在 JS(X) 和模板中引用组件的方式尽可能的一致。然而,混用文件命名方式有的时候会导致大小写不敏感的文件系统的问题,这也是横线连接命名同样完全可取的原因。
components/|- mycomponent.vue
components/|- myComponent.vue
components/|- MyComponent.vue
components/|- my-component.vue
应用特定样式和约定的基础组件 (也就是展示类的、无逻辑的或无状态的组件) 应该全部以一个特定的前缀开头,比如 Base
、App
或 V
。
详解
这些组件为你的应用奠定了一致的基础样式和行为。它们可能只包括:
- HTML 元素
- 其它基础组件
- 第三方 UI 组件库
但是它们绝不会包括全局状态 (比如来自 Vuex store)。
它们的名字通常包含所包裹元素的名字 (比如BaseButton
、BaseTable
),除非没有现成的对应功能的元素 (比如BaseIcon
)。如果你为特定的上下文构建类似的组件,那它们几乎总会消费这些组件 (比如BaseButton
可能会用在ButtonSubmit
上)。
这样做的几个好处:
- 当你在编辑器中以字母顺序排序时,你的应用的基础组件会全部列在一起,这样更容易识别。
- 因为组件名应该始终是多个单词,所以这样做可以避免你在包裹简单组件时随意选择前缀 (比如MyButton
、VueButton
)。
- 因为这些组件会被频繁使用,所以你可能想把它们放到全局而不是在各处分别导入它们。使用相同的前缀可以让 webpack 这样工作:js
const requireComponent = require.context("./src", true, /Base[A-Z]w+.(vue|js)$/)
requireComponent.keys().forEach(function (fileName) {
let baseComponentConfig = requireComponent(fileName)
baseComponentConfig = baseComponentConfig.default || baseComponentConfig
const baseComponentName = baseComponentConfig.name || (
fileName
.replace(/^.+//, '')
.replace(/.w+$/, '')
)
app.component(baseComponentName, baseComponentConfig)
})
components/|- MyButton.vue|- VueTable.vue|- Icon.vue
components/|- BaseButton.vue|- BaseTable.vue|- BaseIcon.vue
components/|- AppButton.vue|- AppTable.vue|- AppIcon.vue
components/|- VButton.vue|- VTable.vue|- VIcon.vue
只应该拥有单个活跃实例的组件应该以 The
前缀命名,以示其唯一性。
这不意味着组件只可用于一个单页面,而是每个页面只使用一次。这些组件永远不接受任何 prop,因为它们是为你的应用定制的,而不是它们在你的应用中的上下文。如果你发现有必要添加 prop,那就表明这实际上是一个可复用的组件,只是目前在每个页面里只使用一次。
components/|- Heading.vue|- MySidebar.vue
components/|- TheHeading.vue|- TheSidebar.vue
和父组件紧密耦合的子组件应该以父组件名作为前缀命名。
如果一个组件只在某个父组件的场景下有意义,这层关系应该体现在其名字上。因为编辑器通常会按字母顺序组织文件,所以这样做可以把相关联的文件排在一起。
::: 详情 你可以试着通过在其父组件命名的目录中嵌套子组件以解决这个问题。比如:
components/|- TodoList/ |- Item/ |- index.vue |- Button.vue |- index.vue
或:
components/|- TodoList/ |- Item/ |- Button.vue |- Item.vue|- TodoList.vue
但是这种方式并不推荐,因为这会导致:
:::
components/|- TodoList.vue|- TodoItem.vue|- TodoButton.vue
components/|- SearchSidebar.vue|- NavigationForSearchSidebar.vue
components/|- TodoList.vue|- TodoListItem.vue|- TodoListItemButton.vue
components/|- SearchSidebar.vue|- SearchSidebarNavigation.vue
组件名称应该以高阶的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾。
详解你可能会疑惑:
“为什么我们给组件命名时不多遵从自然语言呢?”
在自然的英文里,形容词和其它描述语通常都出现在名词之前,否则需要使用连接词。比如:
- Coffee with milk
- Soup of the day
- Visitor to the museum
如果你愿意,你完全可以在组件名里包含这些连接词,但是单词的顺序很重要。
同样要注意在你的应用中所谓的“高阶”是跟语境有关的。比如对于一个带搜索表单的应用来说,它可能包含这样的组件:bash
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
你可能注意到了,我们很难看出来哪些组件是针对搜索的。现在我们来根据规则给组件重新命名:bash
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputExcludeGlob.vue
|- SearchInputQuery.vue
|- SettingsCheckboxLaunchOnStartup.vue
|- SettingsCheckboxTerms.vue
因为编辑器通常会按字母顺序组织文件,所以现在组件之间的重要关系一目了然。
你可能想换成多级目录的方式,把所有的搜索组件放到“search”目录,把所有的设置组件放到“settings”目录。我们只推荐在非常大型 (如有 100+ 个组件) 的应用下才考虑这么做,因为:
- 在多级目录间找来找去,要比在单个components
目录下滚动查找要花费更多的精力。
- 存在组件重名 (比如存在多个ButtonDelete.vue
组件) 的时候在编辑器里更难快速定位。
详解
你可能会疑惑:
“为什么我们给组件命名时不多遵从自然语言呢?”
在自然的英文里,形容词和其它描述语通常都出现在名词之前,否则需要使用连接词。比如:
- Coffee with milk
- Soup of the day
- Visitor to the museum
如果你愿意,你完全可以在组件名里包含这些连接词,但是单词的顺序很重要。
同样要注意在你的应用中所谓的“高阶”是跟语境有关的。比如对于一个带搜索表单的应用来说,它可能包含这样的组件:bash
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
你可能注意到了,我们很难看出来哪些组件是针对搜索的。现在我们来根据规则给组件重新命名:bash
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputExcludeGlob.vue
|- SearchInputQuery.vue
|- SettingsCheckboxLaunchOnStartup.vue
|- SettingsCheckboxTerms.vue
因为编辑器通常会按字母顺序组织文件,所以现在组件之间的重要关系一目了然。
你可能想换成多级目录的方式,把所有的搜索组件放到“search”目录,把所有的设置组件放到“settings”目录。我们只推荐在非常大型 (如有 100+ 个组件) 的应用下才考虑这么做,因为:
- 在多级目录间找来找去,要比在单个components
目录下滚动查找要花费更多的精力。
- 存在组件重名 (比如存在多个ButtonDelete.vue
组件) 的时候在编辑器里更难快速定位。
- 让重构变得更难,因为为一个移动了的组件更新相关引用时,查找/替换通常并不高效。- 让重构变得更难,因为为一个移动了的组件更新相关引用时,查找/替换通常并不高效。
components/|- ClearSearchButton.vue|- ExcludeFromSearchInput.vue|- LaunchOnStartupCheckbox.vue|- RunSearchButton.vue|- SearchInput.vue|- TermsCheckbox.vue
components/|- SearchButtonClear.vue|- SearchButtonRun.vue|- SearchInputQuery.vue|- SearchInputExcludeGlob.vue|- SettingsCheckboxTerms.vue|- SettingsCheckboxLaunchOnStartup.vue
在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。
自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。
不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。
<!-- 在单文件组件、字符串模板和 JSX 中 --><MyComponent></MyComponent>
<!-- 在 DOM 模板中 --><my-component/>
<!-- 在单文件组件、字符串模板和 JSX 中 --><MyComponent/>
<!-- 在 DOM 模板中 --><my-component></my-component>
对于绝大多数项目来说,在单文件组件和字符串模板中组件名称应该总是 PascalCase 的——但是在 DOM 模板中总是 kebab-case 的。
PascalCase 相比 kebab-case 有一些优势:
MyComponent>
视觉上比 <my-component>
更能够和单个单词的 HTML 元素区别开来,因为前者的不同之处有两个大写字母,后者只有一个横线。不幸的是,由于 HTML 是大小写不敏感的,在 DOM 模板中必须仍使用 kebab-case。
还请注意,如果你已经是 kebab-case 的重度用户,那么与 HTML 保持一致的命名约定且在多个项目中保持相同的大小写规则就可能比上述优势更为重要了。在这些情况下,在所有的地方都使用 kebab-case 同样是可以接受的。
<!-- 在单文件组件和字符串模板中 --><mycomponent/>
<!-- 在单文件组件和字符串模板中 --><myComponent/>
<!-- 在 DOM 模板中 --><MyComponent></MyComponent>
<!-- 在单文件组件和字符串模板中 --><MyComponent/>
<!-- 在 DOM 模板中 --><my-component></my-component>
或者
<!-- 在所有地方 --><my-component></my-component>
JS/JSX 中的组件名应该始终是 PascalCase 的,尽管在较为简单的应用中只使用 app.component
进行全局组件注册时,可以使用 kebab-case 字符串。
详解
在 JavaScript 中,PascalCase 是类和构造函数 (本质上任何可以产生多份不同实例的东西) 的命名约定。Vue 组件也有多份实例,所以同样使用 PascalCase 是有意义的。额外的好处是,在 JSX (和模板) 里使用 PascalCase 使得代码的读者更容易分辨 Vue 组件和 HTML 元素。
然而,对于只通过app.component
定义全局组件的应用来说,我们推荐 kebab-case 作为替代。原因是:
- 全局组件很少被 JavaScript 引用,所以遵守 JavaScript 的命名约定意义不大。
- 这些应用往往包含许多 DOM 内的模板,这种情况下是必须使用 kebab-case 的。
app.component('myComponent', { // ...})
import myComponent from './MyComponent.vue'
export default { name: 'myComponent', // ...}
export default { name: 'my-component', // ...}
app.component('MyComponent', { // ...})
app.component('my-component', { // ...})
import MyComponent from './MyComponent.vue'
export default { name: 'MyComponent', // ...}
组件名称应该倾向于完整单词而不是缩写。
编辑器中的自动补全已经让书写长命名的代价非常之低了,而其带来的明确性却是非常宝贵的。不常用的缩写尤其应该避免。
components/|- SdSettings.vue|- UProfOpts.vue
components/|- StudentDashboardSettings.vue|- UserProfileOptions.vue
在声明 prop 的时候,其命名应该始终使用 camelCase,而在模板和 JSX 中应该始终使用 kebab-case。
我们单纯的遵循每个语言的约定。在 JavaScript 中更自然的是 camelCase。而在 HTML 中则是 kebab-case。
props: { 'greeting-text': String}
<WelcomeMessage greetingText="hi"/>
props: { greetingText: String}
<WelcomeMessage greeting-text="hi"/>
多个 attribute 的元素应该分多行撰写,每个 attribute 一行。
在 JavaScript 中,用多行分隔对象的多个 property 是很常见的最佳实践,因为这样更易读。模板和 JSX 值得我们做相同的考虑。
<img src="https://vuejs.org/images/logo.png" rel="external nofollow" rel="external nofollow" alt="Vue Logo">
<MyComponent foo="a" bar="b" baz="c"/>
<img src="https://vuejs.org/images/logo.png" rel="external nofollow" rel="external nofollow" alt="Vue Logo">
<MyComponent foo="a" bar="b" baz="c"/>
组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。
复杂表达式会让你的模板变得不那么声明式。我们应该尽量描述应该出现的是什么,而非如何计算那个值。而且计算属性和方法使得代码可以重用。
{{ fullName.split(' ').map((word) => { return word[0].toUpperCase() + word.slice(1) }).join(' ')}}
<!-- 在模板中 -->{{ normalizedFullName }}
// 复杂表达式已经移入一个计算属性computed: { normalizedFullName() { return this.fullName.split(' ') .map(word => word[0].toUpperCase() + word.slice(1)) .join(' ') }}
应该把复杂计算属性分割为尽可能多的更简单的 property。
详解
更简单、命名得当的计算属性是这样的:
- 易于测试
当每个计算属性都包含一个非常简单且很少依赖的表达式时,撰写测试以确保其正确工作就会更加容易。
- 易于阅读
简化计算属性要求你为每一个值都起一个描述性的名称,即便它不可复用。这使得其他开发者 (以及未来的你) 更容易专注在他们关心的代码上并搞清楚发生了什么。
- 更好的“拥抱变化”
任何能够命名的值都可能用在视图上。举个例子,我们可能打算展示一个信息,告诉用户他们存了多少钱;也可能打算计算税费,但是可能会分开展现,而不是作为总价的一部分。
小的、专注的计算属性减少了信息使用时的假设性限制,所以需求变更时也用不着那么多重构了。
computed: { price() { const basePrice = this.manufactureCost / (1 - this.profitMargin) return ( basePrice - basePrice * (this.discountPercent || 0) ) }}
computed: { basePrice() { return this.manufactureCost / (1 - this.profitMargin) }, discount() { return this.basePrice * (this.discountPercent || 0) }, finalPrice() { return this.basePrice - this.discount }}
非空 HTML attribute 值应该始终带引号 (单引号或双引号,选你 JS 里不用的那个)。
在 HTML 中不带空格的 attribute 值是可以没有引号的,但这鼓励了大家在特征值里不写空格,导致可读性变差。
<input type=text>
<AppSidebar :style={width:sidebarWidth+'px'}>
<input type="text">
<AppSidebar :style="{ width: sidebarWidth + 'px' }">
指令缩写 (用 :
表示 v-bind:
,@
表示 v-on:
和用 #
表示 v-slot
) 应该要么都用要么都不用。
<input v-bind:value="newTodoText" :placeholder="newTodoInstructions">
<input v-on:input="onInput" @focus="onFocus">
<template v-slot:header> <h1>Here might be a page title</h1></template><template #footer> <p>Here's some contact info</p></template>
<input :value="newTodoText" :placeholder="newTodoInstructions">
<input v-bind:value="newTodoText" v-bind:placeholder="newTodoInstructions">
<input @input="onInput" @focus="onFocus">
<input v-on:input="onInput" v-on:focus="onFocus">
<template v-slot:header> <h1>Here might be a page title</h1></template><template v-slot:footer> <p>Here's some contact info</p></template>
<template #header> <h1>Here might be a page title</h1></template><template #footer> <p>Here's some contact info</p></template>
组件/实例的选项应该有统一的顺序。
这是我们推荐的组件选项默认顺序。它们被划分为几大类,所以你也能知道从插件里添加的新 property 应该放到哪里。
name
components
directives
extends
mixins
provide
/inject
inheritAttrs
props
emits
setup
data
computed
watch
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
activated
deactivated
beforeUnmount
unmounted
errorCaptured
renderTracked
renderTriggered
methods
template
/render
元素 (包括组件) 的 attribute 应该有统一的顺序。
这是我们为组件选项推荐的默认顺序。它们被划分为几大类,所以你也能知道新添加的自定义 attribute 和指令应该放到哪里。
is
v-for
v-if
v-else-if
v-else
v-show
v-cloak
v-pre
v-once
id
ref
key
v-model
v-on
v-html
v-text
你可能想在多个 property 之间增加一个空行,特别是在这些选项一屏放不下,需要滚动才能都看到的时候。
当你的组件开始觉得密集或难以阅读时,在多个 property 之间添加空行可以让其变得容易。在一些诸如 Vim 的编辑器里,这样格式化后的选项还能通过键盘被快速导航。
props: { value: { type: String, required: true }, focused: { type: Boolean, default: false }, label: String, icon: String},computed: { formattedValue() { // ... }, inputClasses() { // ... }}
// 没有空行在组件易于阅读和导航时也没问题。props: { value: { type: String, required: true }, focused: { type: Boolean, default: false }, label: String, icon: String},computed: { formattedValue() { // ... }, inputClasses() { // ... }}
单文件组件应该总是让 <script>
、<template>
和 <style>
标签的顺序保持一致。且 <style>
要放在最后,因为另外两个标签至少要有一个。
<style>/* ... */</style><script>/* ... */</script><template>...</template>
<!-- ComponentA.vue --><script>/* ... */</script><template>...</template><style>/* ... */</style><!-- ComponentB.vue --><template>...</template><script>/* ... */</script><style>/* ... */</style>
<!-- ComponentA.vue --><script>/* ... */</script><template>...</template><style>/* ... */</style><!-- ComponentB.vue --><script>/* ... */</script><template>...</template><style>/* ... */</style>
<!-- ComponentA.vue --><template>...</template><script>/* ... */</script><style>/* ... */</style><!-- ComponentB.vue --><template>...</template><script>/* ... */</script><style>/* ... */</style>
scoped
中的元素选择器谨慎使用元素选择器应该避免在 scoped
中出现。
在 scoped
样式中,类选择器比元素选择器更好,因为大量使用元素选择器是很慢的。
详解
为了给样式设置作用域,Vue 会为元素添加一个独一无二的 attribute,例如 data-v-f3f3eg9。然后修改选择器,使得在匹配选择器的元素中,只有带这个 attribute 才会真正生效 (比如 button[data-v-f3f3eg9])。
问题在于大量的元素和 attribute 组合的选择器 (比如 button[data-v-f3f3eg9]) 会比类和 attribute 组合的选择器慢,所以应该尽可能选用类选择器。
<template> <button>×</button></template><style scoped>button { background-color: red;}</style>
<template> <button class="btn btn-close">×</button></template><style scoped>.btn-close { background-color: red;}</style>
应该优先通过 prop 和事件进行父子组件之间的通信,而不是 this.$parent
或变更 prop。
一个理想的 Vue 应用是 prop 向下传递,事件向上传递的。遵循这一约定会让你的组件更易于理解。然而,在一些边界情况下 prop 的变更或 this.$parent
能够简化两个深度耦合的组件。
问题在于,这种做法在很多简单的场景下可能会更方便。但请当心,不要为了一时方便 (少写代码) 而牺牲数据流向的简洁性 (易于理解)。
app.component('TodoItem', { props: { todo: { type: Object, required: true } }, template: '<input v-model="todo.text">'})
app.component('TodoItem', { props: { todo: { type: Object, required: true } }, methods: { removeTodo() { this.$parent.todos = this.$parent.todos.filter(todo => todo.id !== vm.todo.id) } }, template: ` <span> {{ todo.text }} <button @click="removeTodo"> × </button> </span> `})
app.component('TodoItem', { props: { todo: { type: Object, required: true } }, template: ` <input :value="todo.text" @input="$emit('input', $event.target.value)" > `})
app.component('TodoItem', { props: { todo: { type: Object, required: true } }, template: ` <span> {{ todo.text }} <button @click="$emit('delete')"> × </button> </span> `})
应该优先通过 Vuex 管理全局状态,而不是通过 this.$root
或一个全局事件总线。
通过 this.$root
和/或全局事件总线管理状态在很多简单的情况下都是很方便的,但是并不适用于绝大多数的应用。
Vuex 是 Vue 的官方类 flux 实现,其提供的不仅是一个管理状态的中心区域,还是组织、追踪和调试状态变更的好工具。它很好地集成在了 Vue 生态系统之中 (包括完整的 Vue DevTools 支持)。
// main.jsimport { createApp } from 'vue'import mitt from 'mitt'const app = createApp({ data() { return { todos: [], emitter: mitt() } }, created() { this.emitter.on('remove-todo', this.removeTodo) }, methods: { removeTodo(todo) { const todoIdToRemove = todo.id this.todos = this.todos.filter(todo => todo.id !== todoIdToRemove) } }})
// store/modules/todos.jsexport default { state: { list: [] }, mutations: { REMOVE_TODO (state, todoId) { state.list = state.list.filter(todo => todo.id !== todoId) } }, actions: { removeTodo ({ commit, state }, todo) { commit('REMOVE_TODO', todo.id) } }}
<!-- TodoItem.vue --><template> <span> {{ todo.text }} <button @click="removeTodo(todo)"> X </button> </span></template><script>import { mapActions } from 'vuex'export default { props: { todo: { type: Object, required: true } }, methods: mapActions(['removeTodo'])}</script>
Vue.js 设计的初衷就包括可以被渐进式地采用。这意味着它可以根据需求以多种方式集成到一个项目中。
将 Vue.js 添加到项目中有三种主要方式。
最新版本:
每个版本的详细发行说明可在 GitHub 上找到。
当前是 Beta 版——Vuex 和 Router 的集成仍然是 WIP
在使用 Vue 时,我们推荐在你的浏览器上安装 Vue Devtools,它允许你在一个更友好的界面中审查和调试 Vue 应用。
对于制作原型或学习,你可以这样使用最新版本
<script src="https://unpkg.com/vue@next" rel="external nofollow" ></script>
对于生产环境,我们推荐链接到一个明确的版本号和构建文件,以避免新版本造成的不可预期的破坏:
在用 Vue 构建大型应用时推荐使用 npm 安装 。NPM 能很好地和诸如 Webpack 或 Browserify 模块打包器配合使用。同时 Vue 也提供配套工具来开发单文件组件。
# 最新稳定版$ npm install vue@next
Vue 提供了一个官方的 CLI,为单页面应用 (SPA) 快速搭建繁杂的脚手架。它为现代前端工作流提供了 batteries-included 的构建设置。只需要几分钟的时间就可以运行起来并带有热重载、保存时 lint 校验,以及生产环境可用的构建版本。更多详情可查阅 Vue CLI 的文档。
TIP
CLI 工具假定用户对 Node.js 和相关构建工具有一定程度的了解。如果你是新手,我们强烈建议先在不用构建工具的情况下通读指南,在熟悉 Vue 本身之后再使用 CLI。
对于 Vue 3,你应该使用 npm
上可用的 Vue CLI v4.5 作为 @vue/cli@next
。要升级,你应该需要全局重新安装最新版本的 @vue/cli
:
yarn global add @vue/cli@next# ORnpm install -g @vue/cli@next
然后在 Vue 项目运行:
vue upgrade --next
Vite 是一个 web 开发构建工具,由于其原生 ES 模块导入方法,它允许快速提供代码。
通过在终端中运行以下命令,可以使用 Vite 快速构建 Vue 项目。
使用 npm:
$ npm init vite-app <project-name>$ cd <project-name>$ npm install$ npm run dev
或者 yarn:
$ yarn create vite-app <project-name>$ cd <project-name>$ yarn$ yarn dev
在 npm 包的 dist/ 目录你将会找到很多不同的 Vue.js 构建版本。这里列出了它们之间的差别:
vue(.runtime).global(.prod).js
:<script src="...">
直接使用,则暴露 Vue 全局;vue.global.js
是包含编译器和运行时的“完整”构建,因此它支持动态编译模板。vue.runtime.global.js
只包含运行时,并且需要在构建步骤期间预编译模板。*.prod.js
用于生产的文件。提示
全局打包不是 UMD 构建的,它们被打包成 IIFEs,并且仅用于通过 <script src="...">
直接使用。
vue(.runtime).esm-browser(.prod).js
:<script type="module">
;vue(.runtime).esm-bundler.js
:webpack
,rollup
和 parcel
。process.env.NODE_ENV guards
(需要更换构建工具)@vue/runtime-core
,@vue/runtime-compiler
)
vue.runtime.esm-bundler.js
(默认) 仅运行时,并要求所有模板都要预先编译。这是打包工具的默认入口 (通过 package.json
中的 module 字段),因为在使用 bundler 时,模板通常是预先编译的 (例如:在 *.vue
文件中),你需要将打包工具配置 vue 别名到这个文件vue.cjs(.prod).js
:
require()
进行服务器端渲染。target: 'node'
的 webpack 打包在一起,并正确地将 vue
外部化,则将加载此构建。process.env.NODE_env
会自动需要相应的文件。如果需要在客户端上编译模板 (即:将字符串传递给 template 选项,或使用其在 DOM 中 HTML 作为模板挂载到元素),你需要编译器,因此需要完整的版本:
// 需要编译器Vue.createApp({ template: '<div>{{ hi }}</div>'})// 不需要Vue.createApp({ render() { return Vue.h('div', {}, this.hi) }})
当使用 vue-loader
时,*.vue
文件中的模板在生成时预编译为 JavaScript,在最终的打包器中并不需要编译器,因此可以只使用运行时构建。
提示
已经了解 Vue 2,只想了解 Vue 3 的新功能可以参阅迁移指南!
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带你了解其核心概念和一个示例工程。
如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。
TIP
官方指南假设你已了解关于 HTML、CSS 和 JavaScript 的中级知识。如果你刚开始学习前端开发,将框架作为你的第一步可能不是最好的主意——掌握好基础知识再来吧!之前有其它框架的使用经验会有帮助,但这不是必需的
尝试 Vue.js 最简单的方法是使用 Hello World 例子,你可以在浏览器新标签页中打开它,跟着例子学习一些基础用法。
安装教程给出了更多安装 Vue 的方式。请注意我们不推荐新手直接使用 vue-cli
,尤其是在你还不熟悉基于 Node.js 的构建工具时。
Vue.js 的核心是一个允许采用简洁的模板语法来声明式地将数据渲染进 DOM 的系统:
<div id="counter"> Counter: {{ counter }}</div>
const Counter = { data() { return { counter: 0 } }}Vue.createApp(Counter).mount('#counter')
我们已经成功创建了第一个 Vue 应用!看起来这跟渲染一个字符串模板非常类似,但是 Vue 在背后做了大量工作。现在数据和 DOM 已经被建立了关联,所有东西都是响应式的。我们要怎么确认呢?请看下面的示例,其中 counter
property 每秒递增,你将看到渲染的 DOM 是如何变化的:
const CounterApp = { data() { return { counter: 0 } }, mounted() { setInterval(() => { this.counter++ }, 1000) }}
Counter: 36
Stop timer
除了文本插值,我们还可以像这样绑定元素的 attribute:
<div id="bind-attribute"> <span v-bind:title="message"> 鼠标悬停几秒钟查看此处动态绑定的提示信息! </span></div>
const AttributeBinding = { data() { return { message: 'You loaded this page on ' + new Date().toLocaleString() } }}Vue.createApp(AttributeBinding).mount('#bind-attribute')
这里我们遇到了一点新东西。你看到的 v-bind
attribute 被称为指令。指令带有前缀 v-
,以表示它们是 Vue 提供的特殊 attribute。可能你已经猜到了,它们会在渲染的 DOM 上应用特殊的响应式行为。在这里,该指令的意思是:“将这个元素节点的 title
attribute 和当前活跃实例的 message
property 保持一致”。
为了让用户和应用进行交互,我们可以用 v-on
指令添加一个事件监听器,通过它调用在实例中定义的方法:
<div id="event-handling"> <p>{{ message }}</p> <button v-on:click="reverseMessage">反转 Message</button></div>
const EventHandling = { data() { return { message: 'Hello Vue.js!' } }, methods: { reverseMessage() { this.message = this.message .split('') .reverse() .join('') } }}Vue.createApp(EventHandling).mount('#event-handling')
注意在这个方法中,我们更新了应用的状态,但没有触碰 DOM——所有的 DOM 操作都由 Vue 来处理,你编写的代码只需要关注逻辑层面即可。
Vue 还提供了 v-model
指令,它能轻松实现表单输入和应用状态之间的双向绑定。
<div id="two-way-binding"> <p>{{ message }}</p> <input v-model="message" /></div>
const TwoWayBinding = { data() { return { message: 'Hello Vue!' } }}Vue.createApp(TwoWayBinding).mount('#two-way-binding')
控制切换一个元素是否显示也相当简单:
<div id="conditional-rendering"> <span v-if="seen">现在你看到我了</span></div>
const ConditionalRendering = { data() { return { seen: true } }}Vue.createApp(ConditionalRendering).mount('#conditional-rendering')
这个例子演示了我们不仅可以把数据绑定到 DOM 文本或 attribute,还可以绑定到 DOM 的结构。此外,Vue 也提供一个强大的过渡效果系统,可以在 Vue 插入/更新/移除元素时自动应用过渡效果。
你可以在下面的沙盒中将 seen
从 true
更改为 false
,以检查效果:点击此处实现
还有其它很多指令,每个都有特殊的功能。例如,v-for
指令可以绑定数组的数据来渲染一个项目列表:
<div id="list-rendering"> <ol> <li v-for="todo in todos"> {{ todo.text }} </li> </ol></div>
const ListRendering = { data() { return { todos: [ { text: 'Learn JavaScript' }, { text: 'Learn Vue' }, { text: 'Build something awesome' } ] } }}Vue.createApp(ListRendering).mount('#list-rendering')
组件系统是 Vue 的另一个重要概念,因为它是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。
在 Vue 中,组件本质上是一个具有预定义选项的实例。在 Vue 中注册组件很简单:如对 App
对象所做的那样创建一个组件对象,并将其定义在父级组件的 components
选项中:
// 创建 Vue 应用const app = Vue.createApp(...)// 定义名为 todo-item 的新组件app.component('todo-item', { template: `<li>This is a todo</li>`})// 挂载 Vue 应用app.mount(...)
现在,你可以将其放到到另一个组件的模板中:
<ol> <!-- 创建一个 todo-item 组件实例 --> <todo-item></todo-item></ol>
app.component('todo-item', { props: ['todo'], template: `<li>{{ todo.text }}</li>`})
现在,我们可以使用 v-bind
指令将待办项传到循环输出的每个组件中:
<div id="todo-list-app"> <ol> <!-- 现在我们为每个 todo-item 提供 todo 对象 todo 对象是变量,即其内容可以是动态的。 我们也需要为每个组件提供一个“key”,稍后再 作详细解释。 --> <todo-item v-for="item in groceryList" v-bind:todo="item" v-bind:key="item.id" ></todo-item> </ol></div>
const TodoList = { data() { return { groceryList: [ { id: 0, text: 'Vegetables' }, { id: 1, text: 'Cheese' }, { id: 2, text: 'Whatever else humans are supposed to eat' } ] } }}const app = Vue.createApp(TodoList)app.component('todo-item', { props: ['todo'], template: `<li>{{ todo.text }}</li>`})app.mount('#todo-list-app')
尽管这只是一个刻意设计的例子,但是我们已经设法将应用分割成了两个更小的单元。子单元通过 prop 接口与父单元进行了良好的解耦。我们现在可以进一步改进 <todo-item>
组件,提供更为复杂的模板和逻辑,而不会影响到父应用。
在一个大型应用中,有必要将整个应用程序划分为多个组件,以使开发更易管理。在后续教程中我们将详述组件,不过这里有一个 (假想的) 例子,以展示使用了组件的应用模板是什么样的:
<div id="app"> <app-nav></app-nav> <app-view> <app-sidebar></app-sidebar> <app-content></app-content> </app-view></div>
你可能已经注意到 Vue 组件非常类似于自定义元素——它是 Web 组件规范的一部分,这是因为 Vue 的组件语法部分参考了该规范。例如 Vue 组件实现了 Slot API 与 is
attribute。但是,还是有几个关键差别:
虽然 Vue 内部没有使用自定义元素,不过在应用使用自定义元素、或以自定义元素形式发布时,依然有很好的互操作性。Vue CLI 也支持将 Vue 组件构建成为原生的自定义元素。
我们刚才简单介绍了 Vue 核心最基本的功能——本教程的其余部分将更加详细地涵盖这些功能以及其它高阶功能,所以请务必读完整个教程!Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带你了解其核心概念和一个示例工程。如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。如果你想在深入学习 Vue 之前对它有更多了解,我们制作了一个视频,带你了解其核心概念和一个示例工程。如果你已经是有经验的前端开发者,想知道 Vue 与其它库/框架有哪些区别,请查看对比其它框架。
每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的:
const app = Vue.createApp({ /* 选项 */ })
该应用实例是用来在应用中注册“全局”组件的。我们将在后面的指南中详细讨论,简单的例子:
const app = Vue.createApp({})app.component('SearchInput', SearchInputComponent)app.directive('focus', FocusDirective)app.use(LocalePlugin)
应用实例暴露的大多数方法都会返回该同一实例,允许链式:
Vue.createApp({}) .component('SearchInput', SearchInputComponent) .directive('focus', FocusDirective) .use(LocalePlugin)
你可以在 API 参考 中浏览完整的应用 API。
传递给 createApp
的选项用于配置根组件。当我们挂载应用时,该组件被用作渲染的起点。
一个应用需要被挂载到一个 DOM 元素中。例如,如果我们想把一个 Vue 应用挂载到 <div id="app"></div>
,我们应该传递 #app
:
const RootComponent = { /* 选项 */ }const app = Vue.createApp(RootComponent)const vm = app.mount('#app')
与大多数应用方法不同的是,mount
不返回应用本身。相反,它返回的是根组件实例。
虽然没有完全遵循 MVVM 模型_blank_nofollow,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm
(ViewModel 的缩写) 这个变量名表示组件实例。
尽管本页面上的所有示例都只需要一个单一的组件就可以,但是大多数的真实应用都是被组织成一个嵌套的、可重用的组件树。举个例子,一个 todo 应用组件树可能是这样的:
Root Component└─ TodoList ├─ TodoItem │ ├─ DeleteTodoButton │ └─ EditTodoButton └─ TodoListFooter ├─ ClearTodosButton └─ TodoListStatistics
每个组件将有自己的组件实例 vm
。对于一些组件,如 TodoItem
,在任何时候都可能有多个实例渲染。这个应用中的所有组件实例将共享同一个应用实例。
我们会在稍后的组件系统章节具体展开。不过现在,你只需要明白根组件与其他组件没什么不同,配置选项是一样的,所对应的组件实例行为也是一样的。
在前面的指南中,我们认识了 data
property。在 data
中定义的 property 是通过组件实例暴露的:
const app = Vue.createApp({ data() { return { count: 4 } }})const vm = app.mount('#app')console.log(vm.count) // => 4
还有各种其他的组件选项,可以将用户定义的 property 添加到组件实例中,例如 methods
,props
,computed
,inject
和 setup
。我们将在后面的指南中深入讨论它们。组件实例的所有 property,无论如何定义,都可以在组件的模板中访问。
Vue 还通过组件实例暴露了一些内置 property,如 $attrs
和 $emit
。这些 property 都有一个 $
前缀,以避免与用户定义的 property 名冲突。
每个组件在被创建时都要经过一系列的初始化过程——例如,需要设置数据监听、编译模板、将实例挂载到 DOM 并在数据变化时更新 DOM 等。同时在这个过程中也会运行一些叫做生命周期钩子的函数,这给了用户在不同阶段添加自己的代码的机会。
比如 created 钩子可以用来在一个实例被创建之后执行代码:
Vue.createApp({ data() { return { count: 1} }, created() { // `this` 指向 vm 实例 console.log('count is: ' + this.count) // => "count is: 1" }})
也有一些其它的钩子,在实例生命周期的不同阶段被调用,如 mounted、updated 和 unmounted。生命周期钩子的 this
上下文指向调用它的当前活动实例。
TIP
不要在选项 property 或回调上使用箭头函数,比如 created: () => console.log(this.a)
或 vm.$watch('a', newValue => this.myMethod())
。因为箭头函数并没有 this
,this
会作为变量一直向上级词法作用域查找,直至找到为止,经常导致 Uncaught TypeError: Cannot read property of undefined
或 Uncaught TypeError: this.myMethod is not a function
之类的错误。
下图展示了实例的生命周期。你不需要立马弄明白所有的东西,不过随着你的不断学习和使用,它的参考价值会越来越高。
每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的每个 Vue 应用都是通过用 createApp
函数创建一个新的应用实例开始的
Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层组件实例的数据。所有 Vue.js 的模板都是合法的 HTML,所以能被遵循规范的浏览器和 HTML 解析器解析。
在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应性系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。
如果你熟悉虚拟 DOM 并且偏爱 JavaScript 的原始力量,你也可以不用模板,直接写渲染 (render) 函数,使用可选的 JSX 语法。
数据绑定最常见的形式就是使用“Mustache”语法 (双大括号) 的文本插值:
<span>Message: {{ msg }}</span>
Mustache 标签将会被替代为对应组件实例中 msg
property 的值。无论何时,绑定的组件实例上 msg
property 发生了改变,插值处的内容都会更新。
通过使用 v-once 指令,你也能执行一次性地插值,当数据改变时,插值处的内容不会更新。但请留心这会影响到该节点上的其它数据绑定:
<span v-once>这个将不会改变: {{ msg }}</span>
双大括号会将数据解释为普通文本,而非 HTML 代码。为了输出真正的 HTML,你需要使用v-html
指令:
<p>Using mustaches: {{ rawHtml }}</p><p>Using v-html directive: <span v-html="rawHtml"></span></p>
这个 span
的内容将会被替换成为 property 值 rawHtml
,直接作为 HTML——会忽略解析 property 值中的数据绑定。注意,你不能使用 v-html
来复合局部模板,因为 Vue 不是基于字符串的模板引擎。反之,对于用户界面 (UI),组件更适合作为可重用和可组合的基本单位。
TIP
在你的站点上动态渲染任意的 HTML 是非常危险的,因为它很容易导致 XSS 攻击。请只对可信内容使用 HTML 插值,绝不要将用户提供的内容作为插值。
Mustache 语法不能在 HTML attribute 中使用 ,然而,可以使用 v-bind
指令:
<div v-bind:id="dynamicId"></div>
对于布尔 attribute (它们只要存在就意味着值为 true
),v-bind
工作起来略有不同,在这个例子中:
<button v-bind:disabled="isButtonDisabled">按钮</button>
如果 isButtonDisabled
的值是 null
或 undefined
,则 disabled
attribute 甚至不会被包含在渲染出来的 <button>
元素中。
迄今为止,在我们的模板中,我们一直都只绑定简单的 property 键值。但实际上,对于所有的数据绑定,Vue.js 都提供了完全的 JavaScript 表达式支持。
{{ number + 1 }} {{ ok ? 'YES' : 'NO' }} {{ message.split('').reverse().join('')}}<div v-bind:id="'list-' + id"></div>
这些表达式会在当前活动实例的数据作用域下作为 JavaScript 被解析。有个限制就是,每个绑定都只能包含单个表达式,所以下面的例子都不会生效。
<!-- 这是语句,不是表达式:-->{{ var a = 1 }}<!-- 流控制也不会生效,请使用三元表达式 -->{{ if (ok) { return message } }}
指令 (Directives) 是带有 v-
前缀的特殊 attribute。指令 attribute 的值预期是单个 JavaScript 表达式 (v-for
和 v-on
是例外情况,稍后我们再讨论)。指令的职责是,当表达式的值改变时,将其产生的连带影响,响应式地作用于 DOM。回顾我们在介绍中看到的例子:
<p v-if="seen">现在你看到我了</p>
这里,v-if
指令将根据表达式 seen
的值的真假来插入/移除 <p>
元素。
一些指令能够接收一个“参数”,在指令名称之后以冒号表示。例如,v-bind
指令可以用于响应式地更新 HTML attribute:
<a v-bind:href="url"> ... </a>
在这里 href
是参数,告知 v-bind
指令将该元素的 href
attribute 与表达式 url
的值绑定。
另一个例子是 v-on
指令,它用于监听 DOM 事件:
<a v-on:click="doSomething"> ... </a>
在这里参数是监听的事件名。我们也会更详细地讨论事件处理。
也可以在指令参数中使用 JavaScript 表达式,方法是用方括号括起来:
<!--注意,参数表达式的写法存在一些约束,如之后的“对动态参数表达式的约束”章节所述。--><a v-bind:[attributeName]="url"> ... </a>
这里的 attributeName
会被作为一个 JavaScript 表达式进行动态求值,求得的值将会作为最终的参数来使用。例如,如果你的组件实例有一个 data property attributeName
,其值为 "href"
,那么这个绑定将等价于 v-bind:href
。
同样地,你可以使用动态参数为一个动态的事件名绑定处理函数:
<a v-on:[eventName]="doSomething"> ... </a>
在这个示例中,当 eventName
的值为 "focus"
时,v-on:[eventName]
将等价于 v-on:focus
修饰符 (modifier) 是以半角句号.
指明的特殊后缀,用于指出一个指令应该以特殊方式绑定。例如,.prevent
修饰符告诉 v-on
指令对于触发的事件调用 event.preventDefault()
:
<form v-on:submit.prevent="onSubmit">...</form>
在接下来对 v-on
和 v-for
等功能的探索中,你会看到修饰符的其它例子。
v-
前缀作为一种视觉提示,用来识别模板中 Vue 特定的 attribute。当你在使用 Vue.js 为现有标签添加动态行为 (dynamic behavior) 时,v- 前缀很有帮助,然而,对于一些频繁用到的指令来说,就会感到使用繁琐。同时,在构建由 Vue 管理所有模板的单页面应用程序 (SPA - single page application) 时,v-
前缀也变得没那么重要了。因此,Vue 为 v-bind
和 v-on
这两个最常用的指令,提供了特定简写:
v-bind
缩写<!-- 完整语法 --><a v-bind:href="url"> ... </a><!-- 缩写 --><a :href="url"> ... </a><!-- 动态参数的缩写 --><a :[key]="url"> ... </a>
v-on
缩写<!-- 完整语法 --><a v-on:click="doSomething"> ... </a><!-- 缩写 --><a @click="doSomething"> ... </a><!-- 动态参数的缩写 (2.6.0+) --><a @[event]="doSomething"> ... </a>
它们看起来可能与普通的 HTML 略有不同,但 :
与 @
对于 attribute 名来说都是合法字符,在所有支持 Vue 的浏览器都能被正确地解析。而且,它们不会出现在最终渲染的标记中。缩写语法是完全可选的,但随着你更深入地了解它们的作用,你会庆幸拥有它们。
从下一页开始,我们将在示例中使用缩写,因为这是 Vue 开发者最常用的用法。
动态参数预期会求出一个字符串,异常情况下值为 null
。这个特殊的 null
值可以被显性地用于移除绑定。任何其它非字符串类型的值都将会触发一个警告。
动态参数表达式有一些语法约束,因为某些字符,如空格和引号,放在 HTML attribute 名里是无效的。例如:
<!-- 这会触发一个编译警告 --><a v-bind:['foo' + bar]="value"> ... </a>
变通的办法是使用没有空格或引号的表达式,或用计算属性替代这种复杂表达式。
在 DOM 中使用模板时 (直接在一个 HTML 文件里撰写模板),还需要避免使用大写字符来命名键名,因为浏览器会把 attribute 名全部强制转为小写:
<!--在 DOM 中使用模板时这段代码会被转换为 `v-bind:[someattr]`。除非在实例中有一个名为“someattr”的 property,否则代码不会工作。--><a v-bind:[someAttr]="value"> ... </a>
模板表达式都被放在沙盒中,只能访问全局变量的一个白名单,如 Math
和 Date
。你不应该在模板表达式中试图访问用户定义的全局变量。
组件的 data
选项是一个函数。Vue 在创建新组件实例的过程中调用此函数。它应该返回一个对象,然后 Vue 会通过响应性系统将其包裹起来,并以 $data
的形式存储在组件实例中。为方便起见,该对象的任何顶级 property 也直接通过组件实例暴露出来:
const app = Vue.createApp({ data() { return { count: 4 } }})const vm = app.mount('#app')console.log(vm.$data.count) // => 4console.log(vm.count) // => 4// 修改 vm.count 的值也会更新 $data.countvm.count = 5console.log(vm.$data.count) // => 5// 反之亦然vm.$data.count = 6console.log(vm.count) // => 6
这些实例 property 仅在实例首次创建时被添加,所以你需要确保它们都在 data
函数返回的对象中。必要时,要对尚未提供所需值的 property 使用 null
、undefined
或其他占位的值。。
直接将不包含在 data
中的新 property 添加到组件实例是可行的。但由于该 property 不在背后的响应式 $data
对象内,所以 Vue 的响应性系统不会自动跟踪它。
Vue 使用 $
前缀通过组件实例暴露自己的内置 API。它还为内部 property 保留 _
前缀。你应该避免使用这两个字符开头的的顶级 data
property 名称。
我们用 methods
选项向组件实例添加方法,它应该是一个包含所需方法的对象:
const app = Vue.createApp({ data() { return { count: 4 } }, methods: { increment() { // `this` 指向该组件实例 this.count++ } }})const vm = app.mount('#app')console.log(vm.count) // => 4vm.increment()console.log(vm.count) // => 5
Vue 自动为 methods
绑定 this
,以便于它始终指向组件实例。这将确保方法在用作事件监听或回调时保持正确的 this
指向。在定义 methods
时应避免使用箭头函数,因为这会阻止 Vue 绑定恰当的 this
指向。
这些 methods
和组件实例的其它所有 property 一样可以在组件的模板中被访问。在模板中,它们通常被当做事件监听使用:
<button @click="increment">Up vote</button>
在上面的例子中,点击 <button>
时,会调用 increment
方法。
也可以直接从模板中调用方法。就像下一章节即将看到的,通常换做计算属性会更好。但是,在计算属性不可行的情况下,使用方法可能会很有用。你可以在模板支持 JavaScript 表达式的任何地方调用方法:
<span :title="toTitleDate(date)"> {{ formatDate(date) }}</span>
如果 toTitleDate
或 formatDate
访问任何响应式数据,则将其作为渲染依赖项进行跟踪,就像直接在模板中使用过一样。
从模板调用的方法不应该有任何副作用,比如更改数据或触发异步进程。如果你想这么做,应该换做生命周期钩子。
Vue 没有内置支持防抖和节流,但可以使用 Lodash 等库来实现。
如果某个组件仅使用一次,可以在 methods
中直接应用防抖:
<script src="https://unpkg.com/lodash@4.17.20/lodash.min.js" rel="external nofollow" ></script><script> Vue.createApp({ methods: { // 用 Lodash 的防抖函数 click: _.debounce(function() { // ... 响应点击 ... }, 500) } }).mount('#app')</script>
但是,这种方法对于可复用组件有潜在的问题,因为它们都共享相同的防抖函数。为了使组件实例彼此独立,可以在生命周期钩子的 created
里添加该防抖函数:
app.component('save-button', { created() { // 用 Lodash 的防抖函数 this.debouncedClick = _.debounce(this.click, 500) }, unmounted() { // 移除组件时,取消定时器 this.debouncedClick.cancel() }, methods: { click() { // ... 响应点击 ... } }, template: ` <button @click="debouncedClick"> Save </button> `})
模板内的表达式非常便利,但是设计它们的初衷是用于简单运算的。在模板中放入太多的逻辑会让模板过重且难以维护。例如,有一个嵌套数组对象:
Vue.createApp({ data() { return { author: { name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] } } }})
我们想根据 author
是否已经有一些书来显示不同的消息
<div id="computed-basics"> <p>Has published books:</p> <span>{{ author.books.length > 0 ? 'Yes' : 'No' }}</span></div>
此时,模板不再是简单的和声明性的。你必须先看一下它,然后才能意识到它执行的计算取决于 author.books
。如果要在模板中多次包含此计算,则问题会变得更糟。
所以,对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性。
<div id="computed-basics"> <p>Has published books:</p> <span>{{ publishedBooksMessage }}</span></div>
Vue.createApp({ data() { return { author: { name: 'John Doe', books: [ 'Vue 2 - Advanced Guide', 'Vue 3 - Basic Guide', 'Vue 4 - The Mystery' ] } } }, computed: { // 计算属性的 getter publishedBooksMessage() { // `this` points to the vm instance return this.author.books.length > 0 ? 'Yes' : 'No' } }}).mount('#computed-basics')
Result:点击此处实现
这里声明了一个计算属性 publishedBooksMessage
。
尝试更改应用程序 data
中 books
数组的值,你将看到 publishedBooksMessage
如何相应地更改。
你可以像普通属性一样将数据绑定到模板中的计算属性。Vue 知道 vm.publishedBookMessage
依赖于 vm.author.books
,因此当 vm.author.books
发生改变时,所有依赖 vm.publishedBookMessage
绑定也会更新。而且最妙的是我们已经声明的方式创建了这个依赖关系:计算属性的 getter 函数没有副作用,这使得更易于测试和理解。
你可能已经注意到我们可以通过在表达式中调用方法来达到同样的效果:
<p>{{ calculateBooksMessage() }}</p>
// 在组件中methods: { calculateBooksMessage() { return this.author.books.length > 0 ? 'Yes' : 'No' }}
我们可以将同一函数定义为一个方法而不是一个计算属性。两种方式的最终结果确实是完全相同的。然而,不同的是计算属性是基于它们的反应依赖关系缓存的。计算属性只在相关响应式依赖发生改变时它们才会重新求值。这就意味着只要 author.books
还没有发生改变,多次访问 publishedBookMessage
计算属性会立即返回之前的计算结果,而不必再次执行函数。
这也同样意味着下面的计算属性将不再更新,因为 Date.now () 不是响应式依赖:
computed: { now() { return Date.now() }}
相比之下,每当触发重新渲染时,调用方法将总会再次执行函数。
我们为什么需要缓存?假设我们有一个性能开销比较大的计算属性 list
,它需要遍历一个巨大的数组并做大量的计算。然后我们可能有其他的计算属性依赖于 list
。如果没有缓存,我们将不可避免的多次执行 list
的 getter!如果你不希望有缓存,请用 method
来替代。
计算属性默认只有 getter,不过在需要时你也可以提供一个 setter:
// ...computed: { fullName: { // getter get() { return this.firstName + ' ' + this.lastName }, // setter set(newValue) { const names = newValue.split(' ') this.firstName = names[0] this.lastName = names[names.length - 1] } }}// ...
现在再运行 vm.fullName = 'John Doe'
时,setter 会被调用,vm.firstName
和 vm.lastName
也会相应地被更新。
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch
选项提供了一个更通用的方法,来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。
例如:
<div id="watch-example"> <p> Ask a yes/no question: <input v-model="question" /> </p> <p>{{ answer }}</p></div>
<!-- 因为 AJAX 库和通用工具的生态已经相当丰富,Vue 核心代码没有重复 --><!-- 提供这些功能以保持精简。这也可以让你自由选择自己更熟悉的工具。 --><script src="https://cdn.jsdelivr.net/npm/axios@0.12.0/dist/axios.min.js" rel="external nofollow" ></script><script> const watchExampleVM = Vue.createApp({ data() { return { question: '', answer: 'Questions usually contain a question mark. ;-)' } }, watch: { // whenever question changes, this function will run question(newQuestion, oldQuestion) { if (newQuestion.indexOf('?') > -1) { this.getAnswer() } } }, methods: { getAnswer() { this.answer = 'Thinking...' axios .get('https://yesno.wtf/api') .then(response => { this.answer = response.data.answer }) .catch(error => { this.answer = 'Error! Could not reach the API. ' + error }) } } }).mount('#watch-example')</script>
结果:点击此处实现
在这个示例中,使用 watch
选项允许我们执行异步操作 (访问一个 API),限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态。这些都是计算属性无法做到的。
除了 watch 选项之外,你还可以使用命令式的 vm.$watch API。
Vue 提供了一种更通用的方式来观察和响应当前活动的实例上的数据变动:侦听属性。当你有一些数据需要随着其它数据变动而变动时,你很容易滥用 watch
——特别是如果你之前使用过 AngularJS。然而,通常更好的做法是使用计算属性而不是命令式的 watch
回调。细想一下这个例子:
<div id="demo">{{ fullName }}</div>
const vm = Vue.createApp({ data() { return { firstName: 'Foo', lastName: 'Bar', fullName: 'Foo Bar' } }, watch: { firstName(val) { this.fullName = val + ' ' + this.lastName }, lastName(val) { this.fullName = this.firstName + ' ' + val } }}).mount('#demo')
上面代码是命令式且重复的。将它与计算属性的版本进行比较:
const vm = Vue.createApp({ data() { return { firstName: 'Foo', lastName: 'Bar' } }, computed: { fullName() { return this.firstName + ' ' + this.lastName } }}).mount('#demo')
好得多了,不是吗?
操作元素的 class 列表和内联样式是数据绑定的一个常见需求。因为它们都是 attribute,所以我们可以用 v-bind
处理它们:只需要通过表达式计算出字符串结果即可。不过,字符串拼接麻烦且易错。因此,在将 v-bind
用于 class
和 style
时,Vue.js 做了专门的增强。表达式结果的类型除了字符串之外,还可以是对象或数组。
我们可以传给 :class
(v-bind:class
的简写) 一个对象,以动态地切换 class:
<div :class="{ active: isActive }"></div>
上面的语法表示 active
这个 class 存在与否将取决于数据 property isActive
的 truthiness。
你可以在对象中传入更多字段来动态切换多个 class。此外,:class
指令也可以与普通的 class
attribute 共存。当有如下模板:
<div class="static" :class="{ active: isActive, 'text-danger': hasError }"></div>
和如下 data:
data() { return { isActive: true, hasError: false }}
渲染的结果为:
<div class="static active"></div>
当 isActive
或者 hasError
变化时,class 列表将相应地更新。例如,如果 hasError
的值为 true
,class 列表将变为 "static active text-danger"
。
绑定的数据对象不必内联定义在模板里:
<div :class="classObject"></div>
data() { return { classObject: { active: true, 'text-danger': false } }}
渲染的结果和上面一样。我们也可以在这里绑定一个返回对象的计算属性。这是一个常用且强大的模式:
<div :class="classObject"></div>
data() { return { isActive: true, error: null }},computed: { classObject() { return { active: this.isActive && !this.error, 'text-danger': this.error && this.error.type === 'fatal' } }}
我们可以把一个数组传给 :class
,以应用一个 class 列表:
<div :class="[activeClass, errorClass]"></div>
data() { return { activeClass: 'active', errorClass: 'text-danger' }}
渲染的结果为:
<div class="active text-danger"></div>
如果你想根据条件切换列表中的 class,可以使用三元表达式:
<div :class="[isActive ? activeClass : '', errorClass]"></div>
这样写将始终添加 errorClass
,但是只有在 isActive
为 truthy[1] 时才添加 activeClass。
不过,当有多个条件 class 时这样写有些繁琐。所以在数组语法中也可以使用对象语法:
<div :class="[{ active: isActive }, errorClass]"></div>
这个章节假设你已经对 Vue 组件有一定的了解。当然你也可以先跳过这里,稍后再回过头来看。
例如,如果你声明了这个组件:
const app = Vue.createApp({})app.component('my-component', { template: `<p class="foo bar">Hi!</p>`})
然后在使用它的时候添加一些 class:
<div id="app"> <my-component class="baz boo"></my-component></div>
HTML 将被渲染为:
<p class="foo bar baz boo">Hi</p>
对于带数据绑定 class 也同样适用:
<my-component :class="{ active: isActive }"></my-component>
当 isActive 为 truthy[1] 时,HTML 将被渲染成为:
<p class="foo bar active">Hi</p>
如果你的组件有多个根元素,你需要定义哪些部分将接收这个类。可以使用 $attrs
组件属性执行此操作:
<div id="app"> <my-component class="baz"></my-component></div>
const app = Vue.createApp({})app.component('my-component', { template: ` <p :class="$attrs.class">Hi!</p> <span>This is a child component</span> `})
你可以在非 prop Attribute 小节了解更多关于组件属性继承的信息。
:style
的对象语法十分直观——看着非常像 CSS,但其实是一个 JavaScript 对象。CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名:
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data() { return { activeColor: 'red', fontSize: 30 }}
直接绑定到一个样式对象通常更好,这会让模板更清晰:
<div :style="styleObject"></div>
data() { return { styleObject: { color: 'red', fontSize: '13px' } }}
同样的,对象语法常常结合返回对象的计算属性使用。
:style
的数组语法可以将多个样式对象应用到同一个元素上:
<div :style="[baseStyles, overridingStyles]"></div>
在 :style
中使用需要 (浏览器引擎前缀) vendor prefixes 的 CSS property 时,如 transform
,Vue 将自动侦测并添加相应的前缀。
可以为 style 绑定中的 property 提供一个包含多个值的数组,常用于提供多个带前缀的值,例如:
<div :style="{ display: ['-webkit-box', '-ms-flexbox', 'flex'] }"></div>
这样写只会渲染数组中最后一个被浏览器支持的值。在本例中,如果浏览器支持不带浏览器前缀的 flexbox,那么就只会渲染 display: flex
。
v-if
v-if
指令用于条件性地渲染一块内容。这块内容只会在指令的表达式返回 truthy 值的时候被渲染。
<h1 v-if="awesome">Vue is awesome!</h1>
也可以用 v-else
添加一个“else 块”:
<h1 v-if="awesome">Vue is awesome!</h1><h1 v-else>Oh no ????</h1>
<template>
元素上使用 v-if
条件渲染分组因为 v-if
是一个指令,所以必须将它添加到一个元素上。但是如果想切换多个元素呢?此时可以把一个 <template>
元素当做不可见的包裹元素,并在上面使用 v-if
。最终的渲染结果将不包含 <template>
元素。
<template v-if="ok"> <h1>Title</h1> <p>Paragraph 1</p> <p>Paragraph 2</p></template>
v-else
你可以使用 v-else
指令来表示 v-if
的“else 块”:
<div v-if="Math.random() > 0.5"> Now you see me</div><div v-else> Now you don't</div>
v-else
元素必须紧跟在带 v-if
或者 v-else-if
的元素的后面,否则它将不会被识别。
v-else-if
v-else-if
,顾名思义,充当 v-if
的“else-if 块”,可以连续使用:
<div v-if="type === 'A'"> A</div><div v-else-if="type === 'B'"> B</div><div v-else-if="type === 'C'"> C</div><div v-else> Not A/B/C</div>
类似于 v-else
,v-else-if
也必须紧跟在带 v-if
或者 v-else-if
的元素之后。
v-show
另一个用于根据条件展示元素的选项是 v-show
指令。用法大致一样:
<h1 v-show="ok">Hello!</h1>
不同的是带有 v-show
的元素始终会被渲染并保留在 DOM 中。v-show
只是简单地切换元素的 CSS property display。
注意,v-show
不支持 <template>
元素,也不支持 v-else
。
v-if
vs v-show
v-if
是“真正”的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。
v-if
也是惰性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
相比之下,v-show
就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
一般来说,v-if
有更高的切换开销,而 v-show
有更高的初始渲染开销。因此,如果需要非常频繁地切换,则使用 v-show
较好;如果在运行时条件很少改变,则使用 v-if
较好。
v-if
与 v-for
一起使用提示
不推荐同时使用 v-if
和 v-for
。请查阅风格指南以获取更多信息。
当 v-if
与 v-for
一起使用时,v-if
具有比 v-for
更高的优先级。请查阅列表渲染指南以获取详细信息。
v-for
把一个数组对应为一组元素我们可以用 v-for
指令基于一个数组来渲染一个列表。v-for
指令需要使用 item in items
形式的特殊语法,其中 items 是源数据数组,而 item
则是被迭代的数组元素的别名。
<ul id="array-rendering"> <li v-for="item in items"> {{ item.message }} </li></ul>
Vue.createApp({ data() { return { items: [{ message: 'Foo' }, { message: 'Bar' }] } }}).mount('#array-rendering')
结果:点击此处实现
在 v-for
块中,我们可以访问所有父作用域的 property。v-for
还支持一个可选的第二个参数,即当前项的索引。
<ul id="array-with-index"> <li v-for="(item, index) in items"> {{ parentMessage }} - {{ index }} - {{ item.message }} </li></ul>
Vue.createApp({ data() { return { parentMessage: 'Parent', items: [{ message: 'Foo' }, { message: 'Bar' }] } }}).mount('#array-with-index')
结果:点击此处实现
你也可以用 of
替代 in
作为分隔符,因为它更接近 JavaScript 迭代器的语法:
<div v-for="item of items"></div>
v-for
里使用对象你也可以用 v-for
来遍历一个对象的 property。
<ul id="v-for-object" class="demo"> <li v-for="value in myObject"> {{ value }} </li></ul>
Vue.createApp({ data() { return { myObject: { title: 'How to do lists in Vue', author: 'Jane Doe', publishedAt: '2016-04-10' } } }}).mount('#v-for-object')
结果:点击此处实现
你也可以提供第二个的参数为 property 名称 (也就是键名 key):
<li v-for="(value, name) in myObject"> {{ name }}: {{ value }}</li>
还可以用第三个参数作为索引:
<li v-for="(value, name, index) in myObject"> {{ index }}. {{ name }}: {{ value }}</li>
提示
在遍历对象时,会按 Object.keys()
的结果遍历,但是不能保证它在不同 JavaScript 引擎下的结果都一致。
当 Vue 正在更新使用 v-for
渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。
这个默认的模式是高效的,但是只适用于不依赖子组件状态或临时 DOM 状态 (例如:表单输入值) 的列表渲染输出。
为了给 Vue 一个提示,以便它能跟踪每个节点的身份,从而重用和重新排序现有元素,你需要为每项提供一个唯一 key
attribute:
<div v-for="item in items" :key="item.id"> <!-- content --></div>
建议尽可能在使用 v-for
时提供 key
attribute,除非遍历输出的 DOM 内容非常简单,或者是刻意依赖默认行为以获取性能上的提升。
因为它是 Vue 识别节点的一个通用机制,key
并不仅与 v-for
特别关联。后面我们将在指南中看到,它还具有其它用途。
提示
不要使用对象或数组之类的非基本类型值作为 v-for
的 key。请用字符串或数值类型的值。
更多 key
attribute 的细节用法请移步至 key
的 API 文档。
Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括:
push()
pop()
shift()
unshift()
splice()
sort()
reverse()
你可以打开控制台,然后对前面例子的 items
数组尝试调用变更方法。比如 example1.items.push({ message: 'Baz' })
。
变更方法,顾名思义,会变更调用了这些方法的原始数组。相比之下,也有非变更方法,例如 filter()
、concat()
和 slice()
。它们不会变更原始数组,而总是返回一个新数组。当使用非变更方法时,可以用新数组替换旧数组:
example1.items = example1.items.filter(item => item.message.match(/Foo/))
你可能认为这将导致 Vue 丢弃现有 DOM 并重新渲染整个列表。幸运的是,事实并非如此。Vue 为了使得 DOM 元素得到最大范围的重用而实现了一些智能的启发式方法,所以用一个含有相同元素的数组去替换原来的数组是非常高效的操作。
有时,我们想要显示一个数组经过过滤或排序后的版本,而不实际变更或重置原始数据。在这种情况下,可以创建一个计算属性,来返回过滤或排序后的数组。
例如:
<li v-for="n in evenNumbers">{{ n }}</li>
data() { return { numbers: [ 1, 2, 3, 4, 5 ] }},computed: { evenNumbers() { return this.numbers.filter(number => number % 2 === 0) }}
在计算属性不适用的情况下 (例如,在嵌套 v-for
循环中) 你可以使用一个方法:
<ul v-for="numbers in sets"> <li v-for="n in even(numbers)">{{ n }}</li></ul>
data() { return { sets: [[ 1, 2, 3, 4, 5 ], [6, 7, 8, 9, 10]] }},methods: { even(numbers) { return numbers.filter(number => number % 2 === 0) }}
v-for
里使用值的范围v-for
也可以接受整数。在这种情况下,它会把模板重复对应次数。
<div id="range" class="demo"> <span v-for="n in 10">{{ n }} </span></div>
结果:点击此处实现
<template>
中使用 v-for
类似于 v-if
,你也可以利用带有 v-for
的 <template>
来循环渲染一段包含多个元素的内容。比如:
<ul> <template v-for="item in items"> <li>{{ item.msg }}</li> <li class="divider" role="presentation"></li> </template></ul>
v-for
与 v-if
一同使用TIP
注意我们不推荐在同一元素上使用 v-if
和 v-for
。更多细节可查阅风格指南。
当它们处于同一节点,v-if
的优先级比 v-for
更高,这意味着 v-if
将没有权限访问 v-for
里的变量:
<!-- This will throw an error because property "todo" is not defined on instance. --><li v-for="todo in todos" v-if="!todo.isComplete"> {{ todo }}</li>
可以把 v-for
移动到 <template>
标签中来修正:
<template v-for="todo in todos"> <li v-if="!todo.isComplete"> {{ todo }} </li></template>
v-for
这部分内容假定你已经了解组件相关知识。你也完全可以先跳过它,以后再回来查看。
在自定义组件上,你可以像在任何普通元素上一样使用 v-for
:
<my-component v-for="item in items" :key="item.id"></my-component>
然而,任何数据都不会被自动传递到组件里,因为组件有自己独立的作用域。为了把迭代数据传递到组件里,我们要使用 props:
<my-component v-for="(item, index) in items" :item="item" :index="index" :key="item.id"></my-component>
不自动将 item
注入到组件里的原因是,这会使得组件与 v-for
的运作紧密耦合。明确组件数据的来源能够使组件在其他场合重复使用。
下面是一个简单的 todo 列表的完整例子:
<div id="todo-list-example"> <form v-on:submit.prevent="addNewTodo"> <label for="new-todo">Add a todo</label> <input v-model="newTodoText" id="new-todo" placeholder="E.g. Feed the cat" /> <button>Add</button> </form> <ul> <todo-item v-for="(todo, index) in todos" :key="todo.id" :title="todo.title" @remove="todos.splice(index, 1)" ></todo-item> </ul></div>
const app = Vue.createApp({ data() { return { newTodoText: '', todos: [ { id: 1, title: 'Do the dishes' }, { id: 2, title: 'Take out the trash' }, { id: 3, title: 'Mow the lawn' } ], nextTodoId: 4 } }, methods: { addNewTodo() { this.todos.push({ id: this.nextTodoId++, title: this.newTodoText }) this.newTodoText = '' } }})app.component('todo-item', { template: ` <li> {{ title }} <button @click="$emit('remove')">Remove</button> </li> `, props: ['title']})app.mount('#todo-list-example')
我们可以使用 v-on
指令 (通常缩写为 @
符号) 来监听 DOM 事件,并在触发事件时执行一些 JavaScript。用法为 v-on:click="methodName"
或使用快捷方式 @click="methodName"
例如:
<div id="basic-event"> <button @click="counter += 1">Add 1</button> <p>The button above has been clicked {{ counter }} times.</p></div>
Vue.createApp({ data() { return { counter: 1 } }}).mount('#basic-event')
结果:点击此处实现
然而许多事件处理逻辑会更为复杂,所以直接把 JavaScript 代码写在 v-on
指令中是不可行的。因此 v-on
还可以接收一个需要调用的方法名称。
例如:
<div id="event-with-method"> <!-- `greet` 在下面定义的方法名 --> <button @click="greet">Greet</button></div>
Vue.createApp({ data() { return { name: 'Vue.js' } }, methods: { greet(event) { // `this` 内部 `methods` 指向当前活动实例 alert('Hello ' + this.name + '!') // `event` 是原生 DOM event if (event) { alert(event.target.tagName) } } }}).mount('#event-with-method')
结果:点击此处实现
除了直接绑定到一个方法,也可以在内联 JavaScript 语句中调用方法:
<div id="inline-handler"> <button @click="say('hi')">Say hi</button> <button @click="say('what')">Say what</button></div>
Vue.createApp({ methods: { say(message) { alert(message) } }}).mount('#inline-handler')
结果:点击此处实现
有时也需要在内联语句处理器中访问原始的 DOM 事件。可以用特殊变量 $event
把它传入方法:
<button @click="warn('Form cannot be submitted yet.', $event)"> Submit</button>
// ...methods: { warn(message, event) { // now we have access to the native event if (event) { event.preventDefault() } alert(message) }}
事件处理程序中可以有多个方法,这些方法由逗号运算符分隔:
<!-- 这两个 one() 和 two() 将执行按钮点击事件 --><button @click="one($event), two($event)"> Submit</button>
// ...methods: { one(event) { // first handler logic... }, two(event) { // second handler logic... }}
在事件处理程序中调用 event.preventDefault()
或 event.stopPropagation()
是非常常见的需求。尽管我们可以在方法中轻松实现这点,但更好的方式是:方法只有纯粹的数据逻辑,而不是去处理 DOM 事件细节。
为了解决这个问题,Vue.js 为 v-on
提供了事件修饰符。之前提过,修饰符是由点开头的指令后缀来表示的。
.stop
.prevent
.capture
.self
.once
.passive
<!-- 阻止单击事件继续传播 --><a @click.stop="doThis"></a><!-- 提交事件不再重载页面 --><form @submit.prevent="onSubmit"></form><!-- 修饰符可以串联 --><a @click.stop.prevent="doThat"></a><!-- 只有修饰符 --><form @submit.prevent></form><!-- 添加事件监听器时使用事件捕获模式 --><!-- 即内部元素触发的事件先在此处理,然后才交由内部元素进行处理 --><div @click.capture="doThis">...</div><!-- 只当在 event.target 是当前元素自身时触发处理函数 --><!-- 即事件不是从内部元素触发的 --><div @click.self="doThat">...</div>
TIP
使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self
会阻止所有的点击,而 v-on:click.self.prevent
只会阻止对元素自身的点击。
<!-- 点击事件将只会触发一次 --><a @click.once="doThis"></a>
不像其它只能对原生的 DOM 事件起作用的修饰符,.once
修饰符还能被用到自定义的组件事件上。如果你还没有阅读关于组件的文档,现在大可不必担心。
Vue 还对应 addEventListener
中的 passive 选项提供了 .passive
修饰符。
<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发 --><!-- 而不会等待 `onScroll` 完成 --><!-- 这其中包含 `event.preventDefault()` 的情况 --><div @scroll.passive="onScroll">...</div>
这个 .passive
修饰符尤其能够提升移动端的性能。
TIP
不要把 .passive
和 .prevent
一起使用,因为 .prevent
将会被忽略,同时浏览器可能会向你展示一个警告。请记住,.passive
会告诉浏览器你不想阻止事件的默认行为。
在监听键盘事件时,我们经常需要检查详细的按键。Vue 允许为 v-on
或者 @
在监听键盘事件时添加按键修饰符:
<!-- 只有在 `key` 是 `Enter` 时调用 `vm.submit()` --><input @keyup.enter="submit" />
你可以直接将 KeyboardEvent.key
暴露的任意有效按键名转换为 kebab-case 来作为修饰符。
<input @keyup.page-down="onPageDown" />
在上述示例中,处理函数只会在 $event.key
等于 'PageDown'
时被调用。
Vue 为最常用的键提供了别名:
.enter
.tab
.delete
(捕获“删除”和“退格”键).esc
.space
.up
.down
.left
.right
可以用如下修饰符来实现仅在按下相应按键时才触发鼠标或键盘事件的监听器。
.ctrl
.alt
.shift
.meta
提示
注意:在 Mac 系统键盘上,meta 对应 command 键 (⌘)。在 Windows 系统键盘 meta 对应 Windows 徽标键 (⊞)。在 Sun 操作系统键盘上,meta 对应实心宝石键 (◆)。在其他特定键盘上,尤其在 MIT 和 Lisp 机器的键盘、以及其后继产品,比如 Knight 键盘、space-cadet 键盘,meta 被标记为“META”。在 Symbolics 键盘上,meta 被标记为“META”或者“Meta”。
例如:
<!-- Alt + Enter --><input @keyup.alt.enter="clear" /><!-- Ctrl + Click --><div @click.ctrl="doSomething">Do something</div>
TIP
请注意修饰键与常规按键不同,在和 keyup
事件一起用时,事件触发时修饰键必须处于按下状态。换句话说,只有在按住 ctrl
的情况下释放其它按键,才能触发 keyup.ctrl
。而单单释放 ctrl
也不会触发事件。
.exact
修饰符.exact
修饰符允许你控制由精确的系统修饰符组合触发的事件。
<!-- 即使 Alt 或 Shift 被一同按下时也会触发 --><button @click.ctrl="onClick">A</button><!-- 有且只有 Ctrl 被按下的时候才触发 --><button @click.ctrl.exact="onCtrlClick">A</button><!-- 没有任何系统修饰符被按下的时候才触发 --><button @click.exact="onClick">A</button>
.left
.right
.middle
这些修饰符会限制处理函数仅响应特定的鼠标按钮。
你可能注意到这种事件监听的方式违背了关注点分离 (separation of concern) 这个长期以来的优良传统。但不必担心,因为所有的 Vue.js 事件处理方法和表达式都严格绑定在当前视图的 ViewModel 上,它不会导致任何维护上的困难。实际上,使用 v-on
或 @
有几个好处:
你可以用 v-model 指令在表单 <input>
、<textarea>
及 <select>
元素上创建双向数据绑定。它会根据控件类型自动选取正确的方法来更新元素。尽管有些神奇,但 v-model
本质上不过是语法糖。它负责监听用户的输入事件以更新数据,并对一些极端场景进行一些特殊处理。
提示
v-model
会忽略所有表单元素的 value
、checked
、selected
attribute 的初始值而总是将当前活动实例的数据作为数据来源。你应该通过 JavaScript 在组件的 data
选项中声明初始值。
v-model
在内部为不同的输入元素使用不同的 property 并抛出不同的事件:
value
property 和 input
事件;checked
property 和 change
事件;value
作为 prop 并将 change
作为事件。提示
对于需要使用输入法 (如中文、日文、韩文等) 的语言,你会发现 v-model
不会在输入法组织文字过程中得到更新。如果你也想处理这个过程,请使用 input
事件。
<input v-model="message" placeholder="edit me" /><p>Message is: {{ message }}</p>
<span>Multiline message is:</span><p style="white-space: pre-line;">{{ message }}</p><br /><textarea v-model="message" placeholder="add multiple lines"></textarea>
在文本区域插值不起作用,应该使用 v-model
来代替。
<!-- bad --><textarea>{{ text }}</textarea><!-- good --><textarea v-model="text"></textarea>
单个复选框,绑定到布尔值:
<input type="checkbox" id="checkbox" v-model="checked" /><label for="checkbox">{{ checked }}</label>
多个复选框,绑定到同一个数组:
<div id="v-model-multiple-checkboxes"> <input type="checkbox" id="jack" value="Jack" v-model="checkedNames" /> <label for="jack">Jack</label> <input type="checkbox" id="john" value="John" v-model="checkedNames" /> <label for="john">John</label> <input type="checkbox" id="mike" value="Mike" v-model="checkedNames" /> <label for="mike">Mike</label> <br /> <span>Checked names: {{ checkedNames }}</span></div>
Vue.createApp({ data() { return { checkedNames: [] } }}).mount('#v-model-multiple-checkboxes')
<div id="v-model-radiobutton"> <input type="radio" id="one" value="One" v-model="picked" /> <label for="one">One</label> <br /> <input type="radio" id="two" value="Two" v-model="picked" /> <label for="two">Two</label> <br /> <span>Picked: {{ picked }}</span></div>
Vue.createApp({ data() { return { picked: '' } }}).mount('#v-model-radiobutton')
单选时:
<div id="v-model-select" class="demo"> <select v-model="selected"> <option disabled value="">Please select one</option> <option>A</option> <option>B</option> <option>C</option> </select> <span>Selected: {{ selected }}</span></div>
Vue.createApp({ data() { return { selected: '' } }}).mount('#v-model-select')
Note
如果 v-model
表达式的初始值未能匹配任何选项,<select>
元素将被渲染为“未选中”状态。在 iOS 中,这会使用户无法选择第一个选项。因为这样的情况下,iOS 不会触发 change
事件。因此,更推荐像上面这样提供一个值为空的禁用选项。
多选时 (绑定到一个数组):
<select v-model="selected" multiple> <option>A</option> <option>B</option> <option>C</option></select><br /><span>Selected: {{ selected }}</span>
用 v-for
渲染的动态选项:
<div id="v-model-select-dynamic" class="demo"> <select v-model="selected"> <option v-for="option in options" :value="option.value"> {{ option.text }} </option> </select> <span>Selected: {{ selected }}</span></div>
Vue.createApp({ data() { return { selected: 'A', options: [ { text: 'One', value: 'A' }, { text: 'Two', value: 'B' }, { text: 'Three', value: 'C' } ] } }}).mount('#v-model-select-dynamic')
对于单选按钮,复选框及选择框的选项,v-model
绑定的值通常是静态字符串 (对于复选框也可以是布尔值):
<!-- 当选中时,`picked` 为字符串 "a" --><input type="radio" v-model="picked" value="a" /><!-- `toggle` 为 true 或 false --><input type="checkbox" v-model="toggle" /><!-- 当选中第一个选项时,`selected` 为字符串 "abc" --><select v-model="selected"> <option value="abc">ABC</option></select>
但是有时我们可能想把值绑定到当前活动实例的一个动态 property 上,这时可以用 v-bind
实现,此外,使用 v-bind
可以将输入值绑定到非字符串。
<input type="checkbox" v-model="toggle" true-value="yes" false-value="no" />
// when checked:vm.toggle === 'yes'// when unchecked:vm.toggle === 'no'
Tip
这里的 true-value
和 false-value
attribute 并不会影响输入控件的 value
attribute,因为浏览器在提交表单时并不会包含未被选中的复选框。如果要确保表单中这两个值中的一个能够被提交,(即“yes”或“no”),请换用单选按钮。
<input type="radio" v-model="pick" v-bind:value="a" />
// 当选中时vm.pick === vm.a
<select v-model="selected"> <!-- 内联对象字面量 --> <option :value="{ number: 123 }">123</option></select>
// 当被选中时typeof vm.selected // => 'object'vm.selected.number // => 123
.lazy
在默认情况下,v-model
在每次 input
事件触发后将输入框的值与数据进行同步 (除了上述输入法组织文字时)。你可以添加 lazy
修饰符,从而转为在 change
事件_之后_进行同步:
<!-- 在“change”时而非“input”时更新 --><input v-model.lazy="msg" />
.number
如果想自动将用户的输入值转为数值类型,可以给 v-model
添加 number
修饰符:
<input v-model.number="age" type="number" />
这通常很有用,因为即使在 type="number"
时,HTML 输入元素的值也总会返回字符串。如果这个值无法被 parseFloat()
解析,则会返回原始的值。
.trim
如果要自动过滤用户输入的首尾空白字符,可以给 v-model
添加 trim
修饰符:
<input v-model.trim="msg" />
v-model
如果你还不熟悉 Vue 的组件,可以暂且跳过这里。
HTML 原生的输入元素类型并不总能满足需求。幸好,Vue 的组件系统允许你创建具有完全自定义行为且可复用的输入组件。这些输入组件甚至可以和 v-model
一起使用!
要了解更多,请参阅组件指南中的自定义输入组件。
这里有一个 Vue 组件的示例:
// 创建一个Vue 应用const app = Vue.createApp({})// 定义一个名为 button-counter 的新全局组件app.component('button-counter', { data() { return { count: 0 } }, template: ` <button @click="count++"> You clicked me {{ count }} times. </button>`})
INFO
在这里演示的是一个简单的示例,但是在典型的 Vue 应用程序中,我们使用单个文件组件而不是字符串模板。你可以在本节找到有关它们的更多信息。
组件是可复用的组件实例,且带有一个名字:在这个例子中是 <button-counter>
。我们可以在一个通过 new Vue 创建的 Vue 根实例中,把这个组件作为自定义元素来使用:
<div id="components-demo"> <button-counter></button-counter></div>
app.mount('#components-demo')
因为组件是可复用的组件实例,所以它们与 new Vue 接收相同的选项,例如 data
、computed
、watch
、methods
以及生命周期钩子等。仅有的例外是像 el
这样根实例特有的选项。
你可以将组件进行任意次数的复用:
<div id="components-demo"> <button-counter></button-counter> <button-counter></button-counter> <button-counter></button-counter></div>
注意当点击按钮时,每个组件都会各自独立维护它的 count
。因为你每用一次组件,就会有一个它的新实例被创建。
通常一个应用会以一棵嵌套的组件树的形式来组织
例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。
为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:全局注册和局部注册。至此,我们的组件都只是通过 component
全局注册的:
const app = Vue.createApp({})app.component('my-component-name', { // ... 选项 ...})
全局注册的组件可以在随后创建的 app
实例模板中使用,也包括根实例组件树中的所有子组件的模板中。
到目前为止,关于组件注册你需要了解的就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把组件注册读完。
早些时候,我们提到了创建一个博文组件的事情。问题是如果你不能向这个组件传递某一篇博文的标题或内容之类的我们想展示的数据的话,它是没有办法使用的。这也正是 prop 的由来。
Prop 是你可以在组件上注册的一些自定义 attribute。当一个值传递给一个 prop attribute 的时候,它就变成了那个组件实例的一个 property。为了给博文组件传递一个标题,我们可以用一个 props 选项将其包含在该组件可接受的 prop
列表中:
const app = Vue.createApp({})app.component('blog-post', { props: ['title'], template: `<h4>{{ title }}</h4>`})app.mount('#blog-post-demo')
一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。在上述模板中,你会发现我们能够在组件实例中访问这个值,就像访问 data
中的值一样。
一个 prop 被注册之后,你就可以像这样把数据作为一个自定义 attribute 传递进来:
<div id="blog-post-demo" class="demo"> <blog-post title="My journey with Vue"></blog-post> <blog-post title="Blogging with Vue"></blog-post> <blog-post title="Why Vue is so fun"></blog-post></div>
然而在一个典型的应用中,你可能在 data
里有一个博文的数组:
const App = { data() { return { posts: [ { id: 1, title: 'My journey with Vue' }, { id: 2, title: 'Blogging with Vue' }, { id: 3, title: 'Why Vue is so fun' } ] } }}const app = Vue.createApp({})app.component('blog-post', { props: ['title'], template: `<h4>{{ title }}</h4>`})app.mount('#blog-posts-demo')
并想要为每篇博文渲染一个组件:
<div id="blog-posts-demo"> <blog-post v-for="post in posts" :key="post.id" :title="post.title" ></blog-post></div>
如上所示,你会发现我们可以使用 v-bind
来动态传递 prop。这在你一开始不清楚要渲染的具体内容,是非常有用的。
到目前为止,关于 prop 你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把 prop 读完。
在我们开发 <blog-post>
组件时,它的一些功能可能要求我们和父级组件进行沟通。例如我们可能会引入一个辅助功能来放大博文的字号,同时让页面的其它部分保持默认的字号。
在其父组件中,我们可以通过添加一个 postFontSize
数据 property 来支持这个功能:
const App = { data() { return { posts: [ /* ... */ ], postFontSize: 1 } }}
它可以在模板中用来控制所有博文的字号:
<div id="blog-posts-events-demo"> <div v-bind:style="{ fontSize: postFontSize + 'em' }"> <blog-post v-for="post in posts" :key="post.id" :title="title"></blog-post> </div></div>
现在我们在每篇博文正文之前添加一个按钮来放大字号:
app.component('blog-post', { props: ['title'], template: ` <div class="blog-post"> <h4>{{ title }}</h4> <button> Enlarge text </button> </div> `})
问题是这个按钮不会做任何事:
<button> Enlarge text</button>
当点击这个按钮时,我们需要告诉父级组件放大所有博文的文本。幸好组件实例提供了一个自定义事件的系统来解决这个问题。父级组件可以像处理 native DOM 事件一样通过 v-on
或 @
监听子组件实例的任意事件:
<blog-post ... @enlarge-text="postFontSize += 0.1"></blog-post>
同时子组件可以通过调用内建的 $emit 方法并传入事件名称来触发一个事件:
<button @click="$emit('enlarge-text')"> Enlarge text</button>
多亏了 @enlarge-text="postFontSize += 0.1"
监听器,父级将接收事件并更新 postFontSize
值。点击此处实现
我们可以在组件的 emits
选项中列出已抛出的事件。
app.component('blog-post', { props: ['title'], emits: ['enlarge-text']})
这将允许你检查组件抛出的所有事件,还可以选择 validate them
有的时候用一个事件来抛出一个特定的值是非常有用的。例如我们可能想让 <blog-post>
组件决定它的文本要放大多少。这时可以使用 $emit
的第二个参数来提供这个值:
<button @click="$emit('enlarge-text', 0.1)"> Enlarge text</button>
然后当在父级组件监听这个事件的时候,我们可以通过 $event
访问到被抛出的这个值:
<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>
或者,如果这个事件处理函数是一个方法:
<blog-post ... @enlarge-text="onEnlargeText"></blog-post>
那么这个值将会作为第一个参数传入这个方法:
methods: { onEnlargeText(enlargeAmount) { this.postFontSize += enlargeAmount }}
自定义事件也可以用于创建支持 v-model
的自定义输入组件。记住:
<input v-model="searchText" />
等价于:
<input :value="searchText" @input="searchText = $event.target.value" />
当用在组件上时,v-model
则会这样:
<custom-input :model-value="searchText" @update:model-value="searchText = $event"></custom-input>
WARNING
请注意,我们在这里使用的是 model value
,因为我们使用的是 DOM 模板中的 kebab-case。你可以在 DOM Template Parsing Caveats 部分找到关于 kebab cased 和 camelCased 属性的详细说明
为了让它正常工作,这个组件内的 <input>
必须:
value
attribute 绑定到一个名叫 modelValue
的 prop 上input
事件被触发时,将新的值通过自定义的 update:modelValue
事件抛出写成代码之后是这样的:
app.component('custom-input', { props: ['modelValue'], template: ` <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" > `})
现在 v-model
就应该可以在这个组件上完美地工作起来了:
<custom-input v-model="searchText"></custom-input>
在自定义组件中创建 v-model
功能的另一种方法是使用 computed
property 的功能来定义 getter 和 setter。
在下面的示例中,我们使用计算属性重构 <custom-input>
组件。
请记住,get
方法应返回 modelValue
property,或用于绑定的任何 property,set
方法应为该 property 触发相应的 $emit
。
app.component('custom-input', { props: ['modelValue'], template: ` <input v-model="value"> `, computed: { value: { get() { return this.modelValue }, set(value) { this.$emit('update:modelValue', value) } } }})
现在你只需要了解自定义组件事件,但一旦你读完本页并对其内容还觉得不错,我们建议你稍后再阅读有关自定义事件
和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样:
<alert-box> Something bad happened.</alert-box>
可能会渲染出这样的东西:点击此处实现
幸好,Vue 自定义的 <slot>
元素让这变得非常简单:
app.component('alert-box', { template: ` <div class="demo-alert-box"> <strong>Error!</strong> <slot></slot> </div> `})
如你所见,我们只要在需要的地方加入插槽就行了——就这么简单!
到目前为止,关于插槽你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把插槽读完。
有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里:点击此处实现
上述内容可以通过 Vue 的 <component>
元素加一个特殊的 is
attribute 来实现:
<!-- 组件会在 `currentTabComponent` 改变时改变 --><component :is="currentTabComponent"></component>
在上述示例中,currentTabComponent
可以包括
你可以在这里查阅并体验完整的代码,或在这个版本了解绑定组件选项对象,而不是已注册组件名的示例。
请留意,这个 attribute 可以用于常规 HTML 元素,但这些元素将被视为组件,这意味着所有的 attribute 都会作为 DOM attribute 被绑定。对于像 value
这样的 property,若想让其如预期般工作,你需要使用 .prop 修饰器。
到目前为止,关于动态组件你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把动态 & 异步组件读完。
有些 HTML 元素,诸如 <ul>
、<ol>
、<table>
和 <select>
,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li>
、<tr>
和 <option>
,只能出现在其它某些特定的元素内部。
这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:
<table> <blog-post-row></blog-post-row></table>
这个自定义组件 <blog-post-row>
会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 v-is
attribute 给了我们一个变通的办法:
<table> <tr v-is="'blog-post-row'"></tr></table>
WARNING
v-is
值应为 JavaScript 字符串文本:
<!-- 错误的,这样不会渲染任何东西 --><tr v-is="blog-post-row"></tr><!-- 正确的 --><tr v-is="'blog-post-row'"></tr>
另外,HTML 属性名不区分大小写,因此浏览器将把所有大写字符解释为小写。这意味着当你在 DOM 模板中使用时,驼峰 prop 名称和 event 处理器参数需要使用它们的 kebab-cased (横线字符分隔) 等效值:
// 在JavaScript中的驼峰app.component('blog-post', { props: ['postTitle'], template: ` <h3>{{ postTitle }}</h3> `})
<!-- 在HTML则是横线字符分割 --><blog-post post-title="hello!"></blog-post>
需要注意的是如果我们从以下来源使用模板的话,这条限制是*不存在*的:
template: '...'
)<script type="text/x-template">
到这里,你需要了解的解析 DOM 模板时的注意事项——实际上也是 Vue 的全部必要内容,大概就是这些了。恭喜你!接下来还有很多东西要去学习,不过首先,我们推荐你先休息一下,试用一下 Vue,自己随意做些好玩的东西。
如果你感觉已经掌握了这些知识,我们推荐你再回来把完整的组件&异步组件指南,包括侧边栏中组件深入章节的所有页面读完。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
在注册一个组件的时候,我们始终需要给它一个名字。比如在全局注册的时候我们已经看到了:
const app = Vue.createApp({...})app.component('my-component-name', { /* ... */})
该组件名就是 app.component
的第一个参数,在上面的例子中,组件的名称是“my-component-name”。
你给予组件的名字可能依赖于你打算拿它来做什么。当直接在 DOM 中使用一个组件 (而不是在字符串模板或单文件组件) 的时候,我们强烈推荐遵循 W3C 规范中的自定义组件名 (字母全小写且必须包含一个连字符)。这会帮助你避免和当前以及未来的 HTML 元素相冲突。
这样会帮助你避免与当前以及未来的 HTML 元素发生冲突。
你可以在风格指南中查阅到关于组件名的其它建议。
在字符串模板或单个文件组件中定义组件时,定义组件名的方式有两种:
app.component('my-component-name', { /* ... */})
当使用 kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,例如 <my-component-name>
。
app.component('MyComponentName', { /* ... */})
当使用 PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说 <my-component-name>
和 <MyComponentName>
都是可接受的。注意,尽管如此,直接在 DOM (即非字符串的模板) 中使用时只有 kebab-case 是有效的。
到目前为止,我们只用过 app.component
来创建组件:
Vue.createApp({...}).component('my-component-name', { // ... 选项 ...})
这些组件是全局注册的。也就是说它们在注册之后可以用在任何新创建的组件实例的模板中。比如:
const app = Vue.createApp({})app.component('component-a', { /* ... */})app.component('component-b', { /* ... */})app.component('component-c', { /* ... */})app.mount('#app')
<div id="app"> <component-a></component-a> <component-b></component-b> <component-c></component-c></div>
在所有子组件中也是如此,也就是说这三个组件在各自内部也都可以相互使用。
全局注册往往是不够理想的。比如,如果你使用一个像 webpack 这样的构建系统,全局注册所有的组件意味着即便你已经不再使用一个组件了,它仍然会被包含在你最终的构建结果中。这造成了用户下载的 JavaScript 的无谓的增加。
在这些情况下,你可以通过一个普通的 JavaScript 对象来定义组件:
const ComponentA = { /* ... */}const ComponentB = { /* ... */}const ComponentC = { /* ... */}
然后在 components
选项中定义你想要使用的组件:
const app = Vue.createApp({ components: { 'component-a': ComponentA, 'component-b': ComponentB }})
对于 components
对象中的每个 property 来说,其 property 名就是自定义元素的名字,其 property 值就是这个组件的选项对象。
注意局部注册的组件在其子组件中不可用。例如,如果你希望 ComponentA
在 ComponentB
中可用,则你需要这样写:
const ComponentA = { /* ... */}const ComponentB = { components: { 'component-a': ComponentA } // ...}
或者如果你通过 Babel 和 webpack 使用 ES2015 模块,那么代码看起来更像:
import ComponentA from './ComponentA.vue'export default { components: { ComponentA } // ...}
注意在 ES2015+ 中,在对象中放一个类似 ComponentA
的变量名其实是 ComponentA
:ComponentA
的缩写,即这个变量名同时是:
如果你没有通过 import
/require
使用一个模块系统,也许可以暂且跳过这个章节。如果你使用了,那么我们会为你提供一些特殊的使用说明和注意事项。
如果你还在阅读,说明你使用了诸如 Babel 和 webpack 的模块系统。在这些情况下,我们推荐创建一个 components
目录,并将每个组件放置在其各自的文件中。
然后你需要在局部注册之前导入每个你想使用的组件。例如,在一个假设的 ComponentB.js
或 ComponentB.vue
文件中:
import ComponentA from './ComponentA'import ComponentC from './ComponentC'export default { components: { ComponentA, ComponentC } // ...}
现在 ComponentA
和 ComponentC
都可以在 ComponentB
的模板中使用了。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
到这里,我们只看到了以字符串数组形式列出的 prop:
props: ['title', 'likes', 'isPublished', 'commentIds', 'author']
但是,通常你希望每个 prop 都有指定的值类型。这时,你可以以对象形式列出 prop,这些 property 的名称和值分别是 prop 各自的名称和类型:
props: { title: String, likes: Number, isPublished: Boolean, commentIds: Array, author: Object, callback: Function, contactsPromise: Promise // 或任何其他构造函数}
这不仅为你的组件提供了文档,还会在它们遇到错误的类型时从浏览器的 JavaScript 控制台提示用户。你会在这个页面接下来的部分看到类型检查和其它 prop 验证。
这样,你已经知道了可以像这样给 prop 传入一个静态的值:
<blog-post title="My journey with Vue"></blog-post>
你也知道 prop 可以通过 v-bind
或简写 :
动态赋值,例如:
<!-- 动态赋予一个变量的值 --><blog-post :title="post.title"></blog-post><!-- 动态赋予一个复杂表达式的值 --><blog-post :title="post.title + ' by ' + post.author.name"></blog-post>
在上述两个示例中,我们传入的值都是字符串类型的,但实际上任何类型的值都可以传给一个 prop。
<!-- 即便 `42` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue --><!-- 这是一个 JavaScript 表达式而不是一个字符串。 --><blog-post :likes="42"></blog-post><!-- 用一个变量进行动态赋值。--><blog-post :likes="post.likes"></blog-post>
<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。 --><blog-post is-published></blog-post><!-- 即便 `false` 是静态的,我们仍然需要 `v-bind` 来告诉 Vue --><!-- 这是一个 JavaScript 表达式而不是一个字符串。 --><blog-post :is-published="false"></blog-post><!-- 用一个变量进行动态赋值。 --><blog-post :is-published="post.isPublished"></blog-post>
<!-- 即便数组是静态的,我们仍然需要 `v-bind` 来告诉 Vue --><!-- 这是一个 JavaScript 表达式而不是一个字符串。 --><blog-post :comment-ids="[234, 266, 273]"></blog-post><!-- 用一个变量进行动态赋值。 --><blog-post :comment-ids="post.commentIds"></blog-post>
<!-- 即便对象是静态的,我们仍然需要 `v-bind` 来告诉 Vue --><!-- 这是一个 JavaScript 表达式而不是一个字符串。 --><blog-post :author="{ name: 'Veronica', company: 'Veridian Dynamics' }"></blog-post><!-- 用一个变量进行动态赋值。 --><blog-post :author="post.author"></blog-post>
如果你想要将一个对象的所有 property 都作为 prop 传入,你可以使用不带参数的 v-bind
(取代 v-bind
:prop-name
)。例如,对于一个给定的对象 post
:
post: { id: 1, title: 'My Journey with Vue'}
下面的模板:
<blog-post v-bind="post"></blog-post>
等价于:
<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。
另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。
这里有两种常见的试图变更一个 prop 的情形:
props: ['initialCounter'],data() { return { counter: this.initialCounter }}
props: ['size'],computed: { normalizedSize: function () { return this.size.trim().toLowerCase() }}
提示
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。
我们可以为组件的 prop 指定验证要求,例如你知道的这些类型。如果有一个需求没有被满足,则 Vue 会在浏览器控制台中警告你。这在开发一个会被别人用到的组件时尤其有帮助。
为了定制 prop 的验证方式,你可以为 props
中的值提供一个带有验证需求的对象,而不是一个字符串数组。例如:
app.component('my-component', { props: { // 基础的类型检查 (`null` 和 `undefined` 会通过任何类型验证) propA: Number, // 多个可能的类型 propB: [String, Number], // 必填的字符串 propC: { type: String, required: true }, // 带有默认值的数字 propD: { type: Number, default: 100 }, // 带有默认值的对象 propE: { type: Object, // 对象或数组默认值必须从一个工厂函数获取 default: function() { return { message: 'hello' } } }, // 自定义验证函数 propF: { validator: function(value) { // 这个值必须匹配下列字符串中的一个 return ['success', 'warning', 'danger'].indexOf(value) !== -1 } }, // 具有默认值的函数 propG: { type: Function, // 与对象或数组默认值不同,这不是一个工厂函数 —— 这是一个用作默认值的函数 default: function() { return 'Default function' } } }})
当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。
提示
注意那些 prop 会在一个组件实例创建之前进行验证,所以实例的 property (如 data
、computed
等) 在 default
或 validator
函数中是不可用的。
type
可以是下列原生构造函数中的一个:
此外,type
还可以是一个自定义的构造函数,并且通过 instanceof
来进行检查确认。例如,给定下列现成的构造函数:
function Person(firstName, lastName) { this.firstName = firstName this.lastName = lastName}
你可以使用:
app.component('blog-post', { props: { author: Person }})
用于验证 author
prop 的值是否是通过 new Person
创建的。
HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。这意味着当你使用 DOM 中的模板时,camelCase (驼峰命名法) 的 prop 名需要使用其等价的 kebab-case (短横线分隔命名) 命名:
const app = Vue.createApp({})app.component('blog-post', { // camelCase in JavaScript props: ['postTitle'], template: '<h3>{{ postTitle }}</h3>'})
<!-- kebab-case in HTML --><blog-post post-title="hello!"></blog-post>
重申一次,如果你使用字符串模板,那么这个限制就不存在了。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
一个非 prop 的 attribute 是指传向一个组件,但是该组件并没有相应 props 或 emits 定义的 attribute。常见的示例包括 class
、style
和 id
属性。
当组件返回单个根节点时,非 prop attribute 将自动添加到根节点的 attribute 中。例如,在 <date-picker>
组件的实例中:
app.component('date-picker', { template: ` <div class="date-picker"> <input type="datetime" /> </div> `})
如果我们需要通过 data status
property 定义 <date-picker>
组件的状态,它将应用于根节点 (即 div.date-picker
)。
<!-- 具有非prop attribute的Date-picker组件--><date-picker data-status="activated"></date-picker><!-- 渲染 date-picker 组件 --><div class="date-picker" data-status="activated"> <input type="datetime" /></div>
同样的规则适用于事件监听器:
<date-picker @change="submitChange"></date-picker>
app.component('date-picker', { created() { console.log(this.$attrs) // { onChange: () => {} } }})
当有一个 HTML 元素将 change
事件作为 date-picker
的根元素时,这可能会有帮助。
app.component('date-picker', { template: ` <select> <option value="1">Yesterday</option> <option value="2">Today</option> <option value="3">Tomorrow</option> </select> `})
在这种情况下,change
事件监听器从父组件传递到子组件,它将在原生 select
的 change
事件上触发。我们不需要显式地从 date-picker
发出事件:
<div id="date-picker" class="demo"> <date-picker @change="showChange"></date-picker></div>
const app = Vue.createApp({ methods: { showChange(event) { console.log(event.target.value) // 将记录所选选项的值 } }})
如果你不希望组件的根元素继承 attribute,你可以在组件的选项中设置 inheritAttrs: false
。例如:
禁用 attribute 继承的常见情况是需要将 attribute 应用于根节点之外的其他元素。
通过将 inheritAttrs
选项设置为 false
,你可以访问组件的 $attrs
property,该 property 包括组件 props
和 emits
property 中未包含的所有属性 (例如,class
、style
、v-on
监听器等)。
使用上一节中的 date-picker 组件示例,如果需要将所有非 prop attribute 应用于 input
元素而不是根 div
元素,则可以使用 v-bind
缩写来完成。
app.component('date-picker', { inheritAttrs: false, template: ` <div class="date-picker"> <input type="datetime" v-bind="$attrs" /> </div> `})
有了这个新配置,data status
attribute 将应用于 input
元素!
<!-- Date-picker 组件 使用非 prop attribute --><date-picker data-status="activated"></date-picker><!-- 渲染 date-picker 组件 --><div class="date-picker"> <input type="datetime" data-status="activated" /></div>
与单个根节点组件不同,具有多个根节点的组件不具有自动 attribute 回退行为。如果未显式绑定 $attrs
,将发出运行时警告。
<custom-layout id="custom-layout" @click="changeValue"></custom-layout>
// 这将发出警告app.component('custom-layout', { template: ` <header>...</header> <main>...</main> <footer>...</footer> `})// 没有警告,$attrs被传递到<main>元素app.component('custom-layout', { template: ` <header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer> `})
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
不同于组件和 prop,事件名不存在任何自动化的大小写转换。而是触发的事件名需要完全匹配监听这个事件所用的名称。举个例子,如果触发一个 camelCase 名字的事件:
this.$emit('myEvent')
则监听这个名字的 kebab-case 版本是不会有任何效果的:
<!-- 没有效果 --><my-component @my-event="doSomething"></my-component>
不同于组件和 prop,事件名不会被用作一个 JavaScript 变量名或 property 名,所以就没有理由使用 camelCase 或 PascalCase 了。并且 v-on
事件监听器在 DOM 模板中会被自动转换为全小写 (因为 HTML 是大小写不敏感的),所以 @myEvent
将会变成 @myevent
——导致 myEvent
不可能被监听到。
因此,我们推荐你始终使用 kebab-case 的事件名。
在 Vue School 上观看关于定义自定义事件的免费视频。
可以通过 emits
选项在组件上定义已发出的事件。
app.component('custom-form', { emits: ['in-focus', 'submit']})
当在 emits
选项中定义了原生事件 (如 click
) 时,将使用组件中的事件替代原生事件侦听器。
TIP
建议定义所有发出的事件,以便更好地记录组件应该如何工作。
与 prop 类型验证类似,如果使用对象语法而不是数组语法定义发出的事件,则可以验证它。
要添加验证,将为事件分配一个函数,该函数接收传递给 $emit
调用的参数,并返回一个布尔值以指示事件是否有效。
app.component('custom-form', { emits: { // 没有验证 click: null, // 验证submit 事件 submit: ({ email, password }) => { if (email && password) { return true } else { console.warn('Invalid submit event payload!') return false } } }, methods: { submitForm() { this.$emit('submit', { email, password }) } }})
v-model
参数默认情况下,组件上的 v-model
使用 modelValue
作为 prop 和 update:modelValue
作为事件。我们可以通过向 v-model
传递参数来修改这些名称:
<my-component v-model:foo="bar"></my-component>
在本例中,子组件将需要一个 foo
prop 并发出 update:foo
要同步的事件:
const app = Vue.createApp({})app.component('my-component', { props: { foo: String }, template: ` <input type="text" :value="foo" @input="$emit('update:foo', $event.target.value)"> `})
<my-component v-model:foo="bar"></my-component>
v-model
绑定通过利用以特定 prop 和事件为目标的能力,正如我们之前在 v-model
参数中所学的那样,我们现在可以在单个组件实例上创建多个 v-model 绑定。
每个 v-model 将同步到不同的 prop,而不需要在组件中添加额外的选项:
<user-name v-model:first-name="firstName" v-model:last-name="lastName"></user-name>
const app = Vue.createApp({})app.component('user-name', { props: { firstName: String, lastName: String }, template: ` <input type="text" :value="firstName" @input="$emit('update:firstName', $event.target.value)"> <input type="text" :value="lastName" @input="$emit('update:lastName', $event.target.value)"> `})
v-model
修饰符在 2.x 中,我们对组件 v-model
上的 .trim
等修饰符提供了硬编码支持。但是,如果组件可以支持自定义修饰符,则会更有用。在 3.x 中,添加到组件 v-model
的修饰符将通过 modelModifiers
prop 提供给组件:
当我们学习表单输入绑定时,我们看到 v-model
有内置修饰符——.trim
、.number
和 .lazy
。但是,在某些情况下,你可能还需要添加自己的自定义修饰符。
让我们创建一个示例自定义修饰符 capitalize
,它将 v-model
绑定提供的字符串的第一个字母大写。
添加到组件 v-model
的修饰符将通过 modelModifiers
prop 提供给组件。在下面的示例中,我们创建了一个组件,其中包含默认为空对象的 modelModifiers
prop。
请注意,当组件的 created
生命周期钩子触发时,modelModifiers
prop 包含 capitalize
,其值为 true
——因为它被设置在 v-model
绑定 v-model.capitalize="bar"
。
<my-component v-model.capitalize="bar"></my-component>
app.component('my-component', { props: { modelValue: String, modelModifiers: { default: () => ({}) } }, template: ` <input type="text" :value="modelValue" @input="$emit('update:modelValue', $event.target.value)"> `, created() { console.log(this.modelModifiers) // { capitalize: true } }})
现在我们已经设置了 prop,我们可以检查 modelModifiers
对象键并编写一个处理器来更改发出的值。在下面的代码中,每当 <input/>
元素触发 input
事件时,我们都将字符串大写。
<div id="app"> <my-component v-model.capitalize="myText"></my-component> {{ myText }}</div>
const app = Vue.createApp({ data() { return { myText: '' } }})app.component('my-component', { props: { modelValue: String, modelModifiers: { default: () => ({}) } }, methods: { emitValue(e) { let value = e.target.value if (this.modelModifiers.capitalize) { value = value.charAt(0).toUpperCase() + value.slice(1) } this.$emit('update:modelValue', value) } }, template: `<input type="text" :value="modelValue" @input="emitValue">`})app.mount('#app')
对于带参数的 v-model
绑定,生成的 prop 名称将为 arg + "Modifiers"
:
<my-component v-model:foo.capitalize="bar"></my-component>
app.component('my-component', { props: ['foo', 'fooModifiers'], template: ` <input type="text" :value="foo" @input="$emit('update:foo', $event.target.value)"> `, created() { console.log(this.fooModifiers) // { capitalize: true } }})
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
Vue 实现了一套内容分发的 API,这套 API 的设计灵感源自 Web Components 规范草案,将 <slot>
元素作为承载分发内容的出口。
它允许你像这样合成组件:
<todo-button> Add todo</todo-button>
然后在 <todo-button>
的模板中,你可能有:
<!-- todo-button 组件模板 --><button class="btn-primary"> <slot></slot></button>
当组件渲染的时候,将会被替换为“Add Todo”。
<!-- 渲染 HTML --><button class="btn-primary"> Add todo</button>
不过,字符串只是开始!插槽还可以包含任何模板代码,包括 HTML:
<todo-button> <!-- 添加一个Font Awesome 图标 --> <i class="fas fa-plus"></i> Add todo</todo-button>
或其他组件
<todo-button> <!-- 添加一个图标的组件 --> <font-awesome-icon name="plus"></font-awesome-icon> Add todo</todo-button>
如果 <todo-button>
的 template 中没有包含一个 <slot>
元素,则该组件起始标签和结束标签之间的任何内容都会被抛弃
<!-- todo-button 组件模板 --><button class="btn-primary"> Create a new item</button>
<todo-button> <!-- 以下文本不会渲染 --> Add todo</todo-button>
当你想在一个插槽中使用数据时,例如:
<todo-button> Delete a {{ item.name }}</todo-button>
该插槽可以访问与模板其余部分相同的实例 property (即相同的“作用域”)。
插槽不能访问 <todo-button>
的作用域。例如,尝试访问 action
将不起作用:
<todo-button action="delete"> Clicking here will {{ action }} an item <!-- `action` 未被定义,因为它的内容是传递*到* <todo-button>,而不是*在* <todo-button>里定义的。 --></todo-button>
作为一条规则,请记住:
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
有时为一个插槽设置具体的后备 (也就是默认的) 内容是很有用的,它只会在没有提供内容的时候被渲染。例如在一个 <submit-button>
组件中:
<button type="submit"> <slot></slot></button>
我们可能希望这个 <button>
内绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为后备内容,我们可以将它放在 <slot>
标签内:
<button type="submit"> <slot>Submit</slot></button>
现在当我在一个父级组件中使用 <submit-button
> 并且不提供任何插槽内容时:
<submit-button></submit-button>
后备内容“Submit”将会被渲染:
<button type="submit"> Submit</button>
但是如果我们提供内容:
<submit-button> Save</submit-button>
则这个提供的内容将会被渲染从而取代后备内容:
<button type="submit"> Save</button>
有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout>
组件:
<div class="container"> <header> <!-- 我们希望把页头放这里 --> </header> <main> <!-- 我们希望把主要内容放这里 --> </main> <footer> <!-- 我们希望把页脚放这里 --> </footer></div>
对于这样的情况,<slot>
元素有一个特殊的 attribute:name
。这个 attribute 可以用来定义额外的插槽:
<div class="container"> <header> <slot name="header"></slot> </header> <main> <slot></slot> </main> <footer> <slot name="footer"></slot> </footer></div>
一个不带 name
的 <slot>
出口会带有隐含的名字“default”。
在向具名插槽提供内容的时候,我们可以在一个 <template>
元素上使用 v-slot
指令,并以 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 v-slot:footer> <p>Here's some contact info</p> </template></base-layout>
现在 <template>
元素中的所有内容都将会被传入相应的插槽。
渲染的 HTML 将会是:
<div class="container"> <header> <h1>Here might be a page title</h1> </header> <main> <p>A paragraph for the main content.</p> <p>And another one.</p> </main> <footer> <p>Here's some contact info</p> </footer></div>
注意,v-slot
只能添加在 <template>
上 (只有一种例外情况)
有时让插槽内容能够访问子组件中才有的数据是很有用的。当一个组件被用来渲染一个项目数组时,这是一个常见的情况,我们希望能够自定义每个项目的渲染方式。
例如,我们有一个组件,包含 todo-items 的列表。
app.component('todo-list', { data() { return { items: ['Feed a cat', 'Buy milk'] } }, template: ` <ul> <li v-for="(item, index) in items"> {{ item }} </li> </ul> `})
我们可能需要替换插槽以在父组件上自定义它:
<todo-list> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
但是,这是行不通的,因为只有 <todo-list>
组件可以访问 item
,我们将从其父组件提供槽内容。
要使 item
可用于父级提供的 slot 内容,我们可以添加一个 <slot>
元素并将其绑定为属性:
<ul> <li v-for="( item, index ) in items"> <slot :item="item"></slot> </li></ul>
绑定在 <slot
> 元素上的 attribute 被称为插槽 prop。现在在父级作用域中,我们可以使用带值的 v-slot
来定义我们提供的插槽 prop 的名字:
<todo-list> <template v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </template></todo-list>
在这个例子中,我们选择将包含所有插槽 prop 的对象命名为 slotProps
,但你也可以使用任意你喜欢的名字。
在上述情况下,当被提供的内容只有默认插槽时,组件的标签才可以被当作插槽的模板来使用。这样我们就可以把 v-slot
直接用在组件上:
<todo-list v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span></todo-list>
这种写法还可以更简单。就像假定未指明的内容对应默认插槽一样,不带参数的 v-slot
被假定对应默认插槽:
<todo-list v-slot="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span></todo-list>
注意默认插槽的缩写语法不能和具名插槽混用,因为它会导致作用域不明确:
<!-- 无效,会导致警告 --><todo-list v-slot="slotProps"> <todo-list v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </todo-list> <template v-slot:other="otherSlotProps"> slotProps is NOT available here </template></todo-list>
只要出现多个插槽,请始终为所有的插槽使用完整的基于 <template>
的语法:
<todo-list> <template v-slot:default="slotProps"> <i class="fas fa-check"></i> <span class="green">{{ slotProps.item }}</span> </template> <template v-slot:other="otherSlotProps"> ... </template></todo-list>
作用域插槽的内部工作原理是将你的插槽内容包括在一个传入单个参数的函数里:
function (slotProps) { // ... 插槽内容 ...}
这意味着 v-slot
的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。你也可以使用 ES2015 解构来传入具体的插槽 prop,如下:
<todo-list v-slot="{ item }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。它同样开启了 prop 重命名等其它可能,例如将 item
重命名为 todo
:
<todo-list v-slot="{ item: todo }"> <i class="fas fa-check"></i> <span class="green">{{ todo }}</span></todo-list>
你甚至可以定义后备内容,用于插槽 prop 是 undefined 的情形:
<todo-list v-slot="{ item = 'Placeholder' }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
动态指令参数也可以用在 v-slot
上,来定义动态的插槽名:
<base-layout> <template v-slot:[dynamicSlotName]> ... </template></base-layout>
跟 v-on
和 v-bind
一样,v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符 #
。例如 v-slot:header
可以被重写为 #header
:
<base-layout> <template #header> <h1>Here might be a page title</h1> </template> <template #default> <p>A paragraph for the main content.</p> <p>And another one.</p> </template> <template #footer> <p>Here's some contact info</p> </template></base-layout>
然而,和其它指令一样,该缩写只在其有参数的时候才可用。这意味着以下语法是无效的:
<!-- This will trigger a warning --><todo-list #="{ item }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
如果你希望使用缩写的话,你必须始终以明确插槽名取而代之:
<todo-list #default="{ item }"> <i class="fas fa-check"></i> <span class="green">{{ item }}</span></todo-list>
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
通常,当我们需要将数据从父组件传递到子组件时,我们使用 props。想象一下这样的结构:你有一些深嵌套的组件,而你只需要来自深嵌套子组件中父组件的某些内容。在这种情况下,你仍然需要将 prop 传递到整个组件链中,这可能会很烦人。
对于这种情况,我们可以使用 provide
和 inject
对。父组件可以作为其所有子组件的依赖项提供程序,而不管组件层次结构有多深。这个特性有两个部分:父组件有一个 provide
选项来提供数据,子组件有一个 inject
选项来开始使用这个数据。
例如,如果我们有这样的层次结构:
Root└─ TodoList ├─ TodoItem └─ TodoListFooter ├─ ClearTodosButton └─ TodoListStatistics
如果要将 todo-items 的长度直接传递给 TodoListStatistics
,我们将把这个属性向下传递到层次结构:TodoList
-> TodoListFooter
-> TodoListStatistics
。通过 provide/inject 方法,我们可以直接执行以下操作:
const app = Vue.createApp({})app.component('todo-list', { data() { return { todos: ['Feed a cat', 'Buy tickets'] } }, provide: { user: 'John Doe' }, template: ` <div> {{ todos.length }} <!-- 模板的其余部分 --> </div> `})app.component('todo-list-statistics', { inject: ['user'], created() { console.log(`Injected property: ${this.user}`) // > 注入 property: John Doe }})
但是,如果我们尝试在此处提供一些组件实例 property,则这将不起作用:
app.component('todo-list', { data() { return { todos: ['Feed a cat', 'Buy tickets'] } }, provide: { todoLength: this.todos.length // 将会导致错误 'Cannot read property 'length' of undefined` }, template: ` ... `})
要访问组件实例 property,我们需要将 provide
转换为返回对象的函数
app.component('todo-list', { data() { return { todos: ['Feed a cat', 'Buy tickets'] } }, provide() { return { todoLength: this.todos.length } }, template: ` ... `})
这使我们能够更安全地继续开发该组件,而不必担心可能会更改/删除子组件所依赖的某些内容。这些组件之间的接口仍然是明确定义的,就像 prop 一样。
实际上,你可以将依赖注入看作是“long range props”,除了:
inject
property 来自哪里在上面的例子中,如果我们更改了 todos
的列表,这个更改将不会反映在注入的 todoLength
property 中。这是因为默认情况下,provide/inject
绑定不是被动绑定。我们可以通过将 ref
property 或 reactive
对象传递给 provide
来更改此行为。在我们的例子中,如果我们想对祖先组件中的更改做出反应,我们需要为我们提供的 todoLength
分配一个组合式 API computed
property:
app.component('todo-list', { // ... provide() { return { todoLength: Vue.computed(() => this.todos.length) } }})
在这种情况下,对 todos.length
将正确反映在组件中,其中“todoLength”被注入。在组合式 API 部分中阅读关于 reactive
provide/inject 的更多信息。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
keep-alive
我们之前曾经在一个多标签的界面中使用 is
attribute 来切换不同的组件:
<component :is="currentTabComponent"></component>
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复重渲染导致的性能问题。例如我们来展开说一说这个多标签界面:
你会注意到,如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent
实例。
重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 <keep-alive>
元素将其动态组件包裹起来。
<!-- 失活的组件将会被缓存!--><keep-alive> <component :is="currentTabComponent"></component></keep-alive>
来看看修改后的结果:
现在这个 Posts 标签保持了它的状态 (被选中的文章) 甚至当它未被渲染时也是如此。你可以在这个示例查阅到完整的代码。
你可以在 API 参考文档查阅更多关于 <keep-alive>
的细节。
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了简化,Vue 有一个 defineAsyncComponent
方法:
const app = Vue.createApp({})const AsyncComp = Vue.defineAsyncComponent( () => new Promise((resolve, reject) => { resolve({ template: '<div>I am async!</div>' }) }))app.component('async-example', AsyncComp)
如你所见,此方法接受返回 Promise
的工厂函数。从服务器检索组件定义后,应调用 Promise 的 resolve
回调。你也可以调用 reject(reason)
,以指示加载失败。
你也可以在工厂函数中返回一个 Promise
,所以把 webpack 2 和 ES2015 语法加在一起,我们可以这样使用动态导入:
import { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent(() => import('./components/AsyncComponent.vue'))app.component('async-component', AsyncComp)
当在本地注册组件时,你也可以使用 defineAsyncComponent
import { createApp, defineAsyncComponent } from 'vue'createApp({ // ... components: { AsyncComponent: defineAsyncComponent(() => import('./components/AsyncComponent.vue') ) }})
异步组件在默认情况下是可挂起的。这意味着如果它在父链中有一个 <Suspense>
,它将被视为该 <Suspense>
的异步依赖。在这种情况下,加载状态将由 <Suspense>
控制,组件自身的加载、错误、延迟和超时选项将被忽略。
异步组件可以选择退出 Suspense
控制,并通过在其选项中指定 suspensable:false
,让组件始终控制自己的加载状态。
你可以在中查看可用选项的列表 API 参考
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
尽管存在 prop 和事件,但有时你可能仍然需要直接访问 JavaScript 中的子组件。为此,可以使用 ref
attribute 为子组件或 HTML 元素指定引用 ID。例如:
<input ref="input" />
例如,你希望以编程的方式 focus 这个 input 在组件上挂载,这可能有用
const app = Vue.createApp({})app.component('base-input', { template: ` <input ref="input" /> `, methods: { focusInput() { this.$refs.input.focus() } }, mounted() { this.focusInput() }})
此外,还可以向组件本身添加另一个 ref
,并使用它从父组件触发 focusInput
事件:
<base-input ref="usernameInput"></base-input>
this.$refs.usernameInput.focusInput()
当 ref
与 v-for
一起使用时,你得到的 ref
将是一个数组,其中包含镜像数据源的子组件。
WARNING
$refs
只会在组件渲染完成之后生效。这仅作为一个用于直接操作子元素的“逃生舱”——你应该避免在模板或计算属性中访问 $refs
。
该页面假设你已经阅读过了组件基础。如果你还对组件不太了解,推荐你先阅读它。
提示
这里记录的都是和处理边界情况有关的功能,即一些需要对 Vue 的规则做一些小调整的特殊情况。不过注意这些功能都是有劣势或危险的场景的。我们会在每个案例中注明,所以当你使用每个功能的时候请稍加留意。
得益于其响应性系统,Vue 总是知道何时更新 (如果你使用正确的话)。但是,在某些边缘情况下,你可能希望强制更新,尽管事实上没有任何响应式数据发生更改。还有一些情况下,你可能希望防止不必要的更新。
如果你发现自己需要在 Vue 中强制更新,在 99.99%的情况下,你在某个地方犯了错误。例如,你可能依赖于 Vue 响应性系统未跟踪的状态,例如,在组件创建之后添加了 data
属性。
但是,如果你已经排除了上述情况,并且发现自己处于这种非常罕见的情况下,必须手动强制更新,那么你可以使用 $forceUpdate
。
v-once
在 Vue 中渲染纯 HTML 元素的速度非常快,但有时你可能有一个包含很多静态内容的组件。在这些情况下,可以通过向根元素添加 v-once
指令来确保只对其求值一次,然后进行缓存,如下所示:
app.component('terms-of-service', { template: ` <div v-once> <h1>Terms of Service</h1> ... a lot of static content ... </div> `})
TIP
再次提醒,不要过度使用这种模式。虽然在极少数情况下需要渲染大量静态内容时很方便,但除非你注意到渲染速度——慢,否则就没有必要这样做—另外,这可能会在以后引起很多混乱。例如,假设另一个开发人员不熟悉 v-once
或者只是在模板中遗漏了它。他们可能会花上几个小时来弄清楚为什么模板没有正确更新。
Vue 提供了一些抽象概念,可以帮助处理过渡和动画,特别是在响应某些变化时。这些抽象的概念包括:
<transition>
组件来钩住组件中进入和离开 DOM。<transition-group>
组件,通过 FLIP 技术来提高性能。watchers
来处理应用中不同状态的过渡。我们将在本指南接下来的三个部分中介绍所有这些以及更多内容。然而,除了提供这些有用的 API 之外,值得一提的是,我们前面介绍的 class 和 style 声明也可以应用于动画和过渡,用于更简单的用例。
在下一节中,我们将回顾一些 web 动画和过渡的基础知识,并提供一些资源链接以进行进一步的研究。如果你已经熟悉 web 动画,并且了解这些原理如何与 Vue 的某些指令配合使用,可以跳过这一节。对于希望在开始学习之前进一步了解网络动画基础知识的其他人,请继续阅读。
尽管 <transition>
组件对于组件的进入和离开非常有用,但你也可以通过添加一个条件 class 来激活动画,而无需挂载组件。
<div id="demo"> Push this button to do something you shouldn't be doing:<br /> <div :class="{ shake: noActivated }"> <button @click="noActivated = true">Click me</button> <span v-if="noActivated">Oh no!</span> </div></div>
.shake { animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) both; transform: translate3d(0, 0, 0); backface-visibility: hidden; perspective: 1000px;}@keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); }}
const Demo = { data() { return { noActivated: false } }}Vue.createApp(Demo).mount('#demo')
一些过渡效果可以通过插值的方式来实现,例如在发生交互时将样式绑定到元素上。以这个例子为例:
<div id="demo"> <div @mousemove="xCoordinate" :style="{ backgroundColor: `hsl(${x}, 80%, 50%)` }" class="movearea" > <h3>Move your mouse across the screen...</h3> <p>x: {{x}}</p> </div></div>
.movearea { transition: 0.2s background-color ease;}
const Demo = { data() { return { x: 0 } }, methods: { xCoordinate(e) { this.x = e.clientX } }}Vue.createApp(Demo).mount('#demo')
在这个例子中,我们是通过使用插值来创建动画,将触发条件添加到鼠标的移动过程上。同时将 CSS 过渡属性应用在元素上,让元素知道在更新时要使用什么过渡效果。
你可能注意到上面显示的动画使用了 transforms
之类的东西,并应用了诸如 perspective
之类的奇怪的 property——为什么它们是这样实现的,而不是仅仅使用 margin
和 top
等?
我们可以通过对性能的了解,在 web 上创建极其流畅的动画。我们希望尽可能对元素动画进行硬件加速,并使用不触发重绘的 property。我们来介绍一下如何实现这个目标。
我们可以通过工具,例如 CSS Triggers 来查看哪些属性会在动画时触发重绘。在工具中,查看 transform
的相关内容,你将看到:
非常好的是,更改 transform 不会触发任何几何形状变化或绘制。这意味着该操作可能是由合成器线程在 GPU 的帮助下执行。
opacity
属性的行为也类似。因此,他们是在 web 上做元素移动的理想选择。
诸如 perspective
、backface-visibility
和 transform:translateZ(x)
等 property 将让浏览器知道你需要硬件加速。
如果要对一个元素进行硬件加速,可以应用以下任何一个 property (并不是需要全部,任意一个就可以):
perspective: 1000px;backface-visibility: hidden;transform: translateZ(0);
许多像 GreenSock 这样的 JS 库都会默认你需要硬件加速,并在默认情况下应用,所以你不需要手动设置它们。
对于简单 UI 过渡,即从一个状态到另一个没有中间状态的状态,通常使用 0.1s 到 0.4s 之间的计时,大多数人发现 0.25s 是一个最佳选择。你能用这个定时做任何事情吗?并不是。如果你有一些元素需要移动更大的距离,或者有更多的步骤或状态变化,0.25s 并不会有很好的效果,你将不得不有更多的目的性,而且定时也需要更加独特。但这并不意味着你不能在应用中重复使用效果好的默认值。
你也可能会发现,起始动画比结束动画的时间稍长一些,看起来会更好一些。用户通常是在动画开始时被引导的,而在动画结束时没有那么多耐心,因为他们想继续他们的动作。
Easing 是在动画中表达深度的一个重要方式。动画新手最常犯的一个错误是在起始动画节点使用 ease-in
,在结束动画节点使用 ease-out
。实际上你需要的是反过来的。
如果我们将这些状态应用于过渡,它应该像这样:
.button { background: #1b8f5a; /* 应用于初始状态,因此此转换将应用于返回状态 */ transition: background 0.25s ease-in;}.button:hover { background: #3eaf7c; /* 应用于悬停状态,因此在触发悬停时将应用此过渡 */ transition: background 0.35s ease-out;}
Easing 也可以表达动画元素的质量。以下面的 Pen 为例,你认为哪个球是硬的,哪个球是软的?
你可以通过调整你的 Easing 来获得很多独特的效果,使你的动画非常时尚。CSS 允许你通过调整 cubic bezier 属性来修改 Easing,Lea Verou 开发的这个 playground 对探索这个问题非常有帮助。
虽然使用 cubic-bezier ease 提供的两个控制柄可以为简单的动画获得很好的效果,但是 JavaScript 允许多个控制柄,以此支持更多的变化。
以弹跳为例。在 CSS 中,我们必须声明向上和向下的每个关键帧。在 JavaScript 中,我们可以通过在 greensock API(GSAP) 中声明 bounce
来描述 ease 中所有这些移动 (其他 JS 库有其他类型的 easing 默认值)。
这里是 CSS 中用来实现 bounce 的代码 (来自 animate.css 的例子):
@keyframes bounceInDown { from, 60%, 75%, 90%, to { animation-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1); } 0% { opacity: 0; transform: translate3d(0, -3000px, 0) scaleY(3); } 60% { opacity: 1; transform: translate3d(0, 25px, 0) scaleY(0.9); } 75% { transform: translate3d(0, -10px, 0) scaleY(0.95); } 90% { transform: translate3d(0, 5px, 0) scaleY(0.985); } to { transform: translate3d(0, 0, 0); }}.bounceInDown { animation-name: bounceInDown;}
下面是 JS 中使用 GreenSock 实现相同的 bounce:
gsap.from(element, { duration: 1, ease: 'bounce.out', y: -500 })
我们将在之后章节的部分示例中使用 GreenSock。他们有一个很棒的 ease visualizer,帮助你建立精心制作的画架。
在插入、更新或从 DOM 中移除项时,Vue 提供了多种应用转换效果的方法。这包括以下工具:
在这里,我们只会讲到进入、离开和列表的过渡,你也可以看下一节的管理过渡状态 。
Vue 提供了 transition
的封装组件,在下列情形中,可以给任何元素和组件添加进入/离开过渡
v-if
)v-show
)这里是一个典型的例子:
<div id="demo"> <button @click="show = !show"> Toggle </button> <transition name="fade"> <p v-if="show">hello</p> </transition></div>
const Demo = { data() { return { show: true } }}Vue.createApp(Demo).mount('#demo')
.fade-enter-active,.fade-leave-active { transition: opacity 0.5s ease;}.fade-enter-from,.fade-leave-to { opacity: 0;}
当插入或删除包含在 transition
组件中的元素时,Vue 将会做以下处理:
nextTick
概念不同)在进入/离开的过渡中,会有 6 个 class 切换。
v-enter-from
:定义进入过渡的开始状态。在元素被插入之前生效,在元素被插入之后的下一帧移除。v-enter-active
:定义进入过渡生效时的状态。在整个进入过渡的阶段中应用,在元素被插入之前生效,在过渡/动画完成之后移除。这个类可以被用来定义进入过渡的过程时间,延迟和曲线函数。v-enter-to
:定义进入过渡的结束状态。在元素被插入之后下一帧生效 (与此同时 v-enter-from
被移除),在过渡/动画完成之后移除。v-leave-from
:定义离开过渡的开始状态。在离开过渡被触发时立刻生效,下一帧被移除。v-leave-active
:定义离开过渡生效时的状态。在整个离开过渡的阶段中应用,在离开过渡被触发时立刻生效,在过渡/动画完成之后移除。这个类可以被用来定义离开过渡的过程时间,延迟和曲线函数。v-leave-to
:离开过渡的结束状态。在离开过渡被触发之后下一帧生效 (与此同时 v-leave-from
被删除),在过渡/动画完成之后移除。对于这些在过渡中切换的类名来说,如果你使用一个没有名字的 <transition>
,则 v-
是这些class名的默认前缀。如果你使用了 <transition name="my-transition">
,那么 v-enter-from
会替换为 my-transition-enter-from
。
v-enter-active
和 v-leave-active
可以控制进入/离开过渡的不同的缓和曲线,在下面章节会有个示例说明。
常用的过渡都是使用 CSS 过渡。
<div id="demo"> <button @click="show = !show"> Toggle render </button> <transition name="slide-fade"> <p v-if="show">hello</p> </transition></div>
const Demo = { data() { return { show: true } }}Vue.createApp(Demo).mount('#demo')
/* 可以设置不同的进入和离开动画 *//* 设置持续时间和动画函数 */.slide-fade-enter-active { transition: all 0.3s ease-out;}.slide-fade-leave-active { transition: all 0.8s cubic-bezier(1, 0.5, 0.8, 1);}.slide-fade-enter-from,.slide-fade-leave-to { transform: translateX(20px); opacity: 0;}
CSS 动画用法同 CSS 过渡,区别是在动画中 v-enter-from
类名在节点插入 DOM 后不会立即删除,而是在 animationend
事件触发时删除。
下面是一个例子,为了简洁起见,省略了带前缀的 CSS 规则:
<div id="demo"> <button @click="show = !show">Toggle show</button> <transition name="bounce"> <p v-if="show"> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Mauris facilisis enim libero, at lacinia diam fermentum id. Pellentesque habitant morbi tristique senectus et netus. </p> </transition></div>
const Demo = { data() { return { show: true } }}Vue.createApp(Demo).mount('#demo')
.bounce-enter-active { animation: bounce-in 0.5s;}.bounce-leave-active { animation: bounce-in 0.5s reverse;}@keyframes bounce-in { 0% { transform: scale(0); } 50% { transform: scale(1.25); } 100% { transform: scale(1); }}
我们可以通过以下 attribute 来自定义过渡类名:
enter-from-class
enter-active-class
enter-to-class
leave-from-class
leave-active-class
leave-to-class
他们的优先级高于普通的类名,这对于 Vue 的过渡系统和其他第三方 CSS 动画库,如 Animate.css. 结合使用十分有用。
示例:
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css" rel="external nofollow" target="_blank" rel="stylesheet" type="text/css"/><div id="demo"> <button @click="show = !show"> Toggle render </button> <transition name="custom-classes-transition" enter-active-class="animate__animated animate__tada" leave-active-class="animate__animated animate__bounceOutRight" > <p v-if="show">hello</p> </transition></div>
const Demo = { data() { return { show: true } }}Vue.createApp(Demo).mount('#demo')
Vue 为了知道过渡的完成,必须设置相应的事件监听器。它可以是 transitionend
或 animationend
,这取决于给元素应用的 CSS 规则。如果你使用其中任何一种,Vue 能自动识别类型并设置监听。
但是,在一些场景中,你需要给同一个元素同时设置两种过渡动效,比如 animation 很快的被触发并完成了,而 transition 效果还没结束。在这种情况中,你就需要使用 type
attribute 并设置 animation
或 transition
来明确声明你需要 Vue 监听的类型。
在很多情况下,Vue 可以自动得出过渡效果的完成时机。默认情况下,Vue 会等待其在过渡效果的根元素的第一个 transitionend
或 animationend
事件。然而也可以不这样设定——比如,我们可以拥有一个精心编排的一系列过渡效果,其中一些嵌套的内部元素相比于过渡效果的根元素有延迟的或更长的过渡效果。
在这种情况下你可以用 <transition>
组件上的 duration
prop 定制一个显性的过渡持续时间 (以毫秒计):
<transition :duration="1000">...</transition>
你也可以定制进入和移出的持续时间:
<transition :duration="{ enter: 500, leave: 800 }">...</transition>
可以在 attribute 中声明 JavaScript 钩子
<transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @enter-cancelled="enterCancelled" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" @leave-cancelled="leaveCancelled" :css="false"> <!-- ... --></transition>
// ...methods: { // -------- // ENTERING // -------- beforeEnter(el) { // ... }, // 当与 CSS 结合使用时 // 回调函数 done 是可选的 enter(el, done) { // ... done() }, afterEnter(el) { // ... }, enterCancelled(el) { // ... }, // -------- // 离开时 // -------- beforeLeave(el) { // ... }, // 当与 CSS 结合使用时 // 回调函数 done 是可选的 leave(el, done) { // ... done() }, afterLeave(el) { // ... }, // leaveCancelled 只用于 v-show 中 leaveCancelled(el) { // ... }}
这些钩子函数可以结合 CSS transitions/animations 使用,也可以单独使用。
当只用 JavaScript 过渡的时候,在 enter
和 leave
钩中必须使用 done
进行回调。否则,它们将被同步调用,过渡会立即完成。添加 :css="false"
,也会让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。
现在让我们来看一个例子。下面是一个使用 GreenSock 的 JavaScript 过渡:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js" rel="external nofollow" ></script><div id="demo"> <button @click="show = !show"> Toggle </button> <transition @before-enter="beforeEnter" @enter="enter" @leave="leave" :css="false" > <p v-if="show"> Demo </p> </transition></div>
const Demo = { data() { return { show: false } }, methods: { beforeEnter(el) { gsap.set(el, { scaleX: 0.8, scaleY: 1.2 }) }, enter(el, done) { gsap.to(el, { duration: 1, scaleX: 1.5, scaleY: 0.7, opacity: 1, x: 150, ease: 'elastic.inOut(2.5, 1)', onComplete: done }) }, leave(el, done) { gsap.to(el, { duration: 0.7, scaleX: 1, scaleY: 1, x: 300, ease: 'elastic.inOut(2.5, 1)' }) gsap.to(el, { duration: 0.2, delay: 0.5, opacity: 0, onComplete: done }) } }}Vue.createApp(Demo).mount('#demo')
可以通过 appear
attribute 设置节点在初始渲染的过渡
<transition appear> <!-- ... --></transition>
我们之后讨论多个组件的过渡,对于原生标签可以使用 v-if
/v-else
。最常见的多标签过渡是一个列表和描述这个列表为空消息的元素:
<transition> <table v-if="items.length > 0"> <!-- ... --> </table> <p v-else>Sorry, no items found.</p></transition>
实际上,通过使用多个 v-if
或将单个元素绑定到一个动态 property,可以在任意数量的元素之间进行过渡。例如:
<transition> <button v-if="docState === 'saved'" key="saved"> Edit </button> <button v-if="docState === 'edited'" key="edited"> Save </button> <button v-if="docState === 'editing'" key="editing"> Cancel </button></transition>
也可以写为:
<transition> <button :key="docState"> {{ buttonMessage }} </button></transition>
// ...computed: { buttonMessage() { switch (this.docState) { case 'saved': return 'Edit' case 'edited': return 'Save' case 'editing': return 'Cancel' } }}
这里还有一个问题,试着点击下面的按钮:
在“on”按钮和“off”按钮的过渡中,两个按钮都被重绘了,一个离开过渡的时候另一个开始进入过渡。这是 <transition>
的默认行为 —— 进入和离开同时发生。
有时这很有效,例如当过渡项绝对位于彼此的 top 时:
同时生效的进入和离开的过渡不能满足所有要求,所以 Vue 提供了过渡模式
in-out
: 新元素先进行过渡,完成之后当前元素过渡离开。out-in
: 当前元素先进行过渡,完成之后新元素过渡进入。TIP
很快就会发现 out-in
是你大多数时候想要的状态 ????
现在让我们用 out-in
更新 on/off 按钮的转换:
<transition name="fade" mode="out-in"> <!-- ... the buttons ... --></transition>
通过添加一个 attribute,我们修复了原来的过渡,而不必添加任何特殊 style。
我们可以用它来协调更具表现力的动作,例如折叠卡片,如下所示。实际上是两个元素在彼此之间转换,但是由于开始状态和结束状态的比例是相同的:水平为0,它看起来就像一个流体运动。这种轻描淡写对于真实的 UI 微交互非常有用:
组件之间的过渡更简单 —— 我们甚至不需要 key
属性。相反,我们包装了一个动态组件 :
<div id="demo"> <input v-model="view" type="radio" value="v-a" id="a"><label for="a">A</label> <input v-model="view" type="radio" value="v-b" id="b"><label for="b">B</label> <transition name="component-fade" mode="out-in"> <component :is="view"></component> </transition></div>
const Demo = { data() { return { view: 'v-a' } }, components: { 'v-a': { template: '<div>Component A</div>' }, 'v-b': { template: '<div>Component B</div>' } }}Vue.createApp(Demo).mount('#demo')
.component-fade-enter-active,.component-fade-leave-active { transition: opacity 0.3s ease;}.component-fade-enter-from,.component-fade-leave-to { opacity: 0;}
目前为止,关于过渡我们已经讲到:
那么怎么同时渲染整个列表,比如使用 v-for
?在这种场景中,使用 <transition-group>
组件。在我们深入例子之前,先了解关于这个组件的几个特点:
<transition>
,它会以一个真实元素渲染:默认为一个 <span>
。你也可以通过 tag
attribute 更换为其他元素。key
attribute 值。现在让我们由一个简单的例子深入,进入和离开的过渡使用之前一样的 CSS class 名。
<div id="list-demo"> <button @click="add">Add</button> <button @click="remove">Remove</button> <transition-group name="list" tag="p"> <span v-for="item in items" :key="item" class="list-item"> {{ item }} </span> </transition-group></div>
const Demo = { data() { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 } }, methods: { randomIndex() { return Math.floor(Math.random() * this.items.length) }, add() { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove() { this.items.splice(this.randomIndex(), 1) } }}Vue.createApp(Demo).mount('#list-demo')
.list-item { display: inline-block; margin-right: 10px;}.list-enter-active,.list-leave-active { transition: all 1s ease;}.list-enter-from,.list-leave-to { opacity: 0; transform: translateY(30px);}
这个例子有个问题,当添加和移除元素的时候,周围的元素会瞬间移动到他们的新布局的位置,而不是平滑的过渡,我们下面会解决这个问题。
<transition-group>
组件还有一个特殊之处。不仅可以进入和离开动画,还可以改变定位。要使用这个新功能只需了解新增的 v-move
class,它会在元素的改变定位的过程中应用。像之前的类名一样,可以通过 name
attribute 来自定义前缀,也可以通过 move-class
attribute 手动设置。
该 class 主要用于指定过渡 timing 和 easing 曲线,如下所示:
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js" rel="external nofollow" ></script><div id="flip-list-demo"> <button @click="shuffle">Shuffle</button> <transition-group name="flip-list" tag="ul"> <li v-for="item in items" :key="item"> {{ item }} </li> </transition-group></div>
const Demo = { data() { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9] } }, methods: { shuffle() { this.items = _.shuffle(this.items) } }}Vue.createApp(Demo).mount('#flip-list-demo')
.flip-list-move { transition: transform 0.8s ease;}
这个看起来很神奇,内部的实现,Vue 使用了一个叫 FLIP 简单的动画队列使用 transforms 将元素从之前的位置平滑过渡新的位置。
我们将之前实现的例子和这个技术结合,使我们列表的一切变动都会有动画过渡。
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js" rel="external nofollow" ></script><div id="list-complete-demo" class="demo"> <button @click="shuffle">Shuffle</button> <button @click="add">Add</button> <button @click="remove">Remove</button> <transition-group name="list-complete" tag="p"> <span v-for="item in items" :key="item" class="list-complete-item"> {{ item }} </span> </transition-group></div>
const Demo = { data() { return { items: [1, 2, 3, 4, 5, 6, 7, 8, 9], nextNum: 10 } }, methods: { randomIndex() { return Math.floor(Math.random() * this.items.length) }, add() { this.items.splice(this.randomIndex(), 0, this.nextNum++) }, remove() { this.items.splice(this.randomIndex(), 1) }, shuffle() { this.items = _.shuffle(this.items) } }}Vue.createApp(Demo).mount('#list-complete-demo')
.list-complete-item { transition: all 0.8s ease; display: inline-block; margin-right: 10px;}.list-complete-enter-from,.list-complete-leave-to { opacity: 0; transform: translateY(30px);}.list-complete-leave-active { position: absolute;}
TIP
需要注意的是使用 FLIP 过渡的元素不能设置为 display: inline
。作为替代方案,可以设置为 display: inline-block
或者放置于 flex 中
FLIP 动画不仅可以实现单列过渡,多维网格也同样可以过渡:
TODO:示例
通过 data attribute 与 JavaScript 通信,就可以实现列表的交错过渡:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js" rel="external nofollow" ></script><div id="demo"> <input v-model="query" /> <transition-group name="staggered-fade" tag="ul" :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave" > <li v-for="(item, index) in computedList" :key="item.msg" :data-index="index" > {{ item.msg }} </li> </transition-group></div>
const Demo = { data() { return { query: '', list: [ { msg: 'Bruce Lee' }, { msg: 'Jackie Chan' }, { msg: 'Chuck Norris' }, { msg: 'Jet Li' }, { msg: 'Kung Fury' } ] } }, computed: { computedList() { var vm = this return this.list.filter(item => { return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1 }) } }, methods: { beforeEnter(el) { el.style.opacity = 0 el.style.height = 0 }, enter(el, done) { gsap.to(el, { opacity: 1, height: '1.6em', delay: el.dataset.index * 0.15, onComplete: done }) }, leave(el, done) { gsap.to(el, { opacity: 0, height: 0, delay: el.dataset.index * 0.15, onComplete: done }) } }}Vue.createApp(Demo).mount('#demo')
过渡可以通过 Vue 的组件系统实现复用。要创建一个可复用过渡组件,你需要做的就是将 <transition>
或者 <transition-group>
作为根组件,然后将任何子组件放置在其中就可以了。
TODO:使用 Vue3 重构
使用 template 的简单例子:
Vue.component('my-special-transition', { template: ' <transition name="very-special-transition" mode="out-in" @before-enter="beforeEnter" @after-enter="afterEnter" > <slot></slot> </transition> ', methods: { beforeEnter(el) { // ... }, afterEnter(el) { // ... } }})
函数式组件更适合完成这个任务:
Vue.component('my-special-transition', { functional: true, render: function(createElement, context) { var data = { props: { name: 'very-special-transition', mode: 'out-in' }, on: { beforeEnter(el) { // ... }, afterEnter(el) { // ... } } } return createElement('transition', data, context.children) }})
在 Vue 中即使是过渡也是数据驱动的!动态过渡最基本的例子是通过 name
attribute 来绑定动态值。
<transition :name="transitionName"> <!-- ... --></transition>
当你想用 Vue 的过渡系统来定义的 CSS 过渡/动画在不同过渡间切换会非常有用。
所有过渡 attribute 都可以动态绑定,但我们不仅仅只有 attribute 可以利用,还可以通过事件钩子获取上下文中的所有数据,因为事件钩子都是方法。这意味着,根据组件的状态不同,你的 JavaScript 过渡会有不同的表现
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js" rel="external nofollow" ></script><div id="dynamic-fade-demo" class="demo"> Fade In: <input type="range" v-model="fadeInDuration" min="0" :max="maxFadeDuration" /> Fade Out: <input type="range" v-model="fadeOutDuration" min="0" :max="maxFadeDuration" /> <transition :css="false" @before-enter="beforeEnter" @enter="enter" @leave="leave" > <p v-if="show">hello</p> </transition> <button v-if="stop" @click="stop = false; show = false"> Start animating </button> <button v-else @click="stop = true">Stop it!</button></div>
const app = Vue.createApp({ data() { return { show: true, fadeInDuration: 1000, fadeOutDuration: 1000, maxFadeDuration: 1500, stop: true } }, mounted() { this.show = false }, methods: { beforeEnter(el) { el.style.opacity = 0 }, enter(el, done) { var vm = this Velocity( el, { opacity: 1 }, { duration: this.fadeInDuration, complete: function() { done() if (!vm.stop) vm.show = false } } ) }, leave(el, done) { var vm = this Velocity( el, { opacity: 0 }, { duration: this.fadeOutDuration, complete: function() { done() vm.show = true } } ) } }})app.mount('#dynamic-fade-demo')
TODO:示例
最后,创建动态过渡的最终方案是组件通过接受 props 来动态修改之前的过渡。一句老话,唯一的限制是你的想象力。
Vue 的过渡系统提供了非常多简单的方法设置进入、离开和列表的动效。那么对于数据元素本身的动效呢,比如:
这些数据要么本身就以数值形式存储,要么可以转换为数值。有了这些数值后,我们就可以结合 Vue 的响应性和组件系统,使用第三方库来实现切换元素的过渡状态。
通过侦听器我们能监听到任何数值 property 的数值更新。可能听起来很抽象,所以让我们先来看看使用 GreenSock 一个例子:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js" rel="external nofollow" rel="external nofollow" ></script><div id="animated-number-demo"> <input v-model.number="number" type="number" step="20" /> <p>{{ animatedNumber }}</p></div>
const Demo = { data() { return { number: 0, tweenedNumber: 0 } }, computed: { animatedNumber() { return this.tweenedNumber.toFixed(0) } }, watch: { number(newValue) { gsap.to(this.$data, { duration: 0.5, tweenedNumber: newValue }) } }}Vue.createApp(Demo).mount('#animated-number-demo')
更新数字时,更改将在输入下方设置动画。
就像 Vue 的过渡组件一样,数据背后状态过渡会实时更新,这对于原型设计十分有用。当你修改一些变量,即使是一个简单的 SVG 多边形也可实现很多难以想象的效果。
管理太多的状态过渡会很快的增加组件实例复杂性,幸好很多的动画可以提取到专用的子组件。我们来将之前的示例改写一下:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.2.4/gsap.min.js" rel="external nofollow" rel="external nofollow" ></script><div id="app"> <input v-model.number="firstNumber" type="number" step="20" /> + <input v-model.number="secondNumber" type="number" step="20" /> = {{ result }} <p> <animated-integer :value="firstNumber"></animated-integer> + <animated-integer :value="secondNumber"></animated-integer> = <animated-integer :value="result"></animated-integer> </p></div>
const app = Vue.createApp({ data() { return { firstNumber: 20, secondNumber: 40 } }, computed: { result() { return this.firstNumber + this.secondNumber } }})app.component('animated-integer', { template: '<span>{{ fullValue }}</span>', props: { value: { type: Number, required: true } }, data() { return { tweeningValue: 0 } }, computed: { fullValue() { return Math.floor(this.tweeningValue) } }, methods: { tween(newValue, oldValue) { gsap.to(this.$data, { duration: 0.5, tweeningValue: newValue, ease: 'sine' }) } }, watch: { value(newValue, oldValue) { this.tween(newValue, oldValue) } }, mounted() { this.tween(this.value, 0) }})app.mount('#app')
我们能在组件中结合使用这一节讲到各种过渡策略和 Vue 内建的过渡系统。总之,对于完成各种过渡动效几乎没有阻碍。
你可以看到我们如何使用它进行数据可视化,物理效果,角色动画和交互,天空是极限。
只要一个动画,就可以带来生命。不幸的是,当设计师创建图标、logo 和吉祥物的时候,他们交付的通常都是图片或静态的 SVG。所以,虽然 GitHub 的章鱼猫、Twitter 的小鸟以及其它许多 logo 类似于生灵,它们看上去实际上并不是活着的。
Vue 可以帮到你。因为 SVG 的本质是数据,我们只需要这些动物兴奋、思考或警戒的样例。然后 Vue 就可以辅助完成这几种状态之间的过渡动画,来制作你的欢迎页面、加载指示、以及更加带有情感的提示。
Sarah Drasner 展示了下面这个 demo,这个 demo 结合了时间和交互相关的状态改变:
混入 (mixin) 提供了一种非常灵活的方式,来分发 Vue 组件中的可复用功能。一个混入对象可以包含任意组件选项。当组件使用混入对象时,所有混入对象的选项将被“混合”进入该组件本身的选项。
例子:
// define a mixin objectconst myMixin = { created() { this.hello() }, methods: { hello() { console.log('hello from mixin!') } }}// define an app that uses this mixinconst app = Vue.createApp({ mixins: [myMixin]})app.mount('#mixins-basic') // => "hello from mixin!"
当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行“合并”。
比如,数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
const myMixin = { data() { return { message: 'hello', foo: 'abc' } }}const app = Vue.createApp({ mixins: [myMixin], data() { return { message: 'goodbye', bar: 'def' } }, created() { console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" } }})
同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
const myMixin = { created() { console.log('mixin hook called') }}const app = Vue.createApp({ mixins: [myMixin], created() { console.log('component hook called') }})// => "混入对象的钩子被调用"// => "组件钩子被调用"
值为对象的选项,例如 methods
、components
和 directives
,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。
const myMixin = { methods: { foo() { console.log('foo') }, conflicting() { console.log('from mixin') } }}const app = Vue.createApp({ mixins: [myMixin], methods: { bar() { console.log('bar') }, conflicting() { console.log('from self') } }})const vm = app.mount('#mixins-basic')vm.foo() // => "foo"vm.bar() // => "bar"vm.conflicting() // => "from self"
你还可以为 Vue 应用程序全局应用 mixin:
const app = Vue.createApp({ myOption: 'hello!'})// 为自定义的选项 'myOption' 注入一个处理器。app.mixin({ created() { const myOption = this.$options.myOption if (myOption) { console.log(myOption) } }})app.mount('#mixins-global') // => "hello!"
混入也可以进行全局注册。使用时格外小心!一旦使用全局混入,它将影响每一个之后创建的组件 (例如,每个子组件)。
const app = Vue.createApp({ myOption: 'hello!'})// 为自定义的选项 'myOption' 注入一个处理器。app.mixin({ created() { const myOption = this.$options.myOption if (myOption) { console.log(myOption) } }})// 将myOption也添加到子组件app.component('test-component', { myOption: 'hello from component!'})app.mount('#mixins-global')// => "hello!"// => "hello from component!"
大多数情况下,只应当应用于自定义选项,就像上面示例一样。推荐将其作为插件发布,以避免重复应用混入。
自定义选项将使用默认策略,即简单地覆盖已有值。如果想让自定义选项以自定义逻辑合并,可以向 app.config.optionMergeStrategies
添加一个函数:
const app = Vue.createApp({})app.config.optionMergeStrategies.customOption = (toVal, fromVal) => { // return mergedVal}
合并策略接收在父实例和子实例上定义的该选项的值,分别作为第一个和第二个参数。让我们来检查一下使用 mixin 时,这些参数有哪些:
const app = Vue.createApp({ custom: 'hello!'})app.config.optionMergeStrategies.custom = (toVal, fromVal) => { console.log(fromVal, toVal) // => "goodbye!", undefined // => "hello", "goodbye!" return fromVal || toVal}app.mixin({ custom: 'goodbye!', created() { console.log(this.$options.custom) // => "hello!" }})
如你所见,在控制台中,我们先从 mixin 打印 toVal
和 fromVal
,然后从 app
打印。如果存在,我们总是返回 fromVal
,这就是为什么 this.$options.custom
设置为 你好!
最后。让我们尝试将策略更改为始终从子实例返回值:
const app = Vue.createApp({ custom: 'hello!'})app.config.optionMergeStrategies.custom = (toVal, fromVal) => toVal || fromValapp.mixin({ custom: 'goodbye!', created() { console.log(this.$options.custom) // => "goodbye!" }})
在 Vue 2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:
为了解决这些问题,我们添加了一种通过逻辑关注点组织代码的新方法:组合式 API。
除了核心功能默认内置的指令 (v-model
和 v-show
),Vue 也允许注册自定义指令。注意,在 Vue2.0 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。举个聚焦输入框的例子,如下:
当页面加载时,该元素将获得焦点 (注意:autofocus
在移动版 Safari 上不工作)。事实上,只要你在打开这个页面后还没点击过任何内容,这个输入框就应当还是处于聚焦状态。此外,你可以单击 Rerun
按钮,输入将被聚焦。
现在让我们用指令来实现这个功能:
const app = Vue.createApp({})// 注册一个全局自定义指令 `v-focus`app.directive('focus', { // 当被绑定的元素插入到 DOM 中时…… mounted(el) { // Focus the element el.focus() }})
如果想注册局部指令,组件中也接受一个 directives
的选项:
directives: { focus: { // 指令的定义 mounted(el) { el.focus() } }}
然后你可以在模板中任何元素上使用新的 v-focus
property,如下:
<input v-focus />
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
beforeMount
:当指令第一次绑定到元素并且在挂载父组件之前调用。在这里你可以做一次性的初始化设置。mounted
:在挂载绑定元素的父组件时调用。beforeUpdate
:在更新包含组件的 VNode 之前调用。提示
我们会在稍后讨论渲染函数时介绍更多 VNodes 的细节。
updated
:在包含组件的 VNode 及其子组件的 VNode 更新后调用。beforeUnmount
:在卸载绑定元素的父组件之前调用unmounted
:当指令与元素解除绑定且父组件已卸载时,只调用一次。接下来我们来看一下在自定义指令 API 钩子函数的参数 (即 el
、binding
、vnode
和 prevNnode
)
指令的参数可以是动态的。例如,在 v-mydirective:[argument]="value"
中,argument
参数可以根据组件实例数据进行更新!这使得自定义指令可以在应用中被灵活使用。
例如你想要创建一个自定义指令,用来通过固定布局将元素固定在页面上。我们可以像这样创建一个通过指令值来更新竖直位置像素值的自定义指令:
<div id="dynamic-arguments-example" class="demo"> <p>Scroll down the page</p> <p v-pin="200">Stick me 200px from the top of the page</p></div>
const app = Vue.createApp({})app.directive('pin', { mounted(el, binding) { el.style.position = 'fixed' // binding.value is the value we pass to directive - in this case, it's 200 el.style.top = binding.value + 'px' }})app.mount('#dynamic-arguments-example')
这会把该元素固定在距离页面顶部 200 像素的位置。但如果场景是我们需要把元素固定在左侧而不是顶部又该怎么办呢?这时使用动态参数就可以非常方便地根据每个组件实例来进行更新。
<div id="dynamicexample"> <h3>Scroll down inside this section ↓</h3> <p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p></div>
const app = Vue.createApp({ data() { return { direction: 'right' } }})app.directive('pin', { mounted(el, binding) { el.style.position = 'fixed' // binding.arg is an argument we pass to directive const s = binding.arg || 'top' el.style[s] = binding.value + 'px' }})app.mount('#dynamic-arguments-example')
结果:
我们的定制指令现在已经足够灵活,可以支持一些不同的用例。为了使其更具动态性,我们还可以允许修改绑定值。让我们创建一个附加属性 pinPadding
,并将其绑定到 <input type="range">
。
<div id="dynamicexample"> <h2>Scroll down the page</h2> <input type="range" min="0" max="500" v-model="pinPadding"> <p v-pin:[direction]="pinPadding">Stick me {{ pinPadding + 'px' }} from the {{ direction }} of the page</p></div>
const app = Vue.createApp({ data() { return { direction: 'right', pinPadding: 200 } }})
让我们扩展我们的指令逻辑来重新计算固定元件更新的距离。
app.directive('pin', { mounted(el, binding) { el.style.position = 'fixed' const s = binding.arg || 'top' el.style[s] = binding.value + 'px' }, updated(el, binding) { const s = binding.arg || 'top' el.style[s] = binding.value + 'px' }})
结果:
在很多时候,你可能想在 mounted
和 updated
时触发相同行为,而不关心其它的钩子。比如这样写:
app.directive('pin', (el, binding) => { el.style.position = 'fixed' const s = binding.arg || 'top' el.style[s] = binding.value + 'px'})
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => { console.log(binding.value.color) // => "white" console.log(binding.value.text) // => "hello!"})
在 3.0 中,有了片段支持,组件可能有多个根节点。如果在具有多个根节点的组件上使用自定义指令,则会产生问题。
要解释自定义指令如何在 3.0 中的组件上工作的详细信息,我们首先需要了解自定义指令在 3.0 中是如何编译的。对于这样的指令:
<div v-demo="test"></div>
将大概编译成:
const vDemo = resolveDirective('demo')return withDirectives(h('div'), [[vDemo, test]])
其中 vDemo
是用户编写的指令对象,其中包含 mounted
和 updated
等钩子。
withDirectives
返回一个克隆的 VNode,其中用户钩子被包装并作为 VNode 生命周期钩子注入 (请参见渲染函数更多详情):
{ onVnodeMounted(vnode) { // call vDemo.mounted(...) }}
因此,自定义指令作为 VNode 数据的一部分完全包含在内。当在组件上使用自定义指令时,这些 onVnodeXXX
钩子作为无关的 prop 传递给组件,并以 this.$attrs
结束。
这也意味着可以像这样在模板中直接挂接到元素的生命周期中,这在涉及到自定义指令时非常方便:
<div @vnodeMounted="myHook" />
这和 非 prop 的 attribute类似。因此,组件上自定义指令的规则将与其他无关 attribute 相同:由子组件决定在哪里以及是否应用它。当子组件在内部元素上使用 v-bind="$attrs"
时,它也将应用对其使用的任何自定义指令。
Vue 鼓励我们通过将 UI 和相关行为封装到组件中来构建 UI。我们可以将它们嵌套在另一个内部,以构建一个组成应用程序 UI 的树。
然而,有时组件模板的一部分逻辑上属于该组件,而从技术角度来看,最好将模板的这一部分移动到 DOM 中 Vue app 之外的其他位置。
一个常见的场景是创建一个包含全屏模式的组件。在大多数情况下,你希望模态框的逻辑存在于组件中,但是模态框的快速定位就很难通过 CSS 来解决,或者需要更改组件组合。
考虑下面的 HTML 结构。
<body> <div style="position: relative;"> <h3>Tooltips with Vue 3 Teleport</h3> <div> <modal-button></modal-button> </div> </div></body>
让我们来看看 modal-button
组件:
该组件将有一个 button
元素来触发模态的打开,以及一个具有类 .modal
的 div
元素,它将包含模态的内容和一个用于自关闭的按钮。
const app = Vue.createApp({});app.component('modal-button', { template: ` <button @click="modalOpen = true"> Open full screen modal! </button> <div v-if="modalOpen" class="modal"> <div> I'm a modal! <button @click="modalOpen = false"> Close </button> </div> </div> `, data() { return { modalOpen: false } }})
当在初始的 HTML 结构中使用这个组件时,我们可以看到一个问题——模态是在深度嵌套的 div
中渲染的,而模态的 position:absolute
以父级相对定位的 div
作为引用。
Teleport 提供了一种干净的方法,允许我们控制在 DOM 中哪个父节点下呈现 HTML,而不必求助于全局状态或将其拆分为两个组件。
让我们修改 modal-button
以使用 <teleport>
,并告诉 Vue “Teleport 这个 HTML 到该‘body’标签”。
app.component('modal-button', { template: ` <button @click="modalOpen = true"> Open full screen modal! (With teleport!) </button> <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> `, data() { return { modalOpen: false } }})
因此,一旦我们单击按钮打开模式,Vue 将正确地将模态内容渲染为 body
标签的子级。
如果 <teleport>
包含 Vue 组件,则它仍将是 <teleport>
父组件的逻辑子组件:
const app = Vue.createApp({ template: ` <h1>Root instance</h1> <parent-component /> `})app.component('parent-component', { template: ` <h2>This is a parent component</h2> <teleport to="#endofbody"> <child-component name="John" /> </teleport> `})app.component('child-component', { props: ['name'], template: ` <div>Hello, {{ name }}</div> `})
在这种情况下,即使在不同的地方渲染 child-component
,它仍将是 parent-component
的子级,并将从中接收 name
prop。
这也意味着来自父组件的注入按预期工作,并且子组件将嵌套在 Vue Devtools 中的父组件之下,而不是放在实际内容移动到的位置。
一个常见的用例场景是一个可重用的 <Modal>
组件,它可能同时有多个实例处于活动状态。对于这种情况,多个 <teleport>
组件可以将其内容挂载到同一个目标元素。顺序将是一个简单的追加——稍后挂载将位于目标元素中较早的挂载之后。
<teleport to="#modals"> <div>A</div></teleport><teleport to="#modals"> <div>B</div></teleport><!-- result--><div id="modals"> <div>A</div> <div>B</div></div>
你可以在 API 参考 查看 teleport
组件。
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
让我们深入一个简单的例子,这个例子里 render
函数很实用。假设我们要生成一些带锚点的标题:
<h1> <a name="hello-world" href="#hello-world"> Hello world! </a></h1>
锚点标题的使用非常频繁,我们应该创建一个组件:
<anchored-heading :level="1">Hello world!</anchored-heading>
当开始写一个只能通过 level
prop 动态生成标题 (heading) 的组件时,我们很快就可以得出这样的结论:
const app = Vue.createApp({})app.component('anchored-heading', { template: ` <h1 v-if="level === 1"> <slot></slot> </h1> <h2 v-else-if="level === 2"> <slot></slot> </h2> <h3 v-else-if="level === 3"> <slot></slot> </h3> <h4 v-else-if="level === 4"> <slot></slot> </h4> <h5 v-else-if="level === 5"> <slot></slot> </h5> <h6 v-else-if="level === 6"> <slot></slot> </h6> `, props: { level: { type: Number, required: true } }})
这个模板感觉不太好。它不仅冗长,而且我们为每个级别标题重复书写了 <slot></slot>
。当我们添加锚元素时,我们必须在每个 v-if/v-else-if
分支中再次重复它。
虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render
函数重写上面的例子:
const app = Vue.createApp({})app.component('anchored-heading', { render() { const { h } = Vue return h( 'h' + this.level, // tag name {}, // props/attributes this.$slots.default() // array of children ) }, props: { level: { type: Number, required: true } }})
render()
函数的实现要精简得多,但是需要非常熟悉组件的实例 property。在这个例子中,你需要知道,向组件中传递不带 v-slot
指令的子节点时,比如 anchored-heading
中的 Hello world!
,这些子节点被存储在组件实例中的 $slots.default
中。如果你还不了解,在深入渲染函数之前推荐阅读实例 property API。
在深入渲染函数之前,了解一些浏览器的工作原理是很重要的。以下面这段 HTML 为例:
<div> <h1>My title</h1> Some text content <!-- TODO: Add tagline --></div>
当浏览器读到这些代码时,它会建立一个 ”DOM 节点“ 树 来保持追踪所有内容,如同你会画一张家谱树来追踪家庭成员的发展一样。
每个元素都是一个节点。每段文字也是一个节点。甚至注释也都是节点。一个节点就是页面的一个部分。就像家谱树一样,每个节点都可以有孩子节点 (也就是说每个部分可以包含其它的一些部分)。
高效地更新所有这些节点会是比较困难的,不过所幸你不必手动完成这个工作。你只需要告诉 Vue 你希望页面上的 HTML 是什么,这可以是在一个模板里:
<h1>{{ blogTitle }}</h1>
或者一个渲染函数里:
render() { return Vue.h('h1', {}, this.blogTitle)}
在这两种情况下,Vue 都会自动保持页面的更新,即便 blogTitle
发生了改变。
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:
return Vue.h('h1', {}, this.blogTitle)
h()
到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
h()
参数h()
函数是一个用于创建 vnode 的实用程序。也许可以更准确地将其命名为 createVNode()
,但由于频繁使用和简洁,它被称为 h()
。它接受三个参数:
// @returns {VNode}h( // {String | Object | Function | null} tag // 一个 HTML 标签名、一个组件、一个异步组件,或者 null。 // 使用 null 将会渲染一个注释。 // // 必需的。 'div', // {Object} props // 与 attribute、prop 和事件相对应的对象。 // 我们会在模板中使用。 // // 可选的。 {}, // {String | Array | Object} children // 子 VNodes, 使用 `h()` 构建, // 或使用字符串获取 "文本 Vnode" 或者 // 有 slot 的对象。 // // 可选的。 [ 'Some text comes first.', h('h1', 'A headline'), h(MyComponent, { someProp: 'foobar' }) ])
有了这些知识,我们现在可以完成我们最开始想实现的组件:
const app = Vue.createApp({})/** Recursively get text from children nodes */function getChildrenTextContent(children) { return children .map(node => { return typeof node.children === 'string' ? node.children : Array.isArray(node.children) ? getChildrenTextContent(node.children) : '' }) .join('')}app.component('anchored-heading', { render() { // create kebab-case id from the text contents of the children const headingId = getChildrenTextContent(this.$slots.default()) .toLowerCase() .replace(/W+/g, '-') // replace non-word characters with dash .replace(/(^-|-$)/g, '') // remove leading and trailing dashes return Vue.h('h' + this.level, [ Vue.h( 'a', { name: headingId, href: '#' + headingId }, this.$slots.default() ) ]) }, props: { level: { type: Number, required: true } }})
组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:
render() { const myParagraphVNode = Vue.h('p', 'hi') return Vue.h('div', [ // 错误 - 重复的Vnode! myParagraphVNode, myParagraphVNode ])}
如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:
render() { return Vue.h('div', Array.apply(null, { length: 20 }).map(() => { return Vue.h('p', 'hi') }) )}
v-if
和 v-for
只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-if
和 v-for
:
<ul v-if="items.length"> <li v-for="item in items">{{ item.name }}</li></ul><p v-else>No items found.</p>
这些都可以在渲染函数中用 JavaScript 的 if
/else
和 map()
来重写:
props: ['items'],render() { if (this.items.length) { return Vue.h('ul', this.items.map((item) => { return Vue.h('li', item.name) })) } else { return Vue.h('p', 'No items found.') }}
v-model
v-model
指令扩展为 modelValue
和 onUpdate:modelValue
在模板编译过程中,我们必须自己提供这些prop:
props: ['modelValue'],render() { return Vue.h(SomeComponent, { modelValue: this.modelValue, 'onUpdate:modelValue': value => this.$emit('update:modelValue', value) })}
v-on
我们必须为事件处理程序提供一个正确的prop名称,例如,要处理 click
事件,prop名称应该是 onClick
。
render() { return Vue.h('div', { onClick: $event => console.log('clicked', $event.target) })}
对于 .passive
、 .capture
和 .once
事件修饰符,Vue提供了处理程序的对象语法:
实例:
render() { return Vue.h('input', { onClick: { handler: this.doThisInCapturingMode, capture: true }, onKeyUp: { handler: this.doThisOnce, once: true }, onMouseOver: { handler: this.doThisOnceInCapturingMode, once: true, capture: true }, })}
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 | 处理函数中的等价操作 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
按键: .enter , .13 | if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码 |
修饰键: .ctrl , .alt , .shift , .meta | if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey , shiftKey , 或 metaKey ) |
这里是一个使用所有修饰符的例子:
render() { return Vue.h('input', { onKeyUp: event => { // 如果触发事件的元素不是事件绑定的元素 // 则返回 if (event.target !== event.currentTarget) return // 如果向上键不是回车键,则中止 // 没有同时按下按键 (13) 和 shift 键 if (!event.shiftKey || event.keyCode !== 13) return // 停止事件传播 event.stopPropagation() // 阻止该元素默认的 keyup 事件 event.preventDefault() // ... } })}
你可以通过 this.$slots
访问静态插槽的内容,每个插槽都是一个 VNode 数组:
render() { // `<div><slot></slot></div>` return Vue.h('div', {}, this.$slots.default())}
props: ['message'],render() { // `<div><slot :text="message"></slot></div>` return Vue.h('div', {}, this.$slots.default({ text: this.message }))}
要使用渲染函数将插槽传递给子组件,请执行以下操作:
render() { // `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>` return Vue.h('div', [ Vue.h('child', {}, { // pass `slots` as the children object // in the form of { name: props => VNode | Array<VNode> } default: (props) => Vue.h('span', props.text) }) ])}
如果你写了很多渲染函数,可能会觉得下面这样的代码写起来很痛苦:
Vue.h( 'anchored-heading', { level: 1 }, [Vue.h('span', 'Hello'), ' world!'])
特别是对应的模板如此简单的情况下:
<anchored-heading :level="1"> <span>Hello</span> world! </anchored-heading>
这就是为什么会有一个 Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'new Vue({ el: '#demo', render() { return ( <AnchoredHeading level={1}> <span>Hello</span> world! </AnchoredHeading> ) }})
有关 JSX 如何映射到 JavaScript 的更多信息,请参阅使用文档 。
你可能会有兴趣知道,Vue 的模板实际上被编译成了渲染函数。这是一个实现细节,通常不需要关心。但如果你想看看模板的功能具体是怎样被编译的,可能会发现会非常有意思。下面是一个使用 Vue.compile
来实时编译模板字符串的简单示例:
插件是自包含的代码,通常向 Vue 添加全局级功能。它可以是公开 install()
方法的 object
,也可以是 function
插件的功能范围没有严格的限制——一般有下面几种:
config.globalProperties
上实现。为了更好地理解如何创建自己的 Vue.js 版插件,我们将创建一个非常简化的插件版本,它显示 i18n
准备好的字符串。
每当这个插件被添加到应用程序中时,如果它是一个对象,就会调用 install
方法。如果它是一个 function
,则函数本身将被调用。在这两种情况下——它都会收到两个参数:由 Vue 的 createApp
生成的 app
对象和用户传入的选项。
让我们从设置插件对象开始。建议在单独的文件中创建它并将其导出,如下所示,以保持包含的逻辑和分离的逻辑。
// plugins/i18n.jsexport default { install: (app, options) => { // Plugin code goes here }}
我们想要一个函数来翻译整个应用程序可用的键,因此我们将使用 app.config.globalProperties
暴露它。
该函数将接收一个 key
字符串,我们将使用它在用户提供的选项中查找转换后的字符串。
// plugins/i18n.jsexport default { install: (app, options) => { app.config.globalProperties.$translate = key => { return key.split('.').reduce((o, i) => { if (o) return o[i] }, i18n) } }}
我们假设用户使用插件时,将在 options
参数中传递一个包含翻译后的键的对象。我们的 $translate
函数将使用诸如 greetings.hello
之类的字符串,查看用户提供的配置内部并返回转换后的值-在这种情况下为 Bonjour!
。
例如:
greetings: { hello: 'Bonjour!'}
插件还允许我们使用 inject
为插件的用户提供功能或 attribute。例如,我们可以允许应用程序访问 options
参数以能够使用翻译对象。
// plugins/i18n.jsexport default { install: (app, options) => { app.config.globalProperties.$translate = key => { return key.split('.').reduce((o, i) => { if (o) return o[i] }, i18n) } app.provide('i18n', options) }}
插件用户现在可以将 inject[in18]
到他们的组件并访问该对象。
另外,由于我们可以访问 app
对象,因此插件可以使用所有其他功能,例如使用 mixin
和 directive
。要了解有关 createApp
和应用程序实例的更多信息,请查看 Application API 文档。
// plugins/i18n.jsexport default { install: (app, options) => { app.config.globalProperties.$translate = (key) => { return key.split('.') .reduce((o, i) => { if (o) return o[i] }, i18n) } app.provide('i18n', options) app.directive('my-directive', { bind (el, binding, vnode, oldVnode) { // some logic ... } ... }) app.mixin({ created() { // some logic ... } ... }) }}
在使用 createApp()
初始化 Vue 应用程序后,你可以通过调用 use()
方法将插件添加到你的应用程序中。
我们将使用在编写插件部分中创建的 i18nPlugin
进行演示。
use()
方法有两个参数。第一个是要安装的插件,在这种情况下为 i18nPlugin
。
它还会自动阻止你多次使用同一插件,因此在同一插件上多次调用只会安装一次该插件。
第二个参数是可选的,并且取决于每个特定的插件。在演示 i18nPlugin
的情况下,它是带有转换后的字符串的对象。
INFO
如果你使用的是第三方插件 (例如 Vuex
或 Vue Router
),请始终查看文档以了解特定插件期望作为第二个参数接收的内容。
import { createApp } from 'vue'import Root from './App.vue'import i18nPlugin from './plugins/i18n'const app = createApp(Root)const i18nStrings = { greetings: { hi: 'Hallo!' }}app.use(i18nPlugin, i18nStrings)app.mount('#app')
awesome-vue 集合了大量由社区贡献的插件和库。
现在是时候深入了!Vue 最独特的特性之一,是其非侵入性的响应性系统。数据模型是被代理的 JavaScript 对象。而当你修改它们时,视图会进行更新。这让状态管理非常简单直观,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。在这个章节,我们将研究一下 Vue 响应性系统的底层的细节。
在 Vue Mastery 上免费观看关于深入响应性原理的视频。
这个术语在程序设计中经常被提及,但这是什么意思呢?响应性是一种允许我们以声明式的方式去适应变化的一种编程范例。人们通常展示的典型例子,是一份 excel 电子表格 (一个非常好的例子)。
如果将数字 2 放在第一个单元格中,将数字 3 放在第二个单元格中并要求提供 SUM,则电子表格会将其计算出来给你。不要惊奇,同时,如果你更新第一个数字,SUM 也会自动更新。
JavaScript 通常不是这样工作的——如果我们想用 JavaScript 编写类似的内容:
var val1 = 2var val2 = 3var sum = val1 + val2// sum// 5val1 = 3// sum// 5
如果我们更新第一个值,sum 不会被修改。
那么我们如何用 JavaScript 实现这一点呢?
当把一个普通的 JavaScript 对象作为 data
选项传给应用或组件实例的时候,Vue 会使用带有 getter 和 setter 的处理程序遍历其所有 property 并将其转换为 Proxy。这是 ES6 仅有的特性,但是我们在 Vue 3 版本也使用了 Object.defineProperty
来支持 IE 浏览器。两者具有相同的 Surface API,但是 Proxy 版本更精简,同时提升了性能。
该部分需要稍微地了解下 Proxy 的某些知识!所以,让我们深入了解一下。关于 Proxy 的文献很多,但是你真正需要知道的是 Proxy 是一个包含另一个对象或函数并允许你对其进行拦截的对象。
我们是这样使用它的:new Proxy(target, handler)
const dinner = { meal: 'tacos'}const handler = { get(target, prop) { return target[prop] }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
好的,到目前为止,我们只是包装这个对象并返回它。很酷,但还不是那么有用。请注意,我们把对象包装在 Proxy 里的同时可以对其进行拦截。这种拦截被称为陷阱。
const dinner = { meal: 'tacos'}const handler = { get(target, prop) { console.log('intercepted!') return target[prop] }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
除了控制台日志,我们可以在这里做任何我们想做的事情。如果我们愿意,我们甚至可以不返回实际值。这就是为什么 Proxy 对于创建 API 如此强大。
此外,Proxy 还提供了另一个特性。我们不必像这样返回值:target[prop]
,而是可以进一步使用一个名为 Reflect
的方法,它允许我们正确地执行 this
绑定,就像这样:
const dinner = { meal: 'tacos'}const handler = { get(target, prop, receiver) { return Reflect.get(...arguments) }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
我们之前提到过,为了有一个 API 能够在某些内容发生变化时更新最终值,我们必须在内容发生变化时设置新的值。我们在处理器,一个名为 track
的函数中执行此操作,该函数可以传入 target
和 key
两个参数。
const dinner = { meal: 'tacos'}const handler = { get(target, prop, receiver) { track(target, prop) return Reflect.get(...arguments) }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
最后,当某些内容发生改变时我们会设置新的值。为此,我们将通过触发这些更改来设置新 Proxy 的更改:
const dinner = { meal: 'tacos'}const handler = { get(target, prop, receiver) { track(target, prop) return Reflect.get(...arguments) }, set(target, key, value, receiver) { trigger(target, key) return Reflect.set(...arguments) }}const proxy = new Proxy(dinner, handler)console.log(proxy.meal)// tacos
还记得几段前的列表吗?现在我们有了一些关于 Vue 如何处理这些更改的答案:
effect
trigger
Proxy 对象对于用户来说是不可见的,但是在内部,它们使 Vue 能够在 property 的值被访问或修改的情况下进行依赖跟踪和变更通知。从 Vue 3 开始,我们的响应性现在可以在独立的包中使用。需要注意的是,记录转换后的数据对象时,浏览器控制台输出的格式会有所不同,因此你可能需要安装 vue-devtools,以提供一种更易于检查的界面。
Vue 在内部跟踪所有已被设置为响应式的对象,因此它始终会返回同一个对象的 Proxy 版本。
从响应式 Proxy 访问嵌套对象时,该对象在返回之前也被转换为 Proxy:
const handler = { get(target, prop, receiver) { track(target, prop) const value = Reflect.get(...arguments) if (isObject(value)) { return reactive(value) } else { return value } } // ...}
Proxy 的使用确实引入了一个需要注意的新警告:在身份比较方面,被代理对象与原始对象不相等 (===
)。例如:
const obj = {}const wrapped = new Proxy(obj, handlers)console.log(obj === wrapped) // false
在大多数情况下,原始版本和包装版本的行为相同,但请注意,它们在依赖严格比对的操作下将是失败的,例如 .filter()
或 .map()
。使用选项式 API 时,这种警告不太可能出现,因为所有响应式都是从 this
访问的,并保证已经是 Proxy。
但是,当使用合成 API 显式创建响应式对象时,最佳做法是不要保留对原始对象的引用,而只使用响应式版本:
const obj = reactive({ count: 0}) // no reference to original
每个组件实例都有一个相应的侦听器实例,该实例将在组件渲染期间把“触碰”的所有 property 记录为依赖项。之后,当触发依赖项的 setter 时,它会通知侦听器,从而使得组件重新渲染。
将对象作为数据传递给组件实例时,Vue 会将其转换为 Proxy。这个 Proxy 使 Vue 能够在 property 被访问或修改时执行依赖项跟踪和更改通知。每个 property 都被视为一个依赖项。
首次渲染后,组件将跟踪一组依赖列表——即在渲染过程中被访问的 property。反过来,组件就成为了其每个 property 的订阅者。当 Proxy 拦截到 set 操作时,该 property 将通知其所有订阅的组件重新渲染。
如果你使用的是 Vue2.x 及以下版本,你可能会对这些版本中存在的一些更改检测警告感兴趣,在这里进行更详细的探讨。
要为 JavaScript 对象创建响应式状态,可以使用 reactive
方法:
import { reactive } from 'vue'// 响应式状态const state = reactive({ count: 0})
reactive
相当于 Vue 2.x 中的 Vue.observable()
API ,为避免与 RxJS 中的 observables 混淆因此对其重命名。该 API 返回一个响应式的对象状态。该响应式转换是“深度转换”——它会影响嵌套对象传递的所有 property。
Vue 中响应式状态的基本用例是我们可以在渲染期间使用它。因为依赖跟踪的关系,当响应式状态改变时视图会自动更新。
这就是 Vue 响应性系统的本质。当从组件中的 data()
返回一个对象时,它在内部交由 reactive()
使其成为响应式对象。模板会被编译成能够使用这些响应式 property 的渲染函数。
在响应性基础 API 章节你可以学习更多关于 reactive
的内容。
refs
想象一下,我们有一个独立的原始值 (例如,一个字符串),我们想让它变成响应式的。当然,我们可以创建一个拥有相同字符串 property 的对象,并将其传递给 reactive
。Vue 为我们提供了一个可以做相同事情的方法 ——ref
:
import { ref } from 'vue'const count = ref(0)
ref
会返回一个可变的响应式对象,该对象作为它的内部值——一个响应式的引用,这就是名称的来源。此对象只包含一个名为 value
的 property :
import { ref } from 'vue'const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
当 ref 作为渲染上下文 (从 setup() 中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动展开为内部值。不需要在模板中追加 .value
:
<template> <div> <span>{{ count }}</span> <button @click="count ++">Increment count</button> </div></template><script> import { ref } from 'vue' export default { setup() { const count = ref(0) return { count } } }</script>
当 ref
作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动展开内部值:
const count = ref(0)const state = reactive({ count})console.log(state.count) // 0state.count = 1console.log(count.value) // 1
如果将新的 ref 赋值给现有 ref 的 property,将会替换旧的 ref:
const otherCount = ref(2)state.count = otherCountconsole.log(state.count) // 2console.log(count.value) // 1
Ref 展开仅发生在被响应式 Object
嵌套的时候。当从 Array
或原生集合类型如 Map
访问 ref 时,不会进行展开:
const books = reactive([ref('Vue 3 Guide')])// 这里需要 .valueconsole.log(books[0].value)const map = reactive(new Map([['count', ref(0)]]))// 这里需要 .valueconsole.log(map.get('count').value)
当我们想使用大型响应式对象的一些 property 时,可能很想使用 ES6 解构来获取我们想要的 property:
import { reactive } from 'vue'const book = reactive({ author: 'Vue Team', year: '2020', title: 'Vue 3 Guide', description: 'You are reading this book right now ;)', price: 'free'})let { author, title } = book
遗憾的是,使用解构的两个 property 的响应性都会丢失。对于这种情况,我们需要将我们的响应式对象转换为一组 ref。这些 ref 将保留与源对象的响应式关联:
import { reactive, toRefs } from 'vue'const book = reactive({ author: 'Vue Team', year: '2020', title: 'Vue 3 Guide', description: 'You are reading this book right now ;)', price: 'free'})let { author, title } = toRefs(book)title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 refconsole.log(book.title) // 'Vue 3 Detailed Guide'
你可以在 Refs API 部分中了解更多有关 refs
的信息
readonly
防止更改响应式对象有时我们想跟踪响应式对象 (ref
或 reactive
) 的变化,但我们也希望防止在应用程序的某个位置更改它。例如,当我们有一个被 provide 的响应式对象时,我们不想让它在注入的时候被改变。为此,我们可以基于原始对象创建一个只读的 Proxy 对象:
import { reactive, readonly } from 'vue'const original = reactive({ count: 0 })const copy = readonly(original)// 在copy上转换original 会触发侦听器依赖original.count++// 转换copy 将导失败并导致警告copy.count++ // 警告: "Set operation on key 'count' failed: target is readonly."
本节使用单文件组件语法作为代码示例
有时我们需要依赖于其他状态的状态——在 Vue 中,这是用组件计算属性处理的,以直接创建计算值,我们可以使用 computed
方法:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象。
const count = ref(1)const plusOne = computed(() => count.value++)console.log(plusOne.value) // 2plusOne.value++ // error
或者,它可以使用一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
const count = ref(1)const plusOne = computed({ get: () => count.value + 1, set: val => { count.value = val - 1 }})plusOne.value = 1console.log(count.value) // 0
watchEffect
为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect
方法。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)watchEffect(() => console.log(count.value))// -> logs 0setTimeout(() => { count.value++ // -> logs 1}, 100)
当 watchEffect
在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
const stop = watchEffect(() => { /* ... */})// laterstop()
有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个 onInvalidate
函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
setup()
或生命周期钩子函数中使用了 watchEffect
,则在组件卸载时)watchEffect(onInvalidate => { const token = performAsyncOperation(id.value) onInvalidate(() => { // id has changed or watcher is stopped. // invalidate previously pending async operation token.cancel() })})
我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要。
在执行数据请求时,副作用函数往往是一个异步函数:
const data = ref(null)watchEffect(async onInvalidate => { onInvalidate(() => {...}) // 我们在Promise解析之前注册清除函数 data.value = await fetchData(props.id)})
我们知道异步函数都会隐式地返回一个 Promise,但是清理函数必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误。
Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。在核心的具体实现中,组件的 update
函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时,默认情况下,会在所有的组件 update
前执行:
<template> <div>{{ count }}</div></template><script> export default { setup() { const count = ref(0) watchEffect(() => { console.log(count.value) }) return { count } } }</script>
在这个例子中:
count
会在初始运行时同步打印出来count
时,将在组件更新前执行副作用。如果需要在组件更新后重新运行侦听器副作用,我们可以传递带有 flush
选项的附加 options
对象 (默认为 'pre'
):
// fire before component updateswatchEffect( () => { /* ... */ }, { flush: 'post' })
flush
选项还接受 sync
,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
onTrack
和 onTrigger
选项可用于调试侦听器的行为。
onTrack
将在响应式 property 或 ref 作为依赖项被追踪时被调用。onTrigger
将在依赖项变更导致副作用被触发时被调用。这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger
语句来检查依赖关系:
watchEffect( () => { /* 副作用 */ }, { onTrigger(e) { debugger } })
onTrack
和 onTrigger
只能在开发模式下工作。
watch
watch
API 完全等同于组件侦听器 property。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
watch
允许我们:侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref
:
// 侦听一个 getterconst state = reactive({ count: 0 })watch( () => state.count, (count, prevCount) => { /* ... */ })// 直接侦听refconst count = ref(0)watch(count, (count, prevCount) => { /* ... */})
侦听器还可以使用数组同时侦听多个源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */})
watchEffect
共享的行为watch
与 watchEffect
共享停止侦听,清除副作用 (相应地 onInvalidate
会作为回调的第三个参数传入)、副作用刷新时机和侦听器调试行为。
提示
在阅读文档之前,你应该已经熟悉了这两个 Vue 基础和创建组件。
在 Vue Mastery 上观看关于组合式 API 的免费视频。
通过创建 Vue 组件,我们可以将接口的可重复部分及其功能提取到可重用的代码段中。仅此一项就可以使我们的应用程序在可维护性和灵活性方面走得更远。然而,我们的经验已经证明,光靠这一点可能是不够的,尤其是当你的应用程序变得非常大的时候——想想几百个组件。在处理如此大的应用程序时,共享和重用代码变得尤为重要。
假设在我们的应用程序中,我们有一个视图来显示某个用户的仓库列表。除此之外,我们还希望应用搜索和筛选功能。处理此视图的组件可能如下所示:
// src/components/UserRepositories.vueexport default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String } }, data () { return { repositories: [], // 1 filters: { ... }, // 3 searchQuery: '' // 2 } }, computed: { filteredRepositories () { ... }, // 3 repositoriesMatchingSearchQuery () { ... }, // 2 }, watch: { user: 'getUserRepositories' // 1 }, methods: { getUserRepositories () { // 使用 `this.user` 获取用户仓库 }, // 1 updateFilters () { ... }, // 3 }, mounted () { this.getUserRepositories() // 1 }}
该组件有以下几个职责:
searchQuery
字符串搜索存储库filters
对象筛选仓库用组件的选项 (data
、computed
、methods
、watch
) 组织逻辑在大多数情况下都有效。然而,当我们的组件变得更大时,逻辑关注点的列表也会增长。这可能会导致组件难以阅读和理解,尤其是对于那些一开始就没有编写这些组件的人来说。
一个大型组件的示例,其中逻辑关注点是按颜色分组。
这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。
如果我们能够将与同一个逻辑关注点相关的代码配置在一起会更好。而这正是组合式 API 使我们能够做到的。
既然我们知道了为什么,我们就可以知道怎么做。为了开始使用组合式 API,我们首先需要一个可以实际使用它的地方。在 Vue 组件中,我们将此位置称为 setup
。
setup
组件选项新的 setup
组件选项在创建组件之前执行,一旦 props
被解析,并充当合成 API 的入口点。
WARNING
由于在执行 setup
时尚未创建组件实例,因此在 setup
选项中没有 this
。这意味着,除了 props
之外,你将无法访问组件中声明的任何属性——本地状态、计算属性或方法。
setup
选项应该是一个接受 props
和 context
的函数,我们将在稍后讨论。此外,我们从 setup
返回的所有内容都将暴露给组件的其余部分 (计算属性、方法、生命周期钩子等等) 以及组件的模板。
让我们添加 setup
到我们的组件中:
// src/components/UserRepositories.vueexport default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String } }, setup(props) { console.log(props) // { user: '' } return {} // 这里返回的任何内容都可以用于组件的其余部分 } // 组件的“其余部分”}
现在让我们从提取第一个逻辑关注点开始 (在原始代码段中标记为“1”)。
- 从假定的外部 API 获取该用户名的仓库,并在用户更改时刷新它
我们将从最明显的部分开始:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'// 在我们的组件内setup (props) { let repositories = [] const getUserRepositories = async () => { repositories = await fetchUserRepositories(props.user) } return { repositories, getUserRepositories // 返回的函数与方法的行为相同 }}
这是我们的出发点,但它还不能工作,因为我们的 repositories
变量是非响应式的。这意味着从用户的角度来看,仓库列表将保持为空。我们来解决这个问题!
ref
的响应式变量在 Vue 3.0 中,我们可以通过一个新的 ref
函数使任何响应式变量在任何地方起作用,如下所示:
import { ref } from 'vue'const counter = ref(0)
ref
接受参数并返回它包装在具有 value
property 的对象中,然后可以使用该 property 访问或更改响应式变量的值:
import { ref } from 'vue'const counter = ref(0)console.log(counter) // { value: 0 }console.log(counter.value) // 0counter.value++console.log(counter.value) // 1
在对象中包装值似乎不必要,但在 JavaScript 中保持不同数据类型的行为统一是必需的。这是因为在 JavaScript 中,Number
或 String
等基本类型是通过值传递的,而不是通过引用传递的:
在任何值周围都有一个包装器对象,这样我们就可以在整个应用程序中安全地传递它,而不必担心在某个地方失去它的响应性。
提示
换句话说,ref
对我们的值创建了一个响应式引用。使用引用的概念将在整个组合式 API 中经常使用。
回到我们的例子,让我们创建一个响应式的 repositories
变量:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'import { ref } from 'vue'// in our componentsetup (props) { const repositories = ref([]) const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(props.user) } return { repositories, getUserRepositories }}
完成!现在,每当我们调用 getUserRepositories
时,repositories
都将发生变化,视图将更新以反映更改。我们的组件现在应该如下所示:
// src/components/UserRepositories.vueimport { fetchUserRepositories } from '@/api/repositories'import { ref } from 'vue'export default { components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList }, props: { user: { type: String } }, setup (props) { const repositories = ref([]) const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(props.user) } return { repositories, getUserRepositories } }, data () { return { filters: { ... }, // 3 searchQuery: '' // 2 } }, computed: { filteredRepositories () { ... }, // 3 repositoriesMatchingSearchQuery () { ... }, // 2 }, watch: { user: 'getUserRepositories' // 1 }, methods: { updateFilters () { ... }, // 3 }, mounted () { this.getUserRepositories() // 1 }}
我们已经将第一个逻辑关注点中的几个部分移到了 setup
方法中,它们彼此非常接近。剩下的就是在 mounted
钩子中调用 getUserRepositories
,并设置一个监听器,以便在 user
prop 发生变化时执行此操作。
我们将从生命周期钩子开始。
setup
为了使组合式 API 的特性与选项式 API 相比更加完整,我们还需要一种在 setup
中注册生命周期钩子的方法。这要归功于从 Vue 导出的几个新函数。组合式 API 上的生命周期钩子与选项式 API 的名称相同,但前缀为 on
:即 mounted
看起来像 onMounted
。
这些函数接受在组件调用钩子时将执行的回调。
让我们将其添加到 setup
函数中:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'import { ref, onMounted } from 'vue'// in our componentsetup (props) { const repositories = ref([]) const getUserRepositories = async () => { repositories.value = await fetchUserRepositories(props.user) } onMounted(getUserRepositories) // on `mounted` call `getUserRepositories` return { repositories, getUserRepositories }}
现在我们需要对 user
prop 所做的更改做出反应。为此,我们将使用独立的 watch
函数。
watch
响应式更改就像我们如何使用 watch
选项在组件内的 user
property 上设置侦听器一样,我们也可以使用从 Vue 导入的 watch
函数执行相同的操作。它接受 3 个参数:
下面让我们快速了解一下它是如何工作的
import { ref, watch } from 'vue'const counter = ref(0)watch(counter, (newValue, oldValue) => { console.log('The new counter value is: ' + counter.value)})
例如,每当 counter
被修改时 counter.value=5
,watch 将触发并执行回调 (第二个参数),在本例中,它将把 'The new counter value is:5'
记录到我们的控制台中。
以下是等效的选项式 API:
export default { data() { return { counter: 0 } }, watch: { counter(newValue, oldValue) { console.log('The new counter value is: ' + this.counter) } }}
有关 watch
的详细信息,请参阅我们的深入指南。
现在我们将其应用到我们的示例中:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'import { ref, onMounted, watch, toRefs } from 'vue'// 在我们组件中setup (props) { // 使用 `toRefs` 创建对prop的 `user` property 的响应式引用 const { user } = toRefs(props) const repositories = ref([]) const getUserRepositories = async () => { // 更新 `prop.user` 到 `user.value` 访问引用值 repositories.value = await fetchUserRepositories(user.value) } onMounted(getUserRepositories) // 在用户 prop 的响应式引用上设置一个侦听器 watch(user, getUserRepositories) return { repositories, getUserRepositories }}
你可能已经注意到在我们的 setup
的顶部使用了 toRefs
。这是为了确保我们的侦听器能够对 user
prop 所做的更改做出反应。
有了这些变化,我们就把第一个逻辑关注点移到了一个地方。我们现在可以对第二个关注点执行相同的操作——基于 searchQuery
进行过滤,这次是使用计算属性。
computed
属性与 ref
和 watch
类似,也可以使用从 Vue 导入的 computed
函数在 Vue 组件外部创建计算属性。让我们回到我们的 counter 例子:
import { ref, computed } from 'vue'const counter = ref(0)const twiceTheCounter = computed(() => counter.value * 2)counter.value++console.log(counter.value) // 1console.log(twiceTheCounter.value) // 2
在这里,computed
函数返回一个作为 computed
的第一个参数传递的 getter 类回调的输出的一个只读的响应式引用。为了访问新创建的计算变量的 value,我们需要像使用 ref
一样使用 .value
property。
让我们将搜索功能移到 setup
中:
// src/components/UserRepositories.vue `setup` functionimport { fetchUserRepositories } from '@/api/repositories'import { ref, onMounted, watch, toRefs, computed } from 'vue'// in our componentsetup (props) { // 使用 `toRefs` 创建对 props 的 `user` property 的响应式引用 const { user } = toRefs(props) const repositories = ref([]) const getUserRepositories = async () => { // 更新 `props.user ` 到 `user.value` 访问引用值 repositories.value = await fetchUserRepositories(user.value) } onMounted(getUserRepositories) // 在用户 prop 的响应式引用上设置一个侦听器 watch(user, getUserRepositories) const searchQuery = ref('') const repositoriesMatchingSearchQuery = computed(() => { return repositories.value.filter( repository => repository.name.includes(searchQuery.value) ) }) return { repositories, getUserRepositories, searchQuery, repositoriesMatchingSearchQuery }}
对于其他的逻辑关注点我们也可以这样做,但是你可能已经在问这个问题了——这不就是把代码移到 setup
选项并使它变得非常大吗?嗯,那是真的。这就是为什么在继续其他任务之前,我们将首先将上述代码提取到一个独立的组合式函数。让我们从创建 useUserRepositories
开始:
// src/composables/useUserRepositories.jsimport { 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.jsimport { 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.vueimport 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 { // 因为我们并不关心未经过滤的仓库 // 我们可以在 `repositories` 名称下暴露过滤后的结果 repositories: repositoriesMatchingSearchQuery, getUserRepositories, searchQuery, } }, data () { return { filters: { ... }, // 3 } }, computed: { filteredRepositories () { ... }, // 3 }, methods: { updateFilters () { ... }, // 3 }}
此时,你可能已经知道了这个练习,所以让我们跳到最后,迁移剩余的过滤功能。我们不需要深入了解实现细节,因为这不是本指南的重点。
// src/components/UserRepositories.vueimport { toRefs } from 'vue'import useUserRepositories from '@/composables/useUserRepositories'import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'import useRepositoryFilters from '@/composables/useRepositoryFilters'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) const { filters, updateFilters, filteredRepositories } = useRepositoryFilters(repositoriesMatchingSearchQuery) return { // 因为我们并不关心未经过滤的仓库 // 我们可以在 `repositories` 名称下暴露过滤后的结果 repositories: filteredRepositories, getUserRepositories, searchQuery, filters, updateFilters } }}
我们完成了!
请记住,我们只触及了组合式 API 的表面以及它允许我们做什么。要了解更多信息,请参阅深入指南。
本节使用单文件组件代码示例的语法
本指南假定你已经阅读了组合式 API 简介和响应性原理。如果你不熟悉组合式 API,请先阅读这篇文章。
使用 setup
函数时,它将接受两个参数:
props
context
让我们更深入地研究如何使用每个参数。
setup
函数中的第一个参数是 props
。正如在一个标准组件中所期望的那样,setup
函数中的 props
是响应式的,当传入新的 prop 时,它将被更新。
// MyBook.vueexport default { props: { title: String }, setup(props) { console.log(props.title) }}
WARNING
但是,因为 props
是响应式的,你不能使用 ES6 解构,因为它会消除 prop 的响应性。
如果需要解构 prop,可以通过使用 setup
函数中的 toRefs
来安全地完成此操作。
// MyBook.vueimport { toRefs } from 'vue'setup(props) { const { title } = toRefs(props) console.log(title.value)}
传递给 setup
函数的第二个参数是 context
。context
是一个普通的 JavaScript 对象,它暴露三个组件的 property:
// MyBook.vueexport default { setup(props, context) { // Attribute (非响应式对象) console.log(context.attrs) // 插槽 (非响应式对象) console.log(context.slots) // 触发事件 (方法) console.log(context.emit) }}
context
是一个普通的 JavaScript 对象,也就是说,它不是响应式的,这意味着你可以安全地对 context
使用 ES6 解构。
// MyBook.vueexport default { setup(props, { attrs, slots, emit }) { ... }}
attrs
和 slots
是有状态的对象,它们总是会随组件本身的更新而更新。这意味着你应该避免对它们进行解构,并始终以 attrs.x
或 slots.x
的方式引用 property。请注意,与 props
不同,attrs
和 slots
是非响应式的。如果你打算根据 attrs
或 slots
更改应用副作用,那么应该在 onUpdated
生命周期钩子中执行此操作。
执行 setup
时,组件实例尚未被创建。因此,你只能访问以下 property:
props
attrs
slots
emit
换句话说,你将无法访问以下组件选项:
data
computed
methods
如果 setup
返回一个对象,则可以在组件的模板中像传递给 setup
的 props
property 一样访问该对象的 property:
<!-- MyBook.vue --><template> <div>{{ readersNumber }} {{ book.title }}</div></template><script> import { ref, reactive } from 'vue' export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // expose to template return { readersNumber, book } } }</script>
注意,从 setup
返回的 refs 在模板中访问时是被自动解开的,因此不应在模板中使用 .value
。
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:
// MyBook.vueimport { h, ref, reactive } from 'vue'export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // Please note that we need to explicitly expose ref value here return () => h('div', [readersNumber.value, book.title]) }}
this
在 setup()
内部,this
不会是该活跃实例的引用,因为 setup()
是在解析其它组件选项之前被调用的,所以 setup()
内部的 this
的行为与其它选项中的 this
完全不同。这在和其它选项式 API 一起使用 setup()
时可能会导致混淆。
本指南假定你已经阅读了 组合式 API 简介和响应性基础。如果你不熟悉组合式 API,请先阅读这篇文章。
在 Vue Mastery 上观看关于生命周期钩子的免费视频
你可以通过在生命周期钩子前面加上 “on” 来访问组件的生命周期钩子。
下表包含如何在 setup () 内部调用生命周期钩子:
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
TIP
因为 setup
是围绕 beforeCreate
和 created
生命周期钩子运行的,所以不需要显式地定义它们。换句话说,在这些钩子中编写的任何代码都应该直接在 setup
函数中编写。
这些函数接受一个回调函数,当钩子被组件调用时将会被执行:
// MyBook.vueexport default { setup() { // mounted onMounted(() => { console.log('Component is mounted!') }) }}
本指南假定你已经阅读了 Provide / Inject、组合式 API 介绍和响应性基础。如果你不熟悉组合式 API,请先阅读这篇文章。
我们也可以在组合式 API 中使用 provide/inject。两者都只能在当前活动实例的 setup()
期间调用。
假设我们要重写以下代码,其中包含一个 MyMap
组件,该组件使用组合式 API 为 MyMarker
组件提供用户的位置。
<!-- 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', 'geolocation']}</script>
在 setup()
中使用 provide
时,我们首先从 vue
显式导入 provide
方法。这使我们能够调用 provide
时来定义每个 property。
provide
函数允许你通过两个参数定义 property:
<String>
类型)使用 MyMap
组件,我们提供的值可以按如下方式重构:
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { provide('location', 'North Pole') provide('geolocation', { longitude: 90, latitude: 135 }) }}</script>
在 setup()
中使用 inject
时,还需要从 vue
显式导入它。一旦我们这样做了,我们就可以调用它来定义如何将它暴露给我们的组件。
inject
函数有两个参数:
使用 MyMarker
组件,可以使用以下代码对其进行重构:
<!-- src/components/MyMarker.vue --><script>import { inject } from 'vue'export default { setup() { const userLocation = inject('location', 'The Universe') const userGeolocation = inject('geolocation') return { userLocation, userGeolocation } }}</script>
为了增加提供值和注入值之间的响应性,我们可以在提供值时使用 ref 或 reactive。
使用 MyMap
组件,我们的代码可以更新如下:
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide, reactive, ref } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { const location = ref('North Pole') const geolocation = reactive({ longitude: 90, latitude: 135 }) provide('location', location) provide('geolocation', geolocation) }}</script>
现在,如果这两个 property 中有任何更改,MyMarker
组件也将自动更新!
当使用响应式提供/注入值时,建议尽可能,在*提供者*内保持响应式 property 的任何更改。
例如,在需要更改用户位置的情况下,我们最好在 MyMap
组件中执行此操作。
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide, reactive, ref } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { const location = ref('North Pole') const geolocation = reactive({ longitude: 90, latitude: 135 }) provide('location', location) provide('geolocation', geolocation) return { location } }, methods: { updateLocation() { this.location = 'South Pole' } }}</script>
然而,有时我们需要在注入数据的组件内部更新注入的数据。在这种情况下,我们建议提供一个方法来负责改变响应式 property。
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide, reactive, ref } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { const location = ref('North Pole') const geolocation = reactive({ longitude: 90, latitude: 135 }) const updateLocation = () => { location.value = 'South Pole' } provide('location', location) provide('geolocation', 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>
最后,如果要确保通过 provide
传递的数据不会被注入的组件更改,我们建议对提供者的 property 使用 readonly
。
<!-- src/components/MyMap.vue --><template> <MyMarker /></template><script>import { provide, reactive, readonly, ref } from 'vue'import MyMarker from './MyMarker.vueexport default { components: { MyMarker }, setup() { const location = ref('North Pole') const geolocation = reactive({ longitude: 90, latitude: 135 }) const updateLocation = () => { location.value = 'South Pole' } provide('location', readonly(location)) provide('geolocation', readonly(geolocation)) provide('updateLocation', updateLocation) }}</script>
本节代码示例使用单文件组件的语法
本指南假定你已经阅读了组合式 API 简介和响应性基础。如果你不熟悉组合式 API,请先阅读此文章。
在使用组合式 API 时,响应式引用和模板引用的概念是统一的。为了获得对模板内元素或组件实例的引用,我们可以像往常一样声明 ref 并从 setup() 返回:
<template> <div ref="root">This is a root element</div></template><script> import { ref, onMounted } from 'vue' export default { setup() { const root = ref(null) onMounted(() => { // DOM元素将在初始渲染后分配给ref console.log(root.value) // <div>这是根元素</div> }) return { root } } }</script>
这里我们在渲染上下文中暴露 root
,并通过 ref="root"
,将其绑定到 div 作为其 ref。在虚拟 DOM 补丁算法中,如果 VNode 的 ref
键对应于渲染上下文中的 ref,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
作为模板使用的 ref 的行为与任何其他 ref 一样:它们是响应式的,可以传递到 (或从中返回) 复合函数中。
export default { setup() { const root = ref(null) return () => h('div', { ref: root }) // with JSX return () => <div ref={root} /> }}
v-for
中的用法组合式 API 模板引用在 v-for
内部使用时没有特殊处理。相反,请使用函数引用执行自定义处理:
<template> <div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }"> {{ item }} </div></template><script> import { ref, reactive, onBeforeUpdate } from 'vue' export default { setup() { const list = reactive([1, 2, 3]) const divs = ref([]) // 确保在每次更新之前重置ref onBeforeUpdate(() => { divs.value = [] }) return { list, divs } } }</script>
为了学习如何更好地使用 Vue,不需要阅读本页,但是它提供了更多信息,如果你想知道渲染在背后是如何工作的。
现在我们知道了侦听器是如何更新组件的,你可能会问这些更改最终是如何应用到 DOM 中的!也许你以前听说过虚拟 DOM,包括 Vue 在内的许多框架都使用这种方式来确保我们的接口能够有效地反映我们在 JavaScript 中更新的更改
我们用 JavaScript 复制了一个名为 Virtual Dom 的 DOM,我们这样做是因为用 JavaScript 接触 DOM 的计算成本很高。虽然用 JavaScript 执行更新很廉价,但是找到所需的 DOM 节点并用 JS 更新它们的成本很高。所以我们批处理调用,同时更改 DOM。
虚拟 DOM 是轻量级的 JavaScript 对象,由渲染函数创建。它包含三个参数:元素,带有数据的对象,prop,attr 以及更多,和一个数组。数组是我们传递子级的地方,子级也具有所有这些参数,然后它们可以具有子级,依此类推,直到我们构建完整的元素树为止。
如果需要更新列表项,可以使用前面提到的响应性在 JavaScript 中进行。然后,我们对 JavaScript 副本,虚拟 DOM 进行所有更改,并在此与实际 DOM 之间进行区分。只有这样,我们才能对已更改的内容进行更新。虚拟 DOM 允许我们对 UI 进行高效的更新!
该页面仅适用于 Vue 2.x 及更低版本,并假定你已经阅读了响应性部分。请先阅读该部分。
由于 JavaScript 的限制,有些 Vue 无法检测的更改类型。但是,有一些方法可以规避它们以维持响应性。
Vue 无法检测到 property 的添加或删除。由于 Vue 在实例初始化期间执行 getter/setter 转换过程,因此必须在 data
对象中存在一个 property,以便 Vue 对其进行转换并使其具有响应式。例如:
var vm = new Vue({ data: { a: 1 }})// `vm.a` 现在是响应式的vm.b = 2// `vm.b` 不是响应式的
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object,propertyName,value)
方法向嵌套对象添加响应式 property:
Vue.set(vm.someObject, 'b', 2)
你还可以使用 vm.$set
实例方法,这也是全局 Vue.set
方法的别名:
this.$set(this.someObject, 'b', 2)
有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign()
或 _.extend()
。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
// 而不是 `Object.assign(this.someObject, { a: 1, b: 2 })`this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
Vue 不能检测以下数组的变动:
vm.items[indexOfItem] = newValue
vm.items.length = newLength
例如:
var vm = new Vue({ data: { items: ['a', 'b', 'c'] }})vm.items[1] = 'x' // 不是响应式的vm.items.length = 2 // 不是响应式的
为了解决第一种问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue
相同的效果,同时也将在响应性系统内触发状态更新:
// Vue.setVue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splicevm.items.splice(indexOfItem, 1, newValue)
你也可以使用 vm.$set
实例方法,该方法是全局方法 Vue.set
的一个别名:
vm.$set(vm.items, indexOfItem, newValue)
为了解决第二种问题,你可以使用 splice
:
vm.items.splice(newLength)
由于 Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值:
var vm = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '<div>{{ message }}</div>'})// 之后设置 `message`vm.message = 'Hello!'
如果你未在 data 选项中声明 message
,Vue 将警告你渲染函数正在试图访问不存在的 property。
这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使组件实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data
对象就像组件的状态结构 (schema)。提前声明所有的响应式 property,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
例如,当你设置 vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)
。这样回调函数将在 DOM 更新完成后被调用。例如:
<div id="example">{{ message }}</div>
var vm = new Vue({ el: '#example', data: { message: '123' }})vm.message = 'new message' // 更改数据vm.$el.textContent === 'new message' // falseVue.nextTick(function() { vm.$el.textContent === 'new message' // true})
在组件内使用 vm.$nextTick()
实例方法特别方便,因为它不需要全局 Vue
,并且回调函数中的 this
将自动绑定到当前的组件实例上:
Vue.component('example', { template: '<span>{{ message }}</span>', data: function() { return { message: 'not updated' } }, methods: { updateMessage: function() { this.message = 'updated' console.log(this.$el.textContent) // => 'not updated' this.$nextTick(function() { console.log(this.$el.textContent) // => 'updated' }) } }})
因为 $nextTick()
返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: { updateMessage: async function () { this.message = 'updated' console.log(this.$el.textContent) // => 'not updated' await this.$nextTick() console.log(this.$el.textContent) // => 'updated' } }
在很多 Vue 项目中,我们使用 app.component
来定义全局组件,紧接着用 app.mount('#app')
在每个页面内指定一个容器元素。
这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:
所有这些都可以通过扩展名为 .vue
的 single-file components (单文件组件) 来解决,并且还可以使用 webpack 或 Browserify 等构建工具。
这是一个文件名为 Hello.vue
的简单实例:
现在我们获得:
正如我们说过的,我们可以使用预处理器来构建简洁和功能更丰富的组件,比如 Pug,Babel (with ES2015 modules),和 Stylus。
这些特定的语言只是例子,你可以只是简单地使用 Babel,TypeScript,SCSS,PostCSS 或者其他任何能够帮助你提高生产力的预处理器。如果搭配 vue-loader
使用 webpack,它也能为 CSS Modules 提供头等支持。
一个重要的事情值得注意,关注点分离不等于文件类型分离。在现代 UI 开发中,我们已经发现相比于把代码库分离成三个大的层次并将其相互交织起来,把它们划分为松散耦合的组件再将其组合起来更合理一些。在一个组件里,其模板、逻辑和样式是内部耦合的,并且把他们搭配在一起实际上使得组件更加内聚且更可维护。
即便你不喜欢单文件组件,你仍然可以把 JavaScript、CSS 分离成独立的文件然后做到热重载和预编译。
<!-- my-component.vue --><template> <div>This will be pre-compiled</div></template><script src="./my-component.js"></script><style src="./my-component.css"></style>
如果你希望深入了解并开始使用单文件组件,请来 CodeSandbox 看看这个简单的 todo 应用。
有了 .vue
组件,我们就进入了高阶 JavaScript 应用领域。如果你没有准备好的话,意味着还需要学会使用一些附加的工具:
在你花一天时间了解这些资源之后,我们建议你参考 Vue CLI。只要遵循指示,你就能很快地运行一个带有 .vue
组件、ES2015、webpack 和热重载 (hot-reloading) 的 Vue 项目!
CLI 会为你搞定大多数工具的配置问题,同时也支持细粒度自定义配置项。
有时你会想从零搭建你自己的构建工具,这时你需要通过 Vue Loader 手动配置 webpack。关于学习更多 webpack 的内容,请查阅其官方文档和 Webpack Academy。
当构建可靠的应用时,测试在个人或团队构建新特性、重构代码、修复 bug 等工作中扮演了关键的角色。尽管测试的流派有很多,它们在 web 应用这个领域里主要有三大类:
本章节致力于引导大家了解测试的生态系统的并为 Vue 应用或组件库选择适合的工具。
单元测试允许你将独立单元的代码进行隔离测试,其目的是为开发者提供对代码的信心。通过编写细致且有意义的测试,你能够有信心在构建新特性或重构已有代码的同时,保持应用的功能和稳定。
为一个 Vue 应用做单元测试并没有和为其它类型的应用做测试有什么明显的区别。
因为单元测试的建议通常是框架无关的,所以下面只是当你在评估应用的单元测试工具时需要的一些基本指引。
当测试失败时,提供有用的错误信息对于单元测试框架来说至关重要。这是断言库应尽的职责。一个具有高质量错误信息的断言能够最小化调试问题所需的时间。除了简单地告诉你什么测试失败了,断言库还应额外提供上下文以及测试失败的原因,例如预期结果 vs 实际得到的结果。
一些诸如 Jest 这样的单元测试框架会包含断言库。另一些诸如 Mocha 需要你单独安装断言库 (通常会用 Chai)。
因为主流的单元测试框架都是开源的,所以对于一些旨在长期维护其测试且确保项目本身保持活跃的团队来说,拥有一个活跃的社区是至关重要的。额外的好处是,在任何时候遇到问题时,一个活跃的社区会为你提供更多的支持。
尽管生态系统里有很多工具,这里我们列出一些在 Vue 生态系统中常用的单元测试工具。
Jest 是一个专注于简易性的 JavaScript 测试框架。一个其独特的功能是可以为测试生成快照 (snapshot),以提供另一种验证应用单元的方法。
资料:
Mocha 是一个专注于灵活性的 JavaScript 测试框架。因为其灵活性,它允许你选择不同的库来满足诸如侦听 (如 Sinon) 和断言 (如 Chai) 等其它常见的功能。另一个 Mocha 独特的功能是它不止可以在 Node.js 里运行测试,还可以在浏览器里运行测试。
资料:
测试大多数 Vue 组件时都必须将它们挂载到 DOM (虚拟或真实) 上,才能完全断言它们正在工作。这是另一个与框架无关的概念。因此组件测试框架的诞生,是为了让用户能够以可靠的方式完成这项工作,同时还提供了 Vue 特有的诸如对 Vuex、Vue Router 和其他 Vue 插件的集成的便利性。
以下章节提供了在评估最适合你的应用的组件测试框架时需要记住的事项。
毋容置疑,最重要的标准之一就是组件测试库应该尽可能与 Vue 生态系统兼容。虽然这看起来很全面,但需要记住的一些关键集成领域包括单文件组件 (SFC)、Vuex、Vue Router 以及应用所依赖的任何其他特定于 Vue 的插件。
当测试失败时,提供有用的错误日志以最小化调试问题所需的时间对于组件测试框架来说至关重要。除了简单地告诉你什么测试失败了,他们还应额外提供上下文以及测试失败的原因,例如预期结果 vs 实际得到的结果。
Vue Testing Library 是一组专注于测试组件而不依赖实现细节的工具。由于在设计时就充分考虑了可访问性,它采用的方案也使重构变得轻而易举。
它的指导原则是,与软件使用方式相似的测试越多,它们提供的可信度就越高。
资料:
Vue Test Utils 是官方的偏底层的组件测试库,它是为用户提供对 Vue 特定 API 的访问而编写的。如果你对测试 Vue 应用不熟悉,我们建议你使用 Vue Testing Library,它是 Vue Test Utils 的抽象。
资源:
虽然单元测试为开发者提供了一定程度的信心,但是单元测试和组件测试在部署到生产环境时提供应用整体覆盖的能力是有限的。因此,端到端测试可以说从应用最重要的方面进行测试覆盖:当用户实际使用应用时会发生什么。
换句话说,端到端测试验证应用中的所有层。这不仅包括你的前端代码,还包括所有相关的后端服务和基础设施,它们更能代表你的用户所处的环境。通过测试用户操作如何影响应用,端到端测试通常是提高应用是否正常运行的信心的关键。
虽然 web 上的端到端测试因不可信赖 (片面的) 测试和减慢开发过程而得到负面的声誉,但现代端到端工具在创建更可靠的、交互的和实用的测试方面取得了长足进步。在选择端到端测试框架时,以下章节在你为应用选择测试框架时提供了一些指导。
端到端测试的一个主要优点是它能够跨多个浏览器测试应用。尽管 100% 的跨浏览器覆盖看上去很诱人,但需要注意的是,因为持续运行这些跨浏览器测试需要额外的时间和机器消耗,它会降低团队的资源回报。因此,在选择应用需要的跨浏览器测试数量时,必须注意这种权衡。
TIP
针对浏览器特定问题的一个最新进展是,针对不常用的浏览器 (如:< IE11、旧版 Safari 等) 使用应用监视和错误报告工具 (如:Sentry、LogRocket 等)。
端到端测试和开发的主要问题之一是运行整个套件需要很长时间。通常,这只在持续集成和部署 (CI/CD) 管道中完成。现代的端到端测试框架通过添加类似并行化的特性来帮助解决这个问题,这使得 CI/CD 管道的运行速度通常比以前快。此外,在本地开发时,有选择地为正在处理的页面运行单个测试的能力,同时还提供测试的热重载,将有助于提高开发者的工作流程和工作效率。
虽然开发者传统上依赖于在终端窗口中扫描日志来帮助确定测试中出了什么问题,但现代端到端测试框架允许开发者利用他们已经熟悉的工具,例如浏览器开发工具。
虽然生态系统中有许多工具,但以下是一些 Vue.js 生态系统中常用的端到端测试框架。
Cypress.io 是一个测试框架,旨在通过使开发者能够可靠地测试他们的应用,同时提供一流的开发者体验,来提高开发者的生产率。
资料:
Nightwatch.js 是一个端到端测试框架,可用于测试 web 应用和网站,以及 Node.js 单元测试和集成测试。
资料:
Puppeteer 是一个 Node.js 库,它提供高阶 API 来控制浏览器,并可以与其他测试运行程序 (例如 Jest) 配对来测试应用。
资料:
TestCafe 是一个基于端到端的 Node.js 框架,旨在提供简单的设置,以便开发者能够专注于创建易于编写和可靠的测试。
资料:
Vue CLI 提供内置的 TypeScript 工具支持。
随着应用的增长,静态类型系统可以帮助防止许多潜在的运行时错误,这就是为什么 Vue 3 是用 TypeScript 编写的。这意味着在 Vue 中使用 TypeScript 不需要任何其他工具——它具有一流的公民支持。
// tsconfig.json{ "compilerOptions": { "target": "esnext", "module": "esnext", // 这样就可以对 `this` 上的数据属性进行更严格的推断` "strict": true, "jsx": "preserve", "moduleResolution": "node" }}
请注意,必须包含 strict: true
(或至少包含 noImplicitThis: true
,它是 strict
标志的一部分) 才能在组件方法中利用 this
的类型检查,否则它总是被视为 any
类型。
参见 TypeScript 编译选项文档查看更多细节。
Vue CLI 可以生成使用 TypeScript 的新项目,开始:
# 1. Install Vue CLI, 如果尚未安装npm install --global @vue/cli@next# 2. 创建一个新项目, 选择 "Manually select features" 选项vue create my-project-name# 3. 如果已经有一个不存在TypeScript的 Vue CLI项目,请添加适当的 Vue CLI插件:vue add typescript
请确保组件的 script
部分已将语言设置为 TypeScript:
<script lang="ts"> ...</script>
对于使用 TypeScript 开发 Vue 应用程序,我们强烈建议使用 Visual Studio Code,它为 TypeScript 提供了很好的开箱即用支持。如果你使用的是单文件组件 (SFCs),那么可以使用很棒的 Vetur extension,它在 SFCs 中提供了 TypeScript 推理和许多其他优秀的特性。
WebStorm 还为 TypeScript 和 Vue 提供现成的支持。
要让 TypeScript 正确推断 Vue 组件选项中的类型,需要使用 defineComponent
全局方法定义组件:
import { defineComponent } from 'vue'const Component = defineComponent({ // 已启用类型推断})
TypeScript 应该能够在不显式定义类型的情况下推断大多数类型。例如,如果有一个具有数字 count
property 的组件,如果试图对其调用特定于字符串的方法,则会出现错误:
const Component = defineComponent({ data() { return { count: 0 } }, mounted() { const result = this.count.split('') // => Property 'split' does not exist on type 'number' }})
如果你有一个复杂的类型或接口,你可以使用 type assertion 对其进行强制转换:
interface Book { title: string author: string year: number}const Component = defineComponent({ data() { return { book: { title: 'Vue 3 Guide', author: 'Vue Team', year: 2020 } as Book } }})
由于 Vue 声明文件的循环特性,TypeScript 可能难以推断 computed 的类型。因此,你可能需要注释返回类型的计算属性。
import { defineComponent } from 'vue'const Component = defineComponent({ data() { return { message: 'Hello!' } }, computed: { // 需要注释 greeting(): string { return this.message + '!' } // 在使用setter进行计算时,需要对getter进行注释 greetingUppercased: { get(): string { return this.greeting.toUpperCase(); }, set(newValue: string) { this.message = newValue.toUpperCase(); }, }, }})
Vue 对定义了 type
的 prop 执行运行时验证。要将这些类型提供给 TypeScript,我们需要使用 PropType
强制转换构造函数:
import { defineComponent, PropType } from 'vue'interface ComplexMessage { title: string okMessage: string cancelMessage: string}const Component = defineComponent({ props: { name: String, success: { type: String }, callback: { type: Function as PropType<() => void> }, message: { type: Object as PropType<ComplexMessage>, required: true, validator(message: ComplexMessage) { return !!message.title } } }})
如果你发现验证器没有得到类型推断或者成员完成不起作用,那么用期望的类型注释参数可能有助于解决这些问题。
在 setup()
函数中,不需要将类型传递给 props
参数,因为它将从 props
组件选项推断类型。
import { defineComponent } from 'vue'const Component = defineComponent({ props: { message: { type: String, required: true } }, setup(props) { const result = props.message.split('') // 正确, 'message' 被声明为字符串 const filtered = props.message.filter(p => p.value) // 将引发错误: Property 'filter' does not exist on type 'string' }})
ref
Refs 根据初始值推断类型:
import { defineComponent, ref } from 'vue'const Component = defineComponent({ setup() { const year = ref(2020) const result = year.value.split('') // => Property 'split' does not exist on type 'number' }})
有时我们可能需要为 ref 的内部值指定复杂类型。我们可以在调用 ref 重写默认推理时简单地传递一个泛型参数:
const year = ref<string | number>('2020') // year's type: Ref<string | number>year.value = 2020 // ok!
TIP
如果泛型的类型未知,建议将 ref
转换为 Ref<T>
。
reactive
当声明类型 reactive
property,我们可以使用接口:
import { defineComponent, reactive } from 'vue'interface Book { title: string year?: number}export default defineComponent({ name: 'HelloWorld', setup() { const book = reactive<Book>({ title: 'Vue 3 Guide' }) // or const book: Book = reactive({ title: 'Vue 3 Guide' }) // or const book = reactive({ title: 'Vue 3 Guide' }) as Book }})
computed
计算值将根据返回值自动推断类型
import { defineComponent, ref, computed } from 'vue'export default defineComponent({ name: 'CounterButton', setup() { let count = ref(0) // 只读 const doubleCount = computed(() => count.value * 2) const result = doubleCount.value.split('') // => Property 'split' does not exist on type 'number' }})
虽然 Vue.js 本身并不支持移动应用开发,但是有很多解决方案可以用 Vue.js 创建原生 iOS 和 Android 应用。
Capacitor 是一个来自 Ionic Team 的项目,通过提供跨多个平台运行的 API,开发者可以使用单个代码库构建原生 iOS、Android 和 PWA 应用。
资源
NativeScript 使用已熟悉的 Web 技能为跨平台(真正的原生)移动应用提供支持。两者结合在一起是开发沉浸式移动体验的绝佳选择。
资源
对于大多数单页面应用,都推荐使用官方支持的 vue-router 库。更多细节可以移步 vue-router 文档。
如果你只需要非常简单的路由而不想引入一个功能完整的路由库,可以像这样动态渲染一个页面级的组件:
const NotFoundComponent = { template: '<p>Page not found</p>' }const HomeComponent = { template: '<p>Home page</p>' }const AboutComponent = { template: '<p>About page</p>' }const routes = { '/': HomeComponent, '/about': AboutComponent}const SimpleRouter = { data: () => ({ currentRoute: window.location.pathname }), computed: { CurrentComponent() { return routes[this.currentRoute] || NotFoundComponent } }, render() { return Vue.h(this.CurrentComponent) }}Vue.createApp(SimpleRouter).mount('#app')
结合 HTML5 History API,你可以建立一个麻雀虽小但是五脏俱全的客户端路由器。可以直接看实例应用。
如果你有更偏爱的第三方路由,如 Page.js 或者 Director,整合起来也一样简单。这里有一个使用了 Page.js 的完整示例。
由于状态零散地分布在许多组件和组件之间的交互中,大型应用复杂度也经常逐渐增长。为了解决这个问题,Vue 提供 vuex:我们有受到 Elm 启发的状态管理库。vuex 甚至集成到 vue-devtools,无需配置即可进行时光旅行调试 (time travel debugging)。
如果你是来自 React 的开发者,你可能会对 Vuex 和 Redux 间的差异表示关注,Redux 是 React 生态环境中最流行的 Flux 实现。Redux 事实上无法感知视图层,所以它能够轻松的通过一些[简单绑定_blank_nofollow](https://classic.yarnpkg.com/en/packages?q=redux vue&p=1)和 Vue 一起使用。Vuex 区别在于它是一个专门为 Vue 应用所设计。这使得它能够更好地和 Vue 进行整合,同时提供简洁的 API 和改善过的开发体验。
经常被忽略的是,Vue 应用中响应式 data
对象的实际来源——当访问数据对象时,一个组件实例只是简单的代理访问。所以,如果你有一处需要被多个实例间共享的状态,你可以使用一个 reactive 方法让对象作为响应式对象。
const sourceOfTruth = Vue.reactive({ message: 'Hello'})const appA = Vue.createApp({ data() { return sourceOfTruth }}).mount('#app-a')const appB = Vue.createApp({ data() { return sourceOfTruth }}).mount('#app-b')
<div id="app-a">App A: {{ message }}</div><div id="app-b">App B: {{ message }}</div>
现在当 sourceOfTruth
发生变更,appA
和 appB
都将自动地更新它们的视图。我们现在只有一个真实来源,但调试将是一场噩梦。我们应用的任何部分都可以随时更改任何数据,而不会留下变更过的记录。
const appB = Vue.createApp({ data() { return sourceOfTruth }, mounted() { sourceOfTruth.message = 'Goodbye' // both apps will render 'Goodbye' message now }}).mount('#app-b')
为了解决这个问题,我们采用一个简单的 store 模式:
const store = { debug: true, state: Vue.reactive({ message: 'Hello!' }), setMessageAction(newValue) { if (this.debug) { console.log('setMessageAction triggered with', newValue) } this.state.message = newValue }, clearMessageAction() { if (this.debug) { console.log('clearMessageAction triggered') } this.state.message = '' }}
需要注意,所有 store 中 state 的变更,都放置在 store 自身的 action 中去管理。这种集中式状态管理能够被更容易地理解哪种类型的变更将会发生,以及它们是如何被触发。当错误出现时,我们现在也会有一个 log 记录 bug 之前发生了什么。
此外,每个实例/组件仍然可以拥有和管理自己的私有状态:
<div id="app-a">{{sharedState.message}}</div><div id="app-b">{{sharedState.message}}</div>
const appA = Vue.createApp({ data() { return { privateState: {}, sharedState: store.state } }, mounted() { store.setMessageAction('Goodbye!') }}).mount('#app-a')const appB = Vue.createApp({ data() { return { privateState: {}, sharedState: store.state } }}).mount('#app-b')
TIP
重要的是,注意你不应该在 action 中替换原始的状态对象——组件和 store 需要引用同一个共享对象,变更才能够被观察到。
接着我们继续延伸约定,组件不允许直接变更属于 store 实例的 state,而应执行 action 来分发 (dispatch) 事件通知 store 去改变,我们最终达成了 Flux 架构。这样约定的好处是,我们能够记录所有 store 中发生的 state 变更,同时实现能做到记录变更、保存状态快照、历史回滚/时光旅行的先进的调试工具。
说了一圈其实又回到了 Vuex,如果你已经读到这儿,或许可以去尝试一下!
我们创建了一份完整的构建 Vue 服务端渲染应用的指南。这份指南非常深入,适合已经熟悉 Vue、webpack 和 Node.js 开发的开发者阅读。请移步 ssr.vuejs.org。
从头搭建一个服务端渲染的应用是相当复杂的。幸运的是,我们有一个优秀的社区项目 Nuxt.js 让这一切变得非常简单。Nuxt 是一个基于 Vue 生态的更高层的框架,为开发服务端渲染的 Vue 应用提供了极其便利的开发体验。更酷的是,你甚至可以用它来做为静态站生成器。推荐尝试。
Quasar Framework 可以通过其一流的构建系统、合理的配置和开发者扩展性生成 (可选地和 PWA 互通的) SSR 应用,让你的想法的设计和构建变得轻而易举。你可以在服务端挑选执行超过上百款遵循“Material Design 2.0”的组件,并在浏览器端可用。你甚至可以管理网站的 <meta>
标签。Quasar 是一个基于 Node.js 和 webpack 的开发环境,它可以通过一套代码完成 SPA、PWA、SSR、Electron、Capacitor 和 Cordova 应用的快速开发。
Web 可访问性 (也称为 a11y) 是指创建可供任何人使用的网站的实践方式——无论是身患某种障碍、通过慢速的网络连接访问、使用老旧或损坏的硬件,还是仅仅是处于不利环境中的人。例如,在视频中添加字幕可以帮助失聪、重听或在嘈杂的环境中听不到手机的用户。同样,请确保文字对比度不要太低,这对低视力用户和那些试图在强光下使用手机的用户都有帮助。
你是否已经准备开始却又无从下手?
可以先看看由万维网联盟 (W3C) 提供的规划和管理 web 可访问性。
你应该在每个页面的顶部添加一个直接指向主内容区域的链接,这样用户就可以跳过在多个网页上重复的内容。
通常这个链接会放在 App.vue
的顶部,这样它就会是所有页面上的第一个可聚焦元素:
<ul class="skip-links"> <li> <a href="#main" ref="skipLink">跳到主内容</a> </li></ul>
若想在非聚焦状态下隐藏该链接,可以添加以下样式:
.skipLink { white-space: nowrap; margin: 1em auto; top: 0; position: fixed; left: 50%; margin-left: -72px; opacity: 0;}.skipLink:focus { opacity: 1; background-color: white; padding: .5em; border: 1px solid black;}
一旦用户改变路由,请将焦点放回到这个跳过链接。通过用如下方式聚焦 ref
即可实现:
<script>export default { watch: { $route() { this.$refs.skipLink.focus(); } }};</script>
可访问性最重要的部分之一是确保设计本身是可访问的。设计不仅要考虑颜色对比度、字体选择、文本大小和语言,还要考虑应用程序中内容的结构。
用户可以通过标题在应用程序中进行导航。为应用程序的每个部分设置描述性标题可以让用户更容易地预测每个部分的内容。说到标题,有几个推荐的可访问性实践:
<h1>
- <h6>
<main role="main" aria-labelledby="main-title"> <h1 id="main-title">Main title</h1> <section aria-labelledby="section-title"> <h2 id="section-title"> Section Title </h2> <h3>Section Subtitle</h3> <!-- 内容 --> </section> <section aria-labelledby="section-title"> <h2 id="section-title"> Section Title </h2> <h3>Section Subtitle</h3> <!-- 内容 --> <h3>Section Subtitle</h3> <!-- 内容 --> </section></main>
地标 (landmark) 会为应用中的章节提供访问规划。依赖辅助技术的用户可以跳过内容直接导航到应用程序的每个部分。你可以使用 ARIA role 帮助你实现这个目标。
HTML | ARIA Role | 地标的目的 |
---|---|---|
header | role="banner" | 主标题:页面的标题 |
nav | role="navigation" | 适合用作文档或相关文档导航的链接集合 |
main | role="main" | 文档的主体或中心内容 |
footer | role="contentinfo" | 关于父级文档的信息:脚注/版权/隐私声明链接 |
aside | role="complementary" | 用来支持主内容,同时其自身的内容是相对独立且有意义的 |
无对应元素 | role="search" | 该章节包含整个应用的搜索功能 |
form | role="form" | 表单相关元素的集合 |
section | role="region" | 相关的且用户可能会导航到的内容。必须为该元素提供 label |
Tip:
在使用地标 HTML 元素时,建议加上冗余的地标 role attribute,以最大限度地与传统不支持 HTML5 语义元素的浏览器兼容。
当创建一个表单,你可能使用到以下几个元素:<form>
、<label>
、<input>
、<textarea>
和 <button>
。
标签通常放置在表单字段的顶部或左侧:
<form action="/dataCollectionLocation" method="post" autocomplete="on"> <div v-for="item in formItems" :key="item.id" class="form-item"> <label :for="item.id">{{ item.label }}: </label> <input :type="item.type" :id="item.id" :name="item.id" v-model="item.value" /> </div> <button type="submit">Submit</button></form>
注意如何在表单元素中包含 autocomplete='on'
,它将应用于表单中的所有输入。你也可以为每个输入设置不同的自动完成属性的值。
提供标签以描述所有表单控件的用途;链接 for
和 id
:
<label for="name">Name</label><input type="text" name="name" id="name" v-model="name" />
如果你在 chrome 开发工具中检查这个元素,并打开 Elements 选项卡中的 Accessibility 选项卡,你将看到输入是如何从标签中获取其名称的:
警告:
虽然你可能已经看到这样包装输入字段的标签:
<label> Name: <input type="text" name="name" id="name" v-model="name" /></label>
辅助技术更好地支持用匹配的 id 显式设置标签。
你也可以给输入一个带有aria-label
的可访问名称。
<label for="name">Name</label><input type="text" name="name" id="name" v-model="name" :aria-label="nameLabel"/>
请随意在 Chrome DevTools 中检查此元素,以查看可访问名称是如何更改的:
使用 aria-labelledby
类似于 aria-label
,除非标签文本在屏幕上可见。它通过 id
与其他元素配对,你可以链接多个 id
:
<form class="demo" action="/dataCollectionLocation" method="post" autocomplete="on"> <h1 id="billing">Billing</h1> <div class="form-item"> <label for="name">Name:</label> <input type="text" name="name" id="name" v-model="name" aria-labelledby="billing name" /> </div> <button type="submit">Submit</button></form>
aria-describedby 的用法与 aria-labelledby
相同,预期提供了用户可能需要的附加信息的描述。这可用于描述任何输入的标准:
<form class="demo" action="/dataCollectionLocation" method="post" autocomplete="on"> <h1 id="billing">Billing</h1> <div class="form-item"> <label for="name">Full Name:</label> <input type="text" name="name" id="name" v-model="name" aria-labelledby="billing name" aria-describedby="nameDescription" /> <p id="nameDescription">Please provide first and last name.</p> </div> <button type="submit">Submit</button></form>
你可以通过使用 Chrome 开发工具来查看说明:
避免使用占位符,因为它们可能会混淆许多用户。
占位符的一个问题是默认情况下它们不符合颜色对比标准;修复颜色对比度会使占位符看起来像输入字段中预填充的数据。查看以下示例,可以看到满足颜色对比度条件的姓氏占位符看起来像预填充的数据:
最好提供用户在任何输入之外填写表单所需的所有信息。
为输入字段添加说明时,请确保将其正确链接到输入。你可以提供附加指令并在 aria-labelledby
内绑定多个 id。这使得设计更加灵活。
<fieldset> <legend>Using aria-labelledby</legend> <label id="date-label" for="date">Current Date:</label> <input type="date" name="date" id="date" aria-labelledby="date-label date-instructions" /> <p id="date-instructions">MM/DD/YYYY</p></fieldset>
或者,你可以用 aria-describedby
将指令附加到输入。
<fieldset> <legend>Using aria-describedby</legend> <label id="dob" for="dob">Date of Birth:</label> <input type="date" name="dob" id="dob" aria-describedby="dob-instructions" /> <p id="dob-instructions">MM/DD/YYYY</p></fieldset>
通常不建议直观地隐藏标签,即使输入具有可访问的名称。但是,如果输入的功能可以与周围的内容一起理解,那么我们可以隐藏视觉标签。
让我们看看这个搜索字段:
<form role="search"> <label for="search" class="hidden-visually">Search: </label> <input type="text" name="search" id="search" v-model="search" /> <button type="submit">Search</button></form>
我们可以这样做,因为搜索按钮将帮助可视化用户识别输入字段的用途。
我们可以使用 CSS 直观地隐藏元素,但可以将它们用于辅助技术:
.hidden-visually { position: absolute; overflow: hidden; white-space: nowrap; margin: 0; padding: 0; height: 1px; width: 1px; clip: rect(0 0 0 0); clip-path: inset(100%);}
添加 aria hidden="true"
将隐藏辅助技术中的元素,但使其在视觉上对其他用户可用。不要把它用在可聚焦的元素上,纯粹用于装饰性的、复制的或屏幕外的内容上。
<p>This is not hidden from screen readers.</p><p aria-hidden="true">This is hidden from screen readers.</p>
在表单中使用按钮时,必须设置类型以防止提交表单。
也可以使用输入创建按钮:
<form action="/dataCollectionLocation" method="post" autocomplete="on"> <!-- Buttons --> <button type="button">Cancel</button> <button type="submit">Submit</button> <!-- Input buttons --> <input type="button" value="Cancel" /> <input type="submit" value="Submit" /></form>
你可以使用此技术创建功能图像。
<form role="search"> <label for="search" class="hidden-visually">Search: </label> <input type="text" name="search" id="search" v-model="search" /> <input type="image" class="btnImg" src="https://img.icons8.com/search" rel="external nofollow" alt="Search" /> </form>
<form role="search"> <label for="searchIcon" class="hidden-visually">Search: </label> <input type="text" name="searchIcon" id="searchIcon" v-model="searchIcon" /> <button type="submit"> <i class="fas fa-search" aria-hidden="true"></i> <span class="hidden-visually">Search</span> </button></form>
万维网联盟 (W3C) 网络可访问性倡议 (WAI) 为不同的组件制定了 Web 可访问性标准:
WCAG 2.1 在 WCAG 2.0 上进行了扩展,允许通过处理 web 的变化来实现新技术。W3C 鼓励在开发或更新 Web 可访问性策略时使用 WCAG 的最新版本。
W3C 的 WAI-ARIA 为如何构建动态内容和高阶用户界面控件提供了指导。
世界卫生组织估计,世界 15%的人口患有某种形式的残疾,其中 2 - 4%的人严重残疾。估计全世界有 10 亿人,使残疾人成为世界上最大的少数群体。
残疾的种类繁多,大致可分为四类:
请从 WebAim 查看以下链接,以便用户了解:
INFO
刚接触 Vue.js?先从基础指南开始吧。
本指南主要是为有 Vue 2 经验的用户希望了解 Vue 3 的新功能和更改而提供的。在试用 Vue 3 之前,你不必从头阅读这些内容。虽然看起来有很多变化,但很多你已经了解和喜欢 Vue 的部分仍是一样的。不过我们希望尽可能全面,并为每处变化提供详细的例子。
开始学习 Vue 3 Vue Mastery。
<script src="https://unpkg.com/vue@next" rel="external nofollow" ></script>
npm init vite-app hello-vue3 # OR yarn create vite-app hello-vue3
npm install -g @vue/cli # OR yarn global add @vue/cli vue create hello-vue3 # select vue 3 preset
Vue 3 中需要关注的一些新功能包括:
createRenderer
API 来自 @vue/runtime-core
创建自定义渲染器提示
我们仍在开发 Vue 3 的专用迁移版本,该版本的行为与 Vue 2 兼容,运行时警告不兼容。如果你计划迁移一个非常重要的 Vue 2 应用程序,我们强烈建议你等待迁移版本完成以获得更流畅的体验。
下面列出了从 2.x 开始的重大更改:
v-model
用法已更改<template>
v-for和非 - v-for 节点上 key 用法已更改v-if
和 v-for
优先级已更改v-bind="object"
现在排序敏感v-for
中的 ref
不再注册 ref 数组$scopedSlots
property 已删除,所有插槽都通过 $slots
作为函数暴露v-enter
-> v-enter-from
v-leave
-> v-leave-from
$watch
不再支持点分隔字符串路径,请改用计算函数作为参数outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。VUE3.x 现在使用应用程序容器的 innerHTML
。destroyed
生命周期选项被重命名为 unmounted
beforeDestroy
生命周期选项被重命名为 beforeUnmount
default
工厂函数不再有权访问 this
是上下文data
应始终声明为函数data
选项现在可简单地合并$watch
不再支持以点分隔的字符串路径。请改用计算属性函数作为参数。<template>
没有特殊指令的标记 (v-if/else-if/else
、v-for
或 v-slot
) 现在被视为普通元素,并将生成原生的 <template>
元素,而不是渲染其内部内容。outerHTML
将替换为根组件模板 (如果根组件没有模板/渲染选项,则最终编译为模板)。Vue 3.x 现在使用应用容器的 innerHTML
,这意味着容器本身不再被视为模板的一部分。keyCode
支持作为 v-on
的修饰符$destroy
实例方法。用户不应再手动管理单个 Vue 组件的生命周期。我们所有的官方库和工具现在都支持 Vue 3,但大多数仍然处于 beta 状态,并在 npm 的 next
dist 标签下发布。我们正计划在 2020 年底前稳定所有项目,并将其转换为使用 latest
的 dist 标签。
从 v4.5.0 开始,vue-cli
现在提供了内置选项,可在创建新项目时选择 Vue 3 预设。现在可以升级 vue-cli
并运行 vue create
来创建 Vue 3 项目。
Vue Router 4.0 提供了 Vue 3 支持,并有许多突破性的变化,查看 README 中完整的细节,
Vuex 4.0 提供了 Vue 3 支持,其 API 与 3.x 基本相同。唯一的突破性变化是插件的安装方式。
我们正在开发一个新版本的 Devtools,它有一个新的 UI 和经过重构的内部结构,以支持多个 Vue 版本。新版本目前处于测试阶段,目前只支持 Vue 3。Vuex 和路由器的集成也在进行中。
.xpi
文件)推荐使用 VSCode 和我们官方拓展 Vetur,它为 Vue 3 提供了全面的 IDE 支持
项目 | npm | 仓库 |
---|---|---|
@vue/babel-plugin-jsx | [Github] | |
eslint-plugin-vue | [Github] | |
@vue/test-utils | [Github] | |
vue-class-component | [Github] | |
vue-loader | [Github] | |
rollup-plugin-vue | [Github] |
在 Vue 2 中,在 v-for
里使用的 ref
attribute 会用 ref 数组填充相应的 $refs
property。当存在嵌套的 v-for
时,这种行为会变得不明确且效率低下。
在 Vue 3 中,这样的用法将不再在 $ref
中自动创建数组。要从单个绑定获取多个 ref,请将 ref
绑定到一个更灵活的函数上 (这是一个新特性):
<div v-for="item in list" :ref="setItemRef"></div>
结合选项式 API:
export default { data() { return { itemRefs: [] } }, methods: { setItemRef(el) { this.itemRefs.push(el) } }, beforeUpdate() { this.itemRefs = [] }, updated() { console.log(this.itemRefs) }}
结合组合式 API:
import { ref, onBeforeUpdate, onUpdated } from 'vue'export default { setup() { let itemRefs = [] const setItemRef = el => { itemRefs.push(el) } onBeforeUpdate(() => { itemRefs = [] }) onUpdated(() => { console.log(itemRefs) }) return { itemRefs, setItemRef } }}
注意:
itemRefs
不必是数组:它也可以是一个对象,其 ref 会通过迭代的 key 被设置。itemRef
也可以是响应式的且可以被监听。以下是对变化的高层次概述:
defineAsyncComponent
助手方法,用于显式地定义异步组件component
选项重命名为 loader
resolve
和 reject
参数,且必须返回一个 Promise如需更深入的解释,请继续阅读!
以前,异步组件是通过将组件定义为返回 Promise 的函数来创建的,例如:
const asyncPage = () => import('./NextPage.vue')
或者,对于带有选项的更高阶的组件语法:
const asyncPage = { component: () => import('./NextPage.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 asyncPage = defineAsyncComponent(() => import('./NextPage.vue'))// 带选项的异步组件const asyncPageWithOptions = defineAsyncComponent({ loader: () => import('./NextPage.vue'), delay: 200, timeout: 3000, errorComponent: ErrorComponent, loadingComponent: LoadingComponent})
对 2.x 所做的另一个更改是,component
选项现在被重命名为 loader
,以便准确地传达不能直接提供组件定义的信息。
import { defineAsyncComponent } from 'vue'const asyncPageWithOptions = defineAsyncComponent({ loader: () => import('./NextPage.vue'), delay: 200, timeout: 3000, error: ErrorComponent, loading: LoadingComponent})
此外,与 2.x 不同,loader 函数不再接收 resolve
和 reject
参数,且必须始终返回 Promise。
// 2.x 版本const oldAsyncComponent = (resolve, reject) => { /* ... */}// 3.x 版本const asyncComponent = defineAsyncComponent( () => new Promise((resolve, reject) => { /* ... */ }))
有关异步组件用法的详细信息,请参阅:
信息
这是一个低级的内部 API 更改,不会影响大多数开发人员。
下面是对这些变化的高层次总结:
false
。相反,它被设置为 attr=“false”。移除 attribute,使用 null
或者 undefined
。如需更深入的解释,请继续阅读!
在 2.x,我们有以下策略来强制 v-bind
的值:
value
的 ,
,``,等等。falsy
的,Vue 会移除它们 (undefined
,null
or false
) 另外加上它们 (见这里和这里)。contenteditable
,draggable
和 spellcheck
),Vue 会尝试强制将它们串起来 (目前对 contenteditable
做了特殊处理,修复 vuejs/vue#9397)。falsy
值 (undefined
,null
,or false
) 并按原样设置其他值 (见这里)。下表描述了 Vue 如何使用普通非布尔 attribute 强制“枚举 attribute”:
绑定表达式 | foo 正常 | draggable 枚举 |
---|---|---|
:attr="null" | / | draggable="false" |
:attr="undefined" | / | / |
:attr="true" | foo="true" | draggable="true" |
:attr="false" | / | draggable="false" |
:attr="0" | foo="0" | draggable="true" |
attr="" | foo="" | draggable="true" |
attr="foo" | foo="foo" | draggable="true" |
attr | foo="" | draggable="true" |
从上表可以看出,当前实现 true
强制为 'true'
但如果 attribute 为 false
,则移除该 attribute。这也导致了不一致性,并要求用户在非常常见的用例中手动强制布尔值为字符串,例如 aria-*
attribute 像 aria-selected
,aria-hidden
,等等。
我们打算放弃“枚举 attribute”的内部概念,并将它们视为普通的非布尔 HTML attribute。
'true'
和 'false'
以外的值,甚至可以使用 contenteditable
等 attribute 的关键字`对于非布尔 attribute,如果 attribute 为 false
,Vue 将停止删除它们,相反强制它们为 'false'
。
true
和 false
之间的不一致性,并使输出 aria-*
attributes 更容易下表描述了新行为:
绑定表达式 | foo 正常 | draggable 枚举 |
---|---|---|
:attr="null" | / | / † |
:attr="undefined" | / | / |
:attr="true" | foo="true" | draggable="true" |
:attr="false" | foo="false" † | draggable="false" |
:attr="0" | foo="0" | draggable="0" † |
attr="" | foo="" | draggable="" † |
attr="foo" | foo="foo" | draggable="foo" † |
attr | foo="" | draggable="" † |
†: 变更
布尔 attributes 的强制保持不变。
缺少枚举 attribute 和 attr="false"
可能会产生不同的 IDL attribute 值 (将反映实际状态),描述如下:
缺少枚举attr | IDL attr & 值 |
---|---|
contenteditable | contentEditable → 'inherit' |
draggable | draggable → false |
spellcheck | spellcheck → true |
为了保持原有的行为,并且我们将强制使用 false
为 'false'
,在 3.x Vue 中,开发人员需要将 v-bind
表达式解析为 false
或 'false'
,表示 contenteditable
和 spellcheck
。
在 2.x 中,枚举 attribute 的无效值被强制为 'true'
。这通常是无意的,不太可能大规模依赖。在 3.x 中,应显式指定 true
或 'true'
。
false
强制为 'false'
而不是删除 attribute在 3.x,null
或 undefined
应用于显式删除 attribute。
Attributes | v-bind value 2.x | v-bind value 3.x | HTML 输出 |
---|---|---|---|
2.x “枚举attribute” i.e. contenteditable , draggable and spellcheck . | undefined , false | undefined , null | removed |
true , 'true' , '' , 1 , 'foo' | true , 'true' | "true" | |
null , 'false' | false , 'false' | "false" | |
其他非布尔attribute eg. aria-checked , tabindex , alt , etc. | undefined , null , false | undefined , null | removed |
'false' | false , 'false' | "false" |
下面是对变更的简要总结:
v-bind="$attrs"
更多信息,请继续阅读!
在 Vue 2,自定义指令是通过使用下面列出的钩子来创建的,这些钩子都是可选的
下面是一个例子:
<p v-highlight="yellow">高亮显示此文本亮黄色</p>
Vue.directive('highlight', { bind(el, binding, vnode) { el.style.background = binding.value }})
在这里,在这个元素的初始设置中,指令通过传递一个值来绑定样式,该值可以通过应用程序更新为不同的值。
然而,在 Vue 3 中,我们为自定义指令创建了一个更具凝聚力的 API。正如你所看到的,它们与我们的组件生命周期方法有很大的不同,即使我们正与类似的事件钩子,我们现在把它们统一起来了:
updated
。最终 API 如下:
const MyDirective = { beforeMount(el, binding, vnode, prevVnode) {}, mounted() {}, beforeUpdate() {}, updated() {}, beforeUnmount() {}, // 新 unmounted() {}}
生成的 API 可以这样使用,与前面的示例相同:
<p v-highlight="yellow">高亮显示此文本亮黄色</p>
const app = Vue.createApp({})app.directive('highlight', { beforeMount(el, binding, vnode) { el.style.background = binding.value }})
既然定制指令生命周期钩子映射了组件本身的那些,那么它们就更容易推理和记住了!
通常建议保持指令独立于它们所使用的组件实例。从自定义指令中访问实例通常表明该指令本身应该是一个组件。然而,在某些情况下,这实际上是有意义的。
在Vue 2中,必须通过vnode
参数访问组件实例:
bind(el, binding, vnode) { const vm = vnode.context}
在Vue 3中,实例现在是bind
的一部分:
mounted(el, binding, vnode) { const vm = binding.instance}
在 Vue 3 中,我们现在支持片段,这允许我们为每个组件返回多个 DOM 节点。你可以想象,对于具有多个 <li>
的组件或一个表的子元素这样的组件有多方便:
<template> <li>Hello</li> <li>Vue</li> <li>Devs!</li></template>
如此灵活,我们可能会遇到一个定制指令的问题,它可能有多个根节点。
因此,自定义指令现在作为虚拟 DOM 节点数据的一部分包含在内。当在组件上使用自定义指令时,钩子作为无关的 prop 传递到组件,并以 this.$attrs
结束。
这也意味着可以像这样在模板中直接挂接到元素的生命周期中,这在涉及到自定义指令时非常方便:
<div @vnodeMounted="myHook" />
这与属性 fallthrough 行为是一致的,因此,当子组件在内部元素上使用 v-bind="$attrs"
时,它也将应用对其使用的任何自定义指令。
is
prop 用法仅限于保留的 <component>
标记。v-is
指令来支持 2.x 用例,其中在原生元素上使用了 v-is
来处理原生 HTML 解析限制。如果我们想添加在 Vue 外部定义的自定义元素 (例如使用 Web 组件 API),我们需要“指示”Vue 将其视为自定义元素。让我们以下面的模板为例。
<plastic-button></plastic-button>
在 Vue 2.x 中,将标记作为自定义元素白名单是通过 Vue.config.ignoredElements
:
// 这将使Vue忽略在Vue外部定义的自定义元素// (例如:使用 Web Components API)Vue.config.ignoredElements = ['plastic-button']
在 Vue 3.0 中,此检查在模板编译期间执行指示编译器将 <plastic-button>
视为自定义元素:
isCustomElement
传递给 Vue 模板编译器,如果使用 vue-loader
,则应通过 vue-loader
的 compilerOptions
选项传递: // webpack 中的配置 rules: [ { test: /.vue$/, use: 'vue-loader', options: { compilerOptions: { isCustomElement: tag => tag === 'plastic-button' } } } // ... ]
app.config.isCustomElement
传递: const app = Vue.createApp({}) app.config.isCustomElement = tag => tag === 'plastic-button'
需要注意的是,运行时配置只会影响运行时模板编译——它不会影响预编译的模板。
自定义元素规范提供了一种将自定义元素用作自定义内置模板的方法,方法是向内置元素添加 is
属性:
<button is="plastic-button">点击我!</button>
Vue 对 is
特殊 prop 的使用是在模拟 native attribute 在浏览器中普遍可用之前的作用。但是,在 2.x 中,它被解释为渲染一个名为 plastic-button
的 Vue 组件,这将阻止上面提到的自定义内置元素的原生使用。
在 3.0 中,我们仅将 Vue 对 is
属性的特殊处理限制到 <component>
tag。
<component>
tag 上使用时,它的行为将与 2.x 中完全相同; <foo is="bar" />
bar
组件。is
prop 渲染 foo
组件。is
选项传递给 createElement
调用,并作为原生 attribute 渲染,这支持使用自定义的内置元素。 <button is="plastic-button">点击我!</button>
plastic-button
组件。 document.createElement('button', { is: 'plastic-button' })
v-is
用于 DOM 内模板解析解决方案提示:本节仅影响直接在页面的 HTML 中写入 Vue 模板的情况。 在 DOM 模板中使用时,模板受原生 HTML 解析规则的约束。一些 HTML 元素,例如
<ul&
,<ol&
,<table&
和<select&
对它们内部可以出现的元素有限制,和一些像<li&
,<tr&
,和<option&
只能出现在某些其他元素中。
在 Vue 2 中,我们建议通过在原生 tag 上使用 is
prop 来解决这些限制:
<table> <tr is="blog-post-row"></tr></table>
随着 is
的行为变化,我们引入了一个新的指令 v-is
,用于解决这些情况:
<table> <tr v-is="'blog-post-row'"></tr></table>
WARNING
v-is
函数像一个动态的 2.x :is
绑定——因此,要按注册名称渲染组件,其值应为 JavaScript 字符串文本:
<!-- 不正确,不会渲染任何内容 --><tr v-is="blog-post-row"></tr><!-- 正确 --><tr v-is="'blog-post-row'"></tr>
config.ignoredElements
与 vue-loader
的 compilerOptions
(使用 build 步骤) 或 app.config.isCustomElement
(使用动态模板编译)<component>
tags 与 is
用法更改为 <component is="...">
(对于 SFC 模板) 或 v-is
(对于 DOM 模板)。data
组件选项声明不再接收纯 JavaScript object
,而需要 function
声明。当合并来自 mixin 或 extend 的多个 data
返回值时,现在是浅层次合并的而不是深层次合并的(只合并根级属性)。
在 2.x 中,开发者可以定义 data
选项是 object
或者是 function
。
例如:
<!-- 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
。
使用上面的示例,代码只有一个可能的实现:
<script> import { createApp } from 'vue' createApp({ data() { return { apiKey: 'a1b2c3' } } }).mount('#app')</script>
此外,当来自组件的 data()
及其 mixin 或 extends 基类被合并时,现在将浅层次执行合并:
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 }}
对于依赖对象声明的用户,我们建议:
data
中的 property对于依赖 mixin 的深度合并行为的用户,我们建议重构代码以完全避免这种依赖,因为 mixin 的深度合并非常隐式,这让代码逻辑更难理解和调试。
$on
,$off
和 $once
实例方法已被移除,应用实例不再实现事件触发接口。
在 2.x 中,Vue 实例可用于触发通过事件触发 API 强制附加的处理程序 ($on
,$off
和 $once
),这用于创建 event hub,以创建在整个应用程序中使用的全局事件侦听器:
// eventHub.jsconst eventHub = new Vue()export default eventHub
// ChildComponent.vueimport eventHub from './eventHub'export default { mounted() { // 添加 eventHub listener eventHub.$on('custom-event', () => { console.log('Custom event triggered!') }) }, beforeDestroy() { // 移除 eventHub listener eventHub.$off('custom-event') }}
// ParentComponent.vueimport eventHub from './eventHub'export default { methods: { callGlobalCustomEvent() { eventHub.$emit('custom-event') // 如果ChildComponent mounted,控制台中将显示一条消息 } }}
我们整个从实例中移除了 $on
,$off
和 $once
方法,$emit
仍然是现有 API 的一部分,因为它用于触发由父组件以声明方式附加的事件处理程序
例如,可以通过使用实现事件发射器接口的外部库来替换现有的 event hub mitt。
在兼容性构建中也可以支持这些方法。
从 Vue 3.0 开始,过滤器已删除,不再支持。
在 2.x,开发者可以使用过滤器来处理通用文本格式。
例如:
<template> <h1>Bank Account Balance</h1> <p>{{ accountBalance | currencyUSD }}</p></template><script> export default { props: { accountBalance: { type: Number, required: true } }, filters: { currencyUSD(value) { return '$' + value } } }</script>
虽然这看起来很方便,但它需要一个自定义语法,打破大括号内表达式是“只是 JavaScript”的假设,这不仅有学习成本,而且有实现成本。
在 3.x 中,过滤器已删除,不再支持。相反地,我们建议用方法调用或计算属性替换它们。
使用上面的例子,这里是一个如何实现它的例子。
<template> <h1>Bank Account Balance</h1> <p>{{ accountInUSD }}</p></template><script> export default { props: { accountBalance: { type: Number, required: true } }, computed: { accountInUSD() { return '$' + this.accountBalance } } }</script>
我们建议用计算属性或方法代替过滤器,而不是使用过滤器。
如果在应用中全局注册了过滤器,那么在每个组件中用计算属性或方法调用来替换它可能就没那么方便了。
相反地,你可以通过全局属性在所有组件中使用它:
// main.jsconst app = createApp(App)app.config.globalProperties.$filters = { currencyUSD(value) { return '$' + value }}
然后,你可以通过 $filters
对象修改所有的模板,像下面这样:
<template> <h1>Bank Account Balance</h1> <p>{{ $filters.currencyUSD(accountBalance) }}</p></template>
注意,这种方式只能用于方法中,不可以在计算属性中使用,因为后者只有在单个组件的上下文中定义时才有意义。
在 Vue 3 中,组件现在正式支持多根节点组件,即片段!
在 2.x 中,不支持多根组件,当用户意外创建多根组件时会发出警告,因此,为了修复此错误,许多组件被包装在一个 <div>
中。
<!-- Layout.vue --><template> <div> <header>...</header> <main>...</main> <footer>...</footer> </div></template>
在 3.x 中,组件现在可以有多个根节点!但是,这确实要求开发者明确定义属性应该分布在哪里。
<!-- Layout.vue --><template> <header>...</header> <main v-bind="$attrs">...</main> <footer>...</footer></template>
有关 attribute 继承如何工作的详细信息,见非 Prop Attributes。
就变化而言,属于高等级内容:
props
和 context
的普通函数创建 (即:slots
,attrs
,emit
)。functional
attribute 在单文件组件 (SFC) <template>
已被移除{ functional: true }
选项在通过函数创建组件已被移除更多信息,请继续阅读!
在 Vue 2 中,函数式组件有两个主要用例:
然而,在 Vue 3 中,有状态组件的性能已经提高到可以忽略不计的程度。此外,有状态组件现在还包括返回多个根节点的能力。
因此,函数式组件剩下的唯一用例就是简单组件,比如创建动态标题的组件。否则,建议你像平常一样使用有状态组件。
使用 <dynamic-heading>
组件,负责提供适当的标题 (即:h1
,h2
,h3
,等等),在 2.x 中,这可能是作为单个文件组件编写的:
// Vue 2 函数式组件示例export default { functional: true, props: ['level'], render(h, { props, data, children }) { return h(`h${props.level}`, data, children) }}
或者,对于喜欢在单个文件组件中使用 <template>
的用户:
// Vue 2 函数式组件示例使用 <template><template functional> <component :is="`h${props.level}`" v-bind="attrs" v-on="listeners" /></template><script>export default { props: ['level']}</script>
现在在 Vue 3 中,所有的函数式组件都是用普通函数创建的,换句话说,不需要定义 { functional: true }
组件选项。
他们将接收两个参数:props
和 context
。context
参数是一个对象,包含组件的 attrs
,slots
,和 emit
property。
此外,现在不是在 render
函数中隐式提供 h
,而是全局导入 h
。
使用前面提到的 <dynamic-heading>
组件的示例,下面是它现在的样子。
import { h } from 'vue'const DynamicHeading = (props, context) => { return h(`h${props.level}`, context.attrs, context.slots)}DynamicHeading.props = ['level']export default DynamicHeading
在 3.x 中,有状态组件和函数式组件之间的性能差异已经大大减少,并且在大多数用例中是微不足道的。因此,在 SFCs 上使用 functional
的开发人员的迁移路径是删除该 attribute,并将 props
的所有引用重命名为 $props
,将 attrs
重命名为 $attrs
。
使用之前的 <dynamic-heading>
示例,下面是它现在的样子。
<template> <component v-bind:is="`h${$props.level}`" v-bind="$attrs" /></template><script>export default { props: ['level']}</script>
主要的区别在于:
functional
attribute 在 <template>
中移除listeners
现在作为 $attrs
的一部分传递,可以将其删除有关新函数式组件的用法和对渲染函数的更改的详细信息,见:
Vue 2.x 有许多全局 API 和配置,这些 API 和配置可以全局改变 Vue 的行为。例如,要创建全局组件,可以使用 Vue.component
这样的 API:
Vue.component('button-counter', { data: () => ({ count: 0 }), template: '<button @click="count++">Clicked {{ count }} times.</button>'})
类似地,使用全局指令的声明方式如下:
Vue.directive('focus', { inserted: el => el.focus()})
虽然这种声明方式很方便,但它也会导致一些问题。从技术上讲,Vue 2 没有“app”的概念,我们定义的应用只是通过 new Vue()
创建的根 Vue 实例。从同一个 Vue 构造函数创建的每个根实例共享相同的全局配置,因此:
Vue.config.errorHandler
)。有些 API 像 Vue.use
以及 Vue.mixin
甚至连恢复效果的方法都没有,这使得涉及插件的测试特别棘手。实际上,vue-test-utils 必须实现一个特殊的 API createLocalVue
来处理此问题:import { createLocalVue, mount } from '@vue/test-utils'// 建扩展的 `Vue` 构造函数const localVue = createLocalVue()// 在 “local” Vue构造函数上 “全局” 安装插件localVue.use(MyPlugin)// 通过 `localVue` 来挂载选项mount(Component, { localVue })
// 这会影响两个根实例 Vue.mixin({ /* ... */ }) const app1 = new Vue({ el: '#app-1' }) const app2 = new Vue({ el: '#app-2' })
为了避免这些问题,在 Vue 3 中我们引入...
createApp
调用 createApp
返回一个应用实例,这是 Vue 3 中的新概念:
import { createApp } from 'vue'const app = createApp({})
应用实例暴露当前全局 API 的子集,经验法则是,任何全局改变 Vue 行为的 API 现在都会移动到应用实例上,以下是当前全局 API 及其相应实例 API 的表:
2.x 全局 API | 3.x 实例 API (app ) |
---|---|
Vue.config | app.config |
Vue.config.productionTip | removed (见下方) |
Vue.config.ignoredElements | app.config.isCustomElement (见下方) |
Vue.component | app.component |
Vue.directive | app.directive |
Vue.mixin | app.mixin |
Vue.use | app.use (见下方) |
所有其他不全局改变行为的全局 API 现在被命名为 exports,文档见全局 API Treeshaking。
config.productionTip
移除在 Vue 3.x 中,“使用生产版本”提示仅在使用“dev + full build”(包含运行时编译器并有警告的构建) 时才会显示。
对于 ES 模块构建,由于它们是与 bundler 一起使用的,而且在大多数情况下,CLI 或样板已经正确地配置了生产环境,所以本技巧将不再出现。
config.ignoredElements
替换为 config.isCustomElement
引入此配置选项的目的是支持原生自定义元素,因此重命名可以更好地传达它的功能,新选项还需要一个比旧的 string/RegExp 方法提供更多灵活性的函数:
// beforeVue.config.ignoredElements = ['my-el', /^ion-/]// afterconst app = Vue.createApp({})app.config.isCustomElement = tag => tag.startsWith('ion-')
重要
在 3.0 中,元素是否是组件的检查已转移到模板编译阶段,因此只有在使用运行时编译器时才考虑此配置选项。如果你使用的是 runtime-only 版本 isCustomElement
必须通过 @vue/compiler-dom
在构建步骤替换——比如,通过 compilerOptions
option in vue-loader。
config.isCustomElement
当使用仅运行时构建时时,将发出警告,指示用户在生成设置中传递该选项;插件开发者通常使用 Vue.use
。例如,官方的 vue-router
插件是如何在浏览器环境中自行安装的:
var inBrowser = typeof window !== 'undefined'/* … */if (inBrowser && window.Vue) { window.Vue.use(VueRouter)}
由于 use
全局 API 在 Vue 3 中不再使用,此方法将停止工作并停止调用 Vue.use()
现在将触发警告,于是,开发者必须在应用程序实例上显式指定使用此插件:
const app = createApp(MyApp)app.use(VueRouter)
使用 createApp(/* options */)
初始化后,应用实例 app
可用于挂载具有 app.mount(domTarget)
:
import { createApp } from 'vue'import MyApp from './MyApp.vue'const app = createApp(MyApp)app.mount('#app')
经过所有这些更改,我们在指南开头的组件和指令将被改写为如下内容:
const app = createApp(MyApp)app.component('button-counter', { data: () => ({ count: 0 }), template: '<button @click="count++">Clicked {{ count }} times.</button>'})app.directive('focus', { mounted: el => el.focus()})// 现在所有应用实例都挂载了,与其组件树一起,将具有相同的 “button-counter” 组件 和 “focus” 指令不污染全局环境app.mount('#app')
与在 2.x 根实例中使用 provide
选项类似,Vue 3 应用实例还可以提供可由应用内的任何组件注入的依赖项:
// 在入口app.provide('guide', 'Vue 3 Guide')// 在子组件export default { inject: { book: { from: 'guide' } }, template: `<div>{{ book }}</div>`}
在应用之间共享配置 (如组件或指令) 的一种方法是创建工厂功能,如下所示:
import { createApp } from 'vue'import Foo from './Foo.vue'import Bar from './Bar.vue'const createMyApp = options => { const app = createApp(options) app.directive('focus' /* ... */) return app}createMyApp(Foo).mount('#foo')createMyApp(Bar).mount('#bar')
现在,Foo 和 Bar 实例及其后代中都可以使用 focus
指令。
如果你曾经在 Vue 中手动操作过 DOM,你可能会遇到以下模式:
import Vue from 'vue'Vue.nextTick(() => { // 一些和DOM有关的东西})
或者,如果你一直在对涉及 async components 的应用程序进行单元测试,那么很可能你编写了以下内容:
import { shallowMount } from '@vue/test-utils'import { MyComponent } from './MyComponent.vue'test('an async feature', async () => { const wrapper = shallowMount(MyComponent) // 执行一些DOM相关的任务 await wrapper.vm.$nextTick() // 运行你的断言})
Vue.nextTick()
是一个全局的 API 直接暴露在单个 Vue 对象上——事实上,实例方法 $nextTick()
只是一个方便的包装 Vue.nextTick()
为方便起见,回调的 this
上下文自动绑定到当前实例。
模块捆绑程序,如 webpack 支持 tree-shaking,这是“死代码消除”的一个花哨术语。不幸的是,由于代码是如何在以前的 Vue 版本中编写的,全局 API Vue.nextTick()
不可摇动,将包含在最终捆绑中不管它们实际在哪里使用。
在 Vue 3 中,全局和内部 API 都经过了重构,并考虑到了 tree-shaking 的支持。因此,全局 API 现在只能作为 ES 模块构建的命名导出进行访问。例如,我们之前的片段现在应该如下所示:
import { nextTick } from 'vue'nextTick(() => { // 一些和DOM有关的东西})
and
import { shallowMount } from '@vue/test-utils'import { MyComponent } from './MyComponent.vue'import { nextTick } from 'vue'test('an async feature', async () => { const wrapper = shallowMount(MyComponent) // 执行一些DOM相关的任务 await nextTick() // 运行你的断言})
直接调用 Vue.nextTick()
将导致臭名昭著的 undefined is not a function
错误。
通过这一更改,如果模块绑定器支持 tree-shaking,则 Vue 应用程序中未使用的全局 api 将从最终捆绑包中消除,从而获得最佳的文件大小。
Vue 2.x 中的这些全局 API 受此更改的影响:
Vue.nextTick
Vue.observable
(用 Vue.reactive
替换)Vue.version
Vue.compile
(仅全构建)Vue.set
(仅兼容构建)Vue.delete
(仅兼容构建)除了公共 api,许多内部组件/帮助器现在也被导出为命名导出,只有当编译器的输出是这些特性时,才允许编译器导入这些特性,例如以下模板:
<transition> <div v-show="ok">hello</div></transition>
被编译为类似于以下的内容:
import { h, Transition, withDirectives, vShow } from 'vue'export function render() { return h(Transition, [withDirectives(h('div', 'hello'), [[vShow, this.ok]])])}
这实际上意味着只有在应用程序实际使用了 Transition
组件时才会导入它。换句话说,如果应用程序没有任何 Transition
组件,那么支持此功能的代码将不会出现在最终的捆绑包中。
随着全局 tree-shaking,用户只需为他们实际使用的功能“付费”,更好的是,知道了可选特性不会增加不使用它们的应用程序的捆绑包大小,框架大小在将来已经不再是其他核心功能的考虑因素了,如果有的话。
重要
以上仅适用于 ES Modules builds,用于支持 tree-shaking 的绑定器——UMD 构建仍然包括所有特性,并暴露 Vue 全局变量上的所有内容 (编译器将生成适当的输出,以使用全局外的 api 而不是导入)。
如果你的插件依赖受影响的 Vue 2.x 全局 API,例如:
const plugin = { install: Vue => { Vue.nextTick(() => { // ... }) }}
在 Vue 3 中,必须显式导入:
import { nextTick } from 'vue'const plugin = { install: app => { nextTick(() => { // ... }) }}
如果使用 webpack 这样的模块捆绑包,这可能会导致 Vue 的源代码绑定到插件中,而且通常情况下,这并不是你所期望的。防止这种情况发生的一种常见做法是配置模块绑定器以将 Vue 从最终捆绑中排除。对于 webpack,你可以使用 externals
配置选项:
// webpack.config.jsmodule.exports = { /*...*/ externals: { vue: 'Vue' }}
这将告诉 webpack 将 Vue 模块视为一个外部库,而不是捆绑它。
如果你选择的模块绑定器恰好是 Rollup,你基本上可以免费获得相同的效果,因为默认情况下,Rollup 会将绝对模块 id (在我们的例子中为 'vue'
) 作为外部依赖项,而不会将它们包含在最终的 bundle 中。但是在绑定期间,它可能会发出一个“将 vue 作为外部依赖” 警告,可使用 external
选项抑制该警告:
// rollup.config.jsexport default { /*...*/ external: ['vue']}
对内联特性的支持已被移除。
在 2.x 中,Vue 为子组件提供了 inline-template
attribute,以便将其内部内容用作模板,而不是将其作为分发内容。
<my-component inline-template> <div> <p>它们被编译为组件自己的模板</p> <p>不是父级所包含的内容。</p> </div></my-component>
将不再支持此功能。
inline-template
的大多数用例都假设没有构建工具设置,所有模板都直接写在 HTML 页面中
<script>
标签在这种情况下,最简单的解决方法是将 <script>
与其他类型一起使用:
<script type="text/html" id="my-comp-template"> <div>{{ hello }}</div></script>
在组件中,使用选择器将模板作为目标:
const MyComp = { template: '#my-comp-template' // ...}
这不需要任何构建设置,可以在所有浏览器中工作,不受任何 DOM HTML 解析警告的约束 (例如,你可以使用 camelCase prop 名称),并且在大多数 ide 中提供了正确的语法高亮显示。在传统的服务器端框架中,可以将这些模板拆分为服务器模板部分 (包括在主 HTML 模板中),以获得更好的可维护性。
以前使用 inline-template
的组件也可以使用默认 slot——进行重构,这使得数据范围更加明确,同时保留了内联编写子内容的便利性:
<!-- 2.x 语法 --><my-comp inline-template :msg="parentMsg"> {{ msg }} {{ childState }}</my-comp><!-- 默认 Slot 版本 --><my-comp v-slot="{ childState }"> {{ parentMsg }} {{ childState }}</my-comp>
子级现在应该渲染默认 slot*,而不是不提供模板:
<!-- 在子模板中,在传递时渲染默认slot 在必要的private状态下。--><template> <slot :childState="childState" /></template>
- 提示:在 3.x,slot 可以渲染为具有原生 fragments 支持的根目录!
v-if
/v-else
/v-else-if
的各分支项key
将不再是必须的,因为现在 Vue 会自动生成唯一的key
。key
,那么每个分支必须使用唯一的 key
。你不能通过故意使用相同的 key
来强制重用分支。<template v-for>
的 key
应该设置在 <template>
标签上 (而不是设置在它的子节点上)。特殊的 key
attribute 被用于提示 Vue 的虚拟 DOM 算法来保持对节点身份的持续跟踪。这样 Vue 可以知道何时能够重用和修补现有节点,以及何时需要对它们重新排序或重新创建。关于其它更多信息,可以查看以下章节:
Vue 2.x 建议在 v-if
/v-else
/v-else-if
的分支中使用 key
。
<!-- Vue 2.x --><div v-if="condition" key="yes">Yes</div><div v-else key="no">No</div>
这个示例在 Vue 3.x 中仍能正常工作。但是我们不再建议在 v-if
/v-else
/v-else-if
的分支中继续使用 key
attribute,因为没有为条件分支提供 key
时,也会自动生成唯一的 key
。
<!-- Vue 3.x --><div v-if="condition">Yes</div><div v-else>No</div>
非兼容变更体现在如果你手动提供了 key
,那么每个分支都必须使用一个唯一的 key
。因此大多数情况下都不需要设置这些 key
。
<!-- Vue 2.x --><div v-if="condition" key="a">Yes</div><div v-else key="a">No</div><!-- Vue 3.x (recommended solution: remove keys) --><div v-if="condition">Yes</div><div v-else>No</div><!-- Vue 3.x (alternate solution: make sure the keys are always unique) --><div v-if="condition" key="a">Yes</div><div v-else key="b">No</div>
<template v-for>
在 Vue 2.x 中 <template>
标签不能拥有 key
。不过你可以为其每个子节点分别设置 key
。
<!-- Vue 2.x --><template v-for="item in list"> <div :key="item.id">...</div> <span :key="item.id">...</span></template>
在 Vue 3.x 中 key
则应该被设置在 <template>
标签上。
<!-- Vue 3.x --><template v-for="item in list" :key="item.id"> <div>...</div> <span>...</span></template>
类似地,当使用 <template v-for>
时存在使用 v-if
的子节点,key
应改为设置在 <template>
标签上。
<!-- Vue 2.x --><template v-for="item in list"> <div v-if="item.isVisible" :key="item.id">...</div> <span v-else :key="item.id">...</span></template><!-- Vue 3.x --><template v-for="item in list" :key="item.id"> <div v-if="item.isVisible">...</div> <span v-else>...</span></template>
以下是变更的简要总结:
v-on
修饰符config.keyCodes
在 Vue 2 中,支持 keyCodes
作为修改 v-on
方法的方法。
<!-- 键码版本 --><input v-on:keyup.13="submit" /><!-- 别名版本 --><input v-on:keyup.enter="submit" />
此外,你可以通过全局 config.keyCodes
选项。
Vue.config.keyCodes = { f1: 112}
<!-- 键码版本 --><input v-on:keyup.112="showHelpText" /><!-- 自定别名版本 --><input v-on:keyup.f1="showHelpText" />
从KeyboardEvent.keyCode
has been deprecated 开始,Vue 3 继续支持这一点就不再有意义了。因此,现在建议对任何要用作修饰符的键使用 kebab-cased (短横线) 大小写名称。
<!-- Vue 3 在 v-on 上使用 按键修饰符 --><input v-on:keyup.delete="confirmDelete" />
因此,这意味着 config.keyCodes
现在也已弃用,不再受支持。
对于那些在代码库中使用 keyCode
的用户,我们建议将它们转换为它们的 kebab-cased (短横线) 命名对齐。
生成 prop 默认值的工厂函数不再能访问 this
。
替代方案:
import { inject } from 'vue'export default { props: { theme: { default (props) { // `props` 是传递给组件的原始值。 // 在任何类型/默认强制转换之前 // 也可以使用 `inject` 来访问注入的 property return inject('theme', 'default-theme') } } }}
此更改不会影响 <template>
用户。
以下是更改的简要总结:
h
现在全局导入,而不是作为参数传递给渲染函数更多信息,请继续阅读!
在 2.x 中,e render
函数将自动接收 h
函数 (它是 createElement
的常规别名) 作为参数:
// Vue 2 渲染函数示例export default { render(h) { return h('div') }}
在 3.x 中,h
现在是全局导入的,而不是作为参数自动传递。
// Vue 3 渲染函数示例import { h } from 'vue'export default { render() { return h('div') }}
在 2.x 中,render
函数自动接收诸如 h
之类的参数。
// Vue 2 渲染函数示例export default { render(h) { return h('div') }}
在 3.x 中,由于 render
函数不再接收任何参数,它将主要用于 setup()
函数内部。这还有一个好处:可以访问作用域中声明的响应式状态和函数,以及传递给 setup()
的参数。
import { h, reactive } from 'vue'export default { setup(props, { slots, attrs, emit }) { const state = reactive({ count: 0 }) function increment() { state.count++ } // 返回render函数 return () => h( 'div', { onClick: increment }, state.count ) }}
有关 setup()
如何工作的详细信息,参考组合式 API 指南。
在 2.x 中,domProps
包含 VNode props 中的嵌套列表:
// 2.x{ class: ['button', 'is-outlined'], style: { color: '#34495E' }, attrs: { id: 'submit' }, domProps: { innerHTML: '' }, on: { click: submitForm }, key: 'submit-button'}
在 3.x 中,整个 VNode props 结构是扁平的,使用上面的例子,下面是它现在的样子
// 3.x 语法{ class: ['button', 'is-outlined'], style: { color: '#34495E' }, id: 'submit', innerHTML: '', onClick: submitForm, key: 'submit-button'}
全局导入 h
意味着任何包含 Vue 组件的库都将在某处包含 import { h } from 'vue'
,因此,这会带来一些开销,因为它需要库作者在其构建设置中正确配置 Vue 的外部化:
见 Render 函数指南更详细的文档!
此更改统一了 3.x 中的普通 slot 和作用域 slot。
以下是变化的变更总结:
this.$slots
现在将 slots 作为函数公开this.$scopedSlots
更多信息,请继续阅读!
当使用渲染函数时,即 h
,2.x 用于在内容节点上定义 slot
data property。
// 2.x 语法h(LayoutComponent, [ h('div', { slot: 'header' }, this.header), h('div', { slot: 'content' }, this.content)])
此外,在引用作用域 slot 时,可以使用以下方法引用它们:
// 2.x 语法this.$scopedSlots.header
在 3.x 中,插槽被定义为当前节点的子对象:
// 3.x Syntaxh(LayoutComponent, {}, { header: () => h('div', this.header), content: () => h('div', this.content)})
当你需要以编程方式引用作用域 slot 时,它们现在被统一到 $slots
选项中。
// 2.x 语法this.$scopedSlots.header// 3.x 语法this.$slots.header
大部分更改已经在 2.6 中发布。因此,迁移可以一步到位:
this.$scopedSlots
替换为 this.$slots
。过渡类名 v-enter
修改为 v-enter-from
、过渡类名 v-leave
修改为 v-leave-from
。
在v2.1.8版本之前, 为过渡指令提供了两个过渡类名对应初始和激活状态。
在 v2.1.8 版本中, 引入 v-enter-to
来定义 enter 或 leave 变换之间的过渡动画插帧, 为了向下兼容, 并没有变动 v-enter
类名:
.v-enter,.v-leave-to { opacity: 0;}.v-leave,.v-enter-to { opacity: 1;}
这样做会带来很多困惑, 类似 enter 和 leave 含义过于宽泛并且没有遵循类名钩子的命名约定。
为了更加明确易读,我们现在将这些初始状态重命名为:
.v-enter-from,.v-leave-to { opacity: 0;}.v-leave-from,.v-enter-to { opacity: 1;}
现在,这些状态之间的区别就清晰多了。
<transition>
组件相关属性名也发生了变化:
leave-class
已经被重命名为 leave-from-class
(在渲染函数或 JSX 中可以写为:leaveFromClass
)enter-class
已经被重命名为 enter-from-class
(在渲染函数或 JSX 中可以写为:enterFromClass
).v-enter
字符串实例替换为 .v-enter-from
.v-leave
字符串实例替换为 .v-leave-from
就变化内容而言,此部分属于高阶内容:
v-model
prop 和事件默认名称已更改:value
-> modelValue
;input
-> update:modelValue
;v-bind
的 .sync
修饰符和组件的 model
选项已移除,可用 v-model
作为代替;v-model
进行双向绑定;v-model
修饰符。更多信息,请见下文。
在 Vue 2.0 发布后,开发者使用 v-model
指令必须使用为 value
的 prop。如果开发者出于不同的目的需要使用其他的 prop,他们就不得不使用 v-bind.sync
。此外,由于v-model
和 value
之间的这种硬编码关系的原因,产生了如何处理原生元素和自定义元素的问题。
在 Vue 2.2 中,我们引入了 model
组件选项,允许组件自定义用于 v-model
的 prop 和事件。但是,这仍然只允许在组件上使用一个 model
。
在 Vue 3 中,双向数据绑定的 API 已经标准化,减少了开发者在使用 v-model
指令时的混淆并且在使用 v-model
指令时可以更加灵活。
在 2.x 中,在组件上使用 v-model
相当于绑定 value
prop 和 input
事件:
<ChildComponent v-model="pageTitle" /><!-- 简写: --><ChildComponent :value="pageTitle" @input="pageTitle = $event" />
如果要将属性或事件名称更改为其他名称,则需要在 ChildComponent
组件中添加 model
选项:
<!-- ParentComponent.vue --><ChildComponent v-model="pageTitle" />
// ChildComponent.vueexport default { model: { prop: 'title', event: 'change' }, props: { // 这将允许 `value` 属性用于其他用途 value: String, // 使用 `title` 代替 `value` 作为 model 的 prop title: { type: String, default: 'Default title' } }}
所以,在这个例子中 v-model
的简写如下:
<ChildComponent :title="pageTitle" @change="pageTitle = $event" />
v-bind.sync
在某些情况下,我们可能需要对某一个 prop 进行“双向绑定”(除了前面用 v-model
绑定 prop 的情况)。为此,我们建议使用 update:myPropName
抛出事件。例如,对于在上一个示例中带有 title
prop 的 ChildComponent
,我们可以通过下面的方式将分配新 value 的意图传达给父级:
this.$emit('update:title', newValue)
如果需要的话,父级可以监听该事件并更新本地 data property。例如:
<ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
为了方便起见,我们可以使用 .sync
修饰符来缩写,如下所示:
<ChildComponent :title.sync="pageTitle" />
在 3.x 中,自定义组件上的 v-model
相当于传递了 modelValue
prop 并接收抛出的 update:modelValue
事件:
<ChildComponent v-model="pageTitle" /><!-- 简写: --><ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event"/>
v-model
参数若需要更改 model
名称,而不是更改组件内的 model
选项,那么现在我们可以将一个 argument 传递给 model
:
<ChildComponent v-model:title="pageTitle" /><!-- 简写: --><ChildComponent :title="pageTitle" @update:title="pageTitle = $event" />
这也可以作为 .sync
修饰符的替代,而且允许我们在自定义组件上使用多个 v-model
。
<ChildComponent v-model:title="pageTitle" v-model:content="pageContent" /><!-- 简写: --><ChildComponent :title="pageTitle" @update:title="pageTitle = $event" :content="pageContent" @update:content="pageContent = $event"/>
v-model
修饰符除了像 .trim
这样的 2.x 硬编码的 v-model
修饰符外,现在 3.x 还支持自定义修饰符:
<ChildComponent v-model.capitalize="pageTitle" />
我们可以在 Custom Events 部分中了解有关自定义 v-model
修饰符的更多信息。
我们推荐:
.sync
的部分并将其替换为 v-model
: <ChildComponent :title.sync="pageTitle" /> <!-- 替换为 --> <ChildComponent v-model:title="pageTitle" />
v-model
,请确保分别将 prop 和 event 命名更改为 modelValue
和 update:modelValue
<ChildComponent v-model="pageTitle" />
// ChildComponent.vue export default { props: { modelValue: String // 以前是`value:String` }, methods: { changePageTitle(title) { this.$emit('update:modelValue', title) // 以前是 `this.$emit('input', title)` } } }
更多新的 v-model
语法相关信息,请参考:
在元素上动态绑定 attribute 时,常见的场景是在一个元素中同时使用 v-bind="object"
语法和单独的 property。然而,这就引出了关于合并的优先级的问题。
在 2.x,如果一个元素同时定义了 v-bind="object"
和一个相同的单独的 property,那么这个单独的 property 总是会覆盖 object
中的绑定。
<!-- template --><div id="red" v-bind="{ id: 'blue' }"></div><!-- result --><div id="red"></div>
在 3.x,如果一个元素同时定义了 v-bind="object"
和一个相同的单独的 property,那么声明绑定的顺序决定了它们如何合并。换句话说,相对于假设开发者总是希望单独的 property 覆盖 object
中定义的内容,现在开发者对自己所希望的合并行为有了更好的控制。
<!-- template --><div id="red" v-bind="{ id: 'blue' }"></div><!-- result --><div id="blue"></div><!-- template --><div v-bind="{ id: 'blue' }" id="red"></div><!-- result --><div id="red"></div>
如果你依赖 v-bind
的覆盖功能,目前的建议是确保在单独的 property 之前定义 v-bind
attribute。
译者:本章节大部分内容是针对母语是英文的读者,中文用户可略读,除非你想以英文文档编写者的身份参与 Vue docs 的编写,
编写文档是一种换位思考的练习。我们并不是在描述客观现实——源代码已经做到了。我们的工作是帮助塑造用户与 Vue 生态系统之间的关系。这份不断发展的指南提供了一些规则和建议,说明如何在 Vue 生态系统中始终如一地做到这一点。
<BlogPost>
组件例子比 <ComponentA>
更好。<BlogPost>
组件例子比 <CurrencyExchangeSettings>
更好。attribute
优于 attr
,message
优于 msg
),除非你在 API 中明确引用了缩写 (例如 $attrs
)。标准键盘上包含的缩写符号 (例如,@
,#
,&
) 可以。:
) 结束句子,而不是句点 (.
)。比如:
我们有一些专用的样式来表示需要以特定方式突出显示的内容。这些被捕获为在这个页面请谨慎使用。
滥用这些样式是有一定诱惑力的,因为你可以简单地在标注中添加更改。但是,这会破坏用户的阅读流程,因此,只能在特殊情况下使用。在可能的情况下,我们应该尝试在页面内创建一个叙述和流程,以尊重读者的认知负荷。
在任何情况下都不应该相邻使用两个警告,这表明我们无法很好地解释上下文。
我们欣赏小型、集中的 PR。如果你想进行非常大的更改,请在发起请求之前与团队成员沟通。这是一份详细说明为什么这一点如此重要的书面材料让我们在这个团队里工作得很好。请理解,尽管我们总是很感激你的贡献,但最终我们必须优先考虑哪些对整个项目最有效。
本指南将概述可用于创建文档的不同设计元素。
VuePress 提供了一个自定义容器插件来创建警稿框。有四种类型:
Markdown 范例
::: infoYou can find more information at this site.:::::: tipThis is a great tip to remember!:::::: warningThis is something to be cautious of.:::::: danger DANGERThis is something we do not recommend. Use at your own risk.:::
渲染 Markdown
INFO
你可以在这个网站上找到更多信息。
TIP
这是一个值得记住的好提示!
WARNING
这是需要谨慎的。
DANGER
这是我们不推荐的。使用风险自负。
VuePress 使用 Prism 提供语言语法高亮显示,方法是将语言附加到代码块的起始反撇号:
Markdown 示例
```jsexport default { name: 'MyComponent'}```
渲染输出
export default { name: 'MyComponent'}
向代码块添加行高亮显示,需要在大括号中附加行号。
Markdown 示例
```js{2}export default { name: 'MyComponent', props: { type: String, item: Object }}```
渲染 Markdown
export default { name: 'MyComponent', props: { type: String, item: Object }}
```js{4-5}export default { name: 'MyComponent', props: { type: String, item: Object }}```
export default { name: 'MyComponent', props: { type: String, item: Object }}
```js{2,4-5}export default { name: 'MyComponent', props: { type: String, item: Object }}```
export default { name: 'MyComponent', props: { type: String, item: Object }}
Vue 已经遍布全球,核心团队至少在 6 个不同的时区。论坛包括 7 种语言和计数,我们的许多文档积极维护翻译。我们为 Vue 的国际影响力感到骄傲,但我们可以做得更好。
目前,VUE3 文档仍处于测试阶段,随时可能更改。因此,我们会谨慎对待任何重要的工作,因为我们仍在收集反馈,并根据需要重新编写。当文档处于发布候选阶段时,我们将确保发布公告,以便你可以开始使用!
开始的最好方法是检查此处固定 issues Vuejs.org 其中包含了对社区内各种倡议的积极讨论。
config
是一个包含了 Vue 应用全局配置的对象。你可以在应用挂载前修改其以下 property:
const app = Vue.createApp({})app.config = {...}
Function
undefined
app.config.errorHandler = (err, vm, info) => { // 处理错误 // `info` 是 Vue 特定的错误信息,比如错误所在的生命周期钩子}
指定一个处理函数,来处理组件渲染方法执行期间以及侦听器抛出的未捕获错误。这个处理函数被调用时,可获取错误信息和应用实例。
Function
undefined
app.config.warnHandler = function(msg, vm, trace) { // `trace` 是组件的继承关系追踪}
为 Vue 的运行时警告指定一个自定义处理函数。注意这只会在开发环境下生效,在生产环境下它会被忽略。
[key: string]: any
undefined
app.config.globalProperties.foo = 'bar'app.component('child-component', { mounted() { console.log(this.foo) // 'bar' }})
添加可以在应用程序内的任何组件实例中访问的全局 property。属性名冲突时,组件的 property 将具有优先权。
这可以代替 Vue 2.x Vue.prototype
扩展:
// 之前(Vue 2.x)Vue.prototype.$http = () => {}// 之后(Vue 3.x)const app = Vue.createApp({})app.config.globalProperties.$http = () => {}
(tag: string) => boolean
undefined
// 任何以“ion-”开头的元素都将被识别为自定义元素app.config.isCustomElement = tag => tag.startsWith('ion-')
指定一个方法,用来识别在 Vue 之外定义的自定义元素(例如,使用 Web Components API)。如果组件符合此条件,则不需要本地或全局注册,并且 Vue 不会抛出关于 Unknown custom element
的警告。
注意,所有原生 HTML 和 SVG 标记不需要在此函数中匹配——Vue 解析器自动执行此检查。
{ [key: string]: Function }
{}
const app = Vue.createApp({ mounted() { console.log(this.$options.hello) }})app.config.optionMergeStrategies.hello = (parent, child, vm) => { return `Hello, ${child}`}app.mixin({ hello: 'Vue'})// 'Hello, Vue
为自定义选项定义合并策略。
合并策略选项分别接收在父实例和子实例上定义的该选项的值作为第一个和第二个参数,引用上下文实例被作为第三个参数传入。
boolean
false
设置为 true
以在浏览器开发工具的 performance/timeline 面板中启用对组件初始化、编译、渲染和更新的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器。
在 Vue 3 中,改变全局 Vue 行为的 API 现在被移动到了由新的 createApp
方法所创建的应用实例上。此外,现在它们的影响仅限于该特定应用实例:
import { createApp } from 'vue'const app = createApp({})
调用 createApp
返回一个应用实例。该实例提供了一个应用上下文。应用实例挂载的整个组件树共享相同的上下文,该上下文提供了之前在 Vue 2.x 中“全局”的配置。
另外,由于 createApp
方法返回应用实例本身,因此可以在其后链式调用其它方法,这些方法可以在以下部分中找到。
{string} name
{Function | Object} [definition]
definition
参数,返回应用实例。definition
参数,返回组件定义。注册或检索全局组件。注册还会使用给定的 name
参数自动设置组件的 name
。
import { createApp } from 'vue'const app = createApp({})// 注册一个名为my-component的组件app.component('my-component', { /* ... */})// 检索注册的组件(始终返回构造函数)const MyComponent = app.component('my-component', {})
包含应用配置的对象。
import { createApp } from 'vue'const app = createApp({})app.config = {...}
{string} name
{Function | Object} [definition]
definition
参数,返回应用实例。definition
参数,返回指令定义。注册或检索全局指令。
import { createApp } from 'vue'const app = createApp({})// 注册app.directive('my-directive', { // 指令是具有一组生命周期的钩子: // 在绑定元素的父组件挂载之前调用 beforeMount() {}, // 绑定元素的父组件挂载时调用 mounted() {}, // 在包含组件的 VNode 更新之前调用 beforeUpdate() {}, // 在包含组件的 VNode 及其子组件的 VNode 更新之后调用 updated() {}, // 在绑定元素的父组件卸载之前调用 beforeUnmount() {}, // 卸载绑定元素的父组件时调用 unmounted() {}})// 注册 (功能指令)app.directive('my-directive', () => { // 这将被作为 `mounted` 和 `updated` 调用})// getter, 如果已注册,则返回指令定义const myDirective = app.directive('my-directive')
指令钩子传递了这些参数:
指令绑定到的元素。这可用于直接操作 DOM。
包含以下 property 的对象。
instance
:使用指令的组件实例。value
:传递给指令的值。例如,在 v-my-directive="1 + 1"
中,该值为 2
。oldValue
:先前的值,仅在 beforeUpdate
和 updated
中可用。值是否已更改都可用。arg
:参数传递给指令 (如果有)。例如在 v-my-directive:foo
中,arg 为 "foo"
。modifiers
:包含修饰符 (如果有) 的对象。例如在 v-my-directive.foo.bar
中,修饰符对象为 {foo: true,bar: true}
。dir
:一个对象,在注册指令时作为参数传递。例如,在以下指令中app.directive('focus', { mounted(el) { el.focus() }})
dir
将会是以下对象:
{ mounted(el) { el.focus() }}
上面作为 el 参数收到的真实 DOM 元素的蓝图。
上一个虚拟节点,仅在 beforeUpdate
和 updated
钩子中可用。
Note
除了 el
之外,你应该将这些参数视为只读,并且永远不要修改它们。如果你需要跨钩子共享信息,建议通过元素的自定义数据属性集进行共享。
{Object} mixin
在整个应用范围内应用混入。一旦注册,它们就可以在当前的应用中任何组件模板内使用它。插件作者可以使用此方法将自定义行为注入组件。不建议在应用代码中使用。
{Element | string} rootContainer
{boolean} isHydrate
将应用实例的根组件挂载在提供的 DOM 元素上。
<body> <div id="my-app"></div></body>
import { createApp } from 'vue'const app = createApp({})// 做一些必要的准备app.mount('#my-app')
{string | Symbol} key
value
设置一个可以被注入到应用范围内所有组件中的值。组件应该使用 inject
来接收提供的值。
从 provide
/inject
的角度来看,可以将应用程序视为根级别的祖先,而根组件是其唯一的子级。
该方法不应该与 provide 组件选项或组合式 API 中的 provide 方法混淆。虽然它们也是相同的 provide
/inject
机制的一部分,但是是用来配置组件提供的值而不是应用提供的值。
通过应用提供值在写插件时尤其有用,因为插件一般不能使用组件提供值。这是使用 globalProperties 的替代选择。
Note
provide
和 inject
绑定不是响应式的。这是有意为之。不过,如果你向下传递一个响应式对象,这个对象上的 property 会保持响应式。
向根组件中注入一个 property,值由应用提供。
import { createApp } from 'vue'const app = createApp({ inject: ['user'], template: ` <div> {{ user }} </div> `})app.provide('user', 'administrator')
{Element | string} rootContainer
在提供的 DOM 元素上卸载应用实例的根组件。
<body> <div id="my-app"></div></body>
import { createApp } from 'vue'const app = createApp({})// 做一些必要的准备app.mount('#my-app')// 挂载5秒后,应用将被卸载setTimeout(() => app.unmount('#my-app'), 5000)
{Object | Function} plugin
...options (可选)
安装 Vue.js 插件。如果插件是一个对象,它必须暴露一个 install
方法。如果它本身是一个函数,它将被视为安装方法。
该安装方法将以应用实例作为第一个参数被调用。传给 use
的其他 options
参数将作为后续参数传入该安装方法。
当在同一个插件上多次调用此方法时,该插件将仅安装一次。
返回一个提供应用上下文的应用实例。应用实例挂载的整个组件树共享同一个上下文。
const app = Vue.createApp({})
你可以在 createApp
之后链式调用其它方法,这些方法可以在应用 API 中找到。
该函数接收一个根组件选项对象作为第一个参数:
const app = Vue.createApp({ data() { return { ... } }, methods: {...}, computed: {...} ...})
使用第二个参数,我们可以将根 prop 传递给应用程序:
const app = Vue.createApp( { props: ['username'] }, { username: 'Evan' })
<div id="app"> <!-- 会显示 'Evan' --> {{ username }}</div>
interface Data { [key: string]: unknown}export type CreateAppFunction<HostElement> = ( rootComponent: PublicAPIComponent, rootProps?: Data | null) => App<HostElement>
返回一个”虚拟节点“,通常缩写为 VNode:一个普通对象,其中包含向 Vue 描述它应在页面上渲染哪种节点的信息,包括所有子节点的描述。它的目的是用于手动编写的渲染函数:
render() { return Vue.h('h1', {}, 'Some title')}
接收三个参数:type
,props
和 children
String | Object | Function
HTML 标签名、组件或异步组件。使用返回 null 的函数将渲染一个注释。此参数是必需的。
Object
一个对象,与我们将在模板中使用的 attribute、prop 和事件相对应。可选。
String | Array | Object
子代 VNode,使用 h()
生成,或者使用字符串来获取“文本 VNode”,或带有插槽的对象。可选。
h('div', {}, [ 'Some text comes first.', h('h1', 'A headline'), h(MyComponent, { someProp: 'foobar' }) ])
从实现上看,defineComponent
只返回传递给它的对象。但是,就类型而言,返回的值有一个合成类型的构造函数,用于手动渲染函数、TSX 和 IDE 工具支持。
具有组件选项的对象
import { defineComponent } from 'vue'const MyComponent = defineComponent({ data() { return { count: 1 } }, methods: { increment() { this.count++ } }})
或者是一个 setup
函数,函数名称将作为组件名称来使用
import { defineComponent, ref } from 'vue'const HelloWorld = defineComponent(function HelloWorld() { const count = ref(0) return { count }})
创建一个只有在需要时才会加载的异步组件。
对于基本用法,defineAsyncComponent
可以接受一个返回 Promise
的工厂函数。Promise 的 resolve
回调应该在服务端返回组件定义后被调用。你也可以调用 reject(reason)
来表示加载失败。
import { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent(() => import('./components/AsyncComponent.vue'))app.component('async-component', AsyncComp)
当使用局部注册时,你也可以直接提供一个返回 Promise
的函数:
import { createApp, defineAsyncComponent } from 'vue'createApp({ // ... components: { AsyncComponent: defineAsyncComponent(() => import('./components/AsyncComponent.vue') ) }})
对于高阶用法,defineAsyncComponent
可以接受一个对象:
defineAsyncComponent
方法还可以返回以下格式的对象:
import { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent({ // 工厂函数 loader: () => import('./Foo.vue') // 加载异步组件时要使用的组件 loadingComponent: LoadingComponent, // 加载失败时要使用的组件 errorComponent: ErrorComponent, // 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms) delay: 200, // 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件 // 默认值:Infinity(即永不超时,单位 ms) timeout: 3000, // 定义组件是否可挂起 | 默认值:true suspensible: false, /** * * @param {*} error 错误信息对象 * @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试 * @param {*} fail 一个函数,指示加载程序结束退出 * @param {*} attempts 允许的最大重试次数 */ onError(error, retry, fail, attempts) { if (error.message.match(/fetch/) && attempts <= 3) { // 请求发生错误时重试,最多可尝试 3 次 retry() } else { // 注意,retry/fail 就像 promise 的 resolve/reject 一样: // 必须调用其中一个才能继续错误处理。 fail() } }})
参考:动态和异步组件
WARNING
resolveComponent
只能在 render
或 setup
函数中使用。
如果在当前应用实例中可用,则允许按名称解析 component
。
返回一个 Component
。如果没有找到,则返回 undefined
。
const app = Vue.createApp({})app.component('MyComponent', { /* ... */})
import { resolveComponent } from 'vue'render() { const MyComponent = resolveComponent('MyComponent')}
接受一个参数:name
String
已加载的组件的名称。
WARNING
resolveDynamicComponent
只能在 render
或 setup
函数中使用。
允许使用与 <component :is="">
相同的机制来解析一个 component
。
返回已解析的 Component
或新创建的 VNode
,其中组件名称作为节点标签。如果找不到 Component
,将发出警告。
import { resolveDynamicComponent } from 'vue'render () { const MyComponent = resolveDynamicComponent('MyComponent')}
接受一个参数:component
String | Object (组件的选项对象)
有关详细信息,请参阅动态组件上的文档。
WARNING
resolveDirective
只能在 render
或 setup
函数中使用。
如果在当前应用实例中可用,则允许通过其名称解析一个 directive
。
返回一个 Directive
。如果没有找到,则返回 undefined
。
const app = Vue.createApp({})app.directive('highlight', {})
import { resolveDirective } from 'vue'render () { const highlightDirective = resolveDirective('highlight')}
接受一个参数:name
String
已加载的指令的名称。
WARNING
withDirectives
只能在 render
或 setup
函数中使用。
允许将指令应用于 VNode。返回一个包含应用指令的 VNode。
import { withDirectives, resolveDirective } from 'vue'const foo = resolveDirective('foo')const bar = resolveDirective('bar')return withDirectives(h('div'), [ [foo, this.x], [bar, this.y]])
接受两个参数:vnode
和 directives
。
vnode
一个虚拟节点,通常使用 h()
创建。
Array
一个指令数组。
每个指令本身都是一个数组,最多可以定义 4 个索引,如以下示例所示。
[directive]
- 该指令本身。必选。 const MyDirective = resolveDirective('MyDirective') const nodeWithDirectives = withDirectives(h('div'), [[MyDirective]])
[directive, value]
- 上述内容,再加上分配给指令的类型为 any
的值。 const MyDirective = resolveDirective('MyDirective') const nodeWithDirectives = withDirectives(h('div'), [[MyDirective, 100]])
[directive, value, arg]
- 上述内容,再加上一个 string
参数,比如:在 v-on:click
中的 click
。 const MyDirective = resolveDirective('MyDirective') const nodeWithDirectives = withDirectives(h('div'), [ [MyDirective, 100, 'click'] ])
[directive, value, arg, modifiers]
- 上述内容,再加上定义任何修饰符的 key: value
键值对 Object
。 const MyDirective = resolveDirective('MyDirective') const nodeWithDirectives = withDirectives(h('div'), [ [MyDirective, 100, 'click', { prevent: true }] ])
createRenderer 函数接受两个泛型参数: HostNode
和 HostElement
,对应于宿主环境中的 Node 和 Element 类型。
例如,对于 runtime-dom,HostNode 将是 DOM Node
接口,HostElement 将是 DOM Element
接口。
自定义渲染器可以传入特定于平台的类型,如下所示:
import { createRenderer } from 'vue'const { render, createApp } = createRenderer<Node, Element>({ patchProp, ...nodeOps})
接受两个参数:HostNode
和 HostElement
。
Node
宿主环境中的节点。
Element
宿主环境中的元素。
将回调推迟到下一个 DOM 更新周期之后执行。在更改了一些数据以等待 DOM 更新后立即使用它。
import { createApp, nextTick } from 'vue'const app = createApp({ setup() { const message = ref('Hello!') const changeMessage = async newMessage => { message.value = newMessage await nextTick() console.log('Now DOM is updated') } }})
Function
返回组件实例的 data 对象的函数。在 data
中,我们不建议观察具有自身状态行为的对象,如浏览器 API 对象和原型 property。一个好主意是这里只有一个表示组件 data 的普通对象。
一旦观察过,你就无法在根数据对象上添加响应式 property。因此推荐在创建实例之前,就声明所有的根级响应式 property。
实例创建之后,可以通过 vm.$data
访问原始数据对象。组件实例也代理了 data 对象上所有的 property,因此访问 vm.a
等价于访问 vm.$data.a
。
以 _
或 $
开头的 property 不会被组件实例代理,因为它们可能和 Vue 内置的 property、API 方法冲突。你可以使用例如 vm.$data._property
的方式访问这些 property。
// 直接创建一个实例 const data = { a: 1 } // 这个对象将添加到组件实例中 const vm = Vue.createApp({ data() { return data } }).mount('#app') console.log(vm.a) // => 1
注意,如果你为 data property 使用了箭头函数,则 this
不会指向这个组件的实例,不过你仍然可以将其实例作为函数的第一个参数来访问。
data: vm => ({ a: vm.myProp })
Array<string> | Object
props 可以是数组或对象,用于接收来自父组件的数据。props 可以是简单的数组,或者使用对象作为替代,对象允许配置高阶选项,如类型检测、自定义验证和设置默认值。
你可以基于对象的语法使用以下选项:
type
:可以是下列原生构造函数中的一种:String
、Number
、Boolean
、Array
、Object
、Date
、Function
、Symbol
、任何自定义构造函数、或上述内容组成的数组。会检查一个 prop 是否是给定的类型,否则抛出警告。Prop 类型的更多信息在此。default
:any
为该 prop 指定一个默认值。如果该 prop 没有被传入,则换做用这个值。对象或数组的默认值必须从一个工厂函数返回required
:Boolean
义该 prop 是否是必填项。在非生产环境中,如果这个值为 truthy 且该 prop 没有被传入的,则一个控制台警告将会被抛出。validator
:Function
自定义验证函数会将该 prop 的值作为唯一的参数代入。在非生产环境下,如果该函数返回一个 falsy 的值 (也就是验证失败),一个控制台警告将会被抛出。你可以在这里查阅更多 prop 验证的相关信息。 const app = Vue.createApp({}) // 简单语法 app.component('props-demo-simple', { props: ['size', 'myMessage'] }) // 对象语法,提供验证 app.component('props-demo-advanced', { props: { // 类型检查 height: Number, // 类型检查 + 其他验证 age: { type: Number, default: 0, required: true, validator: value => { return value >= 0 } } } })
{ [key: string]: Function | { get: Function, set: Function } }
计算属性将被混入到组件实例中。所有 getter 和 setter 的 this
上下文自动地绑定为组件实例。
注意,如果你为一个计算属性使用了箭头函数,则 this
不会指向这个组件的实例,不过你仍然可以将其实例作为函数的第一个参数来访问。
computed: { aDouble: vm => vm.a * 2 }
计算属性的结果会被缓存,除非依赖的响应式 property 变化才会重新计算。注意,如果某个依赖 (比如非响应式 property) 在该实例范畴之外,则计算属性是不会被更新的。
const app = Vue.createApp({ data() { return { a: 1 } }, computed: { // 仅读取 aDouble() { return this.a * 2 }, // 读取和设置 aPlus: { get() { return this.a + 1 }, set(v) { this.a = v - 1 } } } }) const vm = app.mount('#app') console.log(vm.aPlus) // => 2 vm.aPlus = 3 console.log(vm.a) // => 2 console.log(vm.aDouble) // => 4
{ [key: string]: Function }
methods 将被混入到组件实例中。可以直接通过 VM 实例访问这些方法,或者在指令表达式中使用。方法中的 this
自动绑定为组件实例。
注意
注意,不应该使用箭头函数来定义 method 函数 (例如 plus:() => this.a++)。理由是箭头函数绑定了父级作用域的上下文,所以 this
将不会按照期望指向组件实例,this.a
将是 undefined。
const app = Vue.createApp({ data() { return { a: 1 } }, methods: { plus() { this.a++ } } }) const vm = app.mount('#app') vm.plus() console.log(vm.a) // => 2
{ [key: string]: string | Function | Object | Array}
一个对象,键是需要观察的表达式,值是对应回调函数。值也可以是方法名,或者包含选项的对象。组件实例将会在实例化时调用 $watch()
,参阅 $watch,了解更多关于 deep
、immediate
和 flush
选项的信息。
const app = Vue.createApp({ data() { return { a: 1, b: 2, c: { d: 4 }, e: 'test', f: 5 } }, watch: { a(val, oldVal) { console.log(`new: ${val}, old: ${oldVal}`) }, // 字符串方法名 b: 'someMethod', // 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深 c: { handler(val, oldVal) { console.log('c changed') }, deep: true }, // 该回调将会在侦听开始之后被立即调用 e: { handler(val, oldVal) { console.log('e changed') }, immediate: true }, // 你可以传入回调数组,它们会被逐一调用 f: [ 'handle1', function handle2(val, oldVal) { console.log('handle2 triggered') }, { handler: function handle3(val, oldVal) { console.log('handle3 triggered') } /* ... */ } ] }, methods: { someMethod() { console.log('b changed') }, handle1() { console.log('handle 1 triggered') } } }) const vm = app.mount('#app') vm.a = 3 // => new: 3, old: 1
注意
注意,不应该使用箭头函数来定义 watcher 函数 (例如 searchQuery: newValue => this.updateAutocomplete(newValue)
)。理由是箭头函数绑定了父级作用域的上下文,所以 this
将不会按照期望指向组件实例,this.updateAutocomplete
将是 undefined。
Array<string> | Object
emits 可以是数组或对象,从组件触发自定义事件,emits 可以是简单的数组,或者对象作为替代,允许配置和事件验证。
在对象语法中,每个 property 的值可以为 null
或验证函数。验证函数将接收传递给 $emit
调用的其他参数。如果 this.$emit('foo',1)
被调用,foo
的相应验证函数将接收参数 1
。验证函数应返回布尔值,以表示事件参数是否有效。
const app = Vue.createApp({}) // 数组语法 app.component('todo-item', { emits: ['check'], created() { this.$emit('check') } }) // 对象语法 app.component('reply-form', { emits: { // 没有验证函数 click: null, // 带有验证函数 submit: payload => { if (payload.email && payload.password) { return true } else { console.warn(`Invalid submit event payload!`) return false } } } })
注意
emits
选项中列出的事件不会从组件的根元素继承,也将从 $attrs
property 中移除。
string
一个字符串模板作为 component 实例的标识使用。模板将会替换挂载的元素。挂载元素的内容都将被忽略,除非模板的内容有分发插槽。
如果值以 #
开始,则它将被用作 querySelector
,并使用匹配元素的 innerHTML 作为模板。常用的技巧是用 <script type="x-template">
包含模板。
注意
出于安全考虑,你应该只使用你信任的 Vue 模板。避免使用其他人生成的内容作为你的模板。
注意
如果 Vue 选项中包含渲染函数,该模板将被忽略。
Function
字符串模板的另一种选择,允许你充分利用 JavaScript 的编程功能。
<div id="app" class="demo"> <my-title blog-title="A Perfect Vue"></my-title> </div>
const app = Vue.createApp({}) app.component('my-title', { render() { return Vue.h( 'h1', // 标签名称 this.blogTitle // 标签内容 ) }, props: { blogTitle: { type: String, required: true } } }) app.mount('#app')
注意
render
函数的优先级高于从挂载元素 template
选项或内置 DOM 提取出的 HTML 模板编译渲染函数。
注意
所有的生命周期钩子自动绑定 this
上下文到实例中,因此你可以访问数据,对 property 和方法进行运算。这意味着你不能使用箭头函数来定义一个生命周期方法 (例如 created: () => this.fetchTodos()
) 。这是因为箭头函数绑定了父上下文,因此 this
与你期待的组件实例不同,this.fetchTodos
的行为未定义。
Function
在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
Function
在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),property 和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el
property 目前尚不可用。
Function
在挂载开始之前被调用:相关的 render
函数首次被调用。
该钩子在服务器端渲染期间不被调用。
Function
实例被挂载后调用,这时 Vue.createApp({}).mount()
被新创建的 vm.$el
替换了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时 vm.$el
也在文档内。
注意 mounted
不会保证所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以在 mounted
内部使用 vm。$nextTick:
mounted() { this.$nextTick(function () { // 仅在渲染整个视图之后运行的代码 }) }
该钩子在服务器端渲染期间不被调用。
Function
数据更新时调用,发生在虚拟 DOM 打补丁之前。这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
该钩子在服务器端渲染期间不被调用,因为只有初次渲染会在服务端进行。
Function
由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或侦听器取而代之。
注意,updated
不会保证所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以在 updated
里使用 vm.$nextTick:
updated() { this.$nextTick(function () { // 仅在渲染整个视图之后运行的代码 }) }
该钩子在服务器端渲染期间不被调用。
Function
被 keep-alive 缓存的组件激活时调用。
该钩子在服务器端渲染期间不被调用。
Function
被 keep-alive 缓存的组件停用时调用。
该钩子在服务器端渲染期间不被调用。
Function
在卸载组件实例之前调用。在这个阶段,实例仍然是完全正常的。
该钩子在服务器端渲染期间不被调用。
Function
卸载组件实例后调用。调用此钩子时,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载。
该钩子在服务器端渲染期间不被调用。
(err: Error, instance: Component, info: string) => ?boolean
当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false
以阻止该错误继续向上传播。
TIP
你可以在此钩子中修改组件的状态。因此在捕获错误时,在模板或渲染函数中有一个条件判断来绕过其它内容就很重要;不然该组件可能会进入一个无限的渲染循环。
错误传播规则
config.errorHandler
被定义,所有的错误仍会发送它,因此这些错误仍然会向单一的分析服务的地方进行汇报。errorCaptured
钩子,则它们将会被相同的错误逐个唤起。errorCaptured
钩子自身抛出了一个错误,则这个新错误和原本被捕获的错误都会发送给全局的 config.errorHandler
。errorCaptured
钩子能够返回 false
以阻止错误继续向上传播。本质上是说“这个错误已经被搞定了且应该被忽略”。它会阻止其它任何会被这个错误唤起的 errorCaptured
钩子和全局的 config.errorHandler
。(e: DebuggerEvent) => void
跟踪虚拟 DOM 重新渲染时调用。钩子接收 debugger event
作为参数。此事件告诉你哪个操作跟踪了组件以及该操作的目标对象和键。
<div id="app"> <button v-on:click="addToCart">Add to cart</button> <p>Cart({{ cart }})</p> </div>
const app = Vue.createApp({ data() { return { cart: 0 } }, renderTracked({ key, target, type }) { console.log({ key, target, type }) /* 当组件第一次渲染时,这将被记录下来: { key: "cart", target: { cart: 0 }, type: "get" } */ }, methods: { addToCart() { this.cart += 1 } } }) app.mount('#app')
(e: DebuggerEvent) => void
当虚拟 DOM 重新渲染为 triggered.Similarly 为renderTracked
,接收 debugger event
作为参数。此事件告诉你是什么操作触发了重新渲染,以及该操作的目标对象和键。
<div id="app"> <button v-on:click="addToCart">Add to cart</button> <p>Cart({{ cart }})</p> </div>
const app = Vue.createApp({ data() { return { cart: 0 } }, renderTriggered({ key, target, type }) { console.log({ key, target, type }) }, methods: { addToCart() { this.cart += 1 /* 这将导致renderTriggered调用 { key: "cart", target: { cart: 1 }, type: "set" } */ } } }) app.mount('#app')
Array<Object>
mixins
选项接收一个混入对象的数组。这些混入对象可以像正常的实例对象一样包含实例选项,这些选项将会被合并到最终的选项中,使用特定的选项合并逻辑。例如,如果 mixin 包含一个 created
钩子,而创建组件本身也有一个,那么两个函数都会被调用。
Mixin 钩子按照传入顺序依次调用,并在调用组件自身的钩子之前被调用。
const mixin = { created: function() { console.log(1) } } Vue.createApp({ created() { console.log(2) }, mixins: [mixin] }) // => 1 // => 2
Object | Function
允许声明扩展另一个组件 (可以是一个简单的选项对象或构造函数)。这主要是为了便于扩展单文件组件。
这和 mixins
类似。
const CompA = { ... } // 在没有调用 `Vue.extend` 时候继承 CompA const CompB = { extends: CompA, ... }
Object | () => Object
Array<string> | { [key: string]: string | Symbol | Object }
这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的 context
特性很相似。
provide
选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。在该对象中你可以使用 ES2015 Symbols 作为 key,但是只在原生支持 Symbol
和 Reflect.ownKeys
的环境下可工作。
inject
选项应该是:
from
property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)default
property 是降级情况下使用的 value提示:提示:
provide
和inject
绑定并不是响应式的。这是刻意为之的。然而,如果你传入了一个响应式的对象,那么其对象的 property 仍是响应式的。
// 父级组件提供 'foo' const Provider = { provide: { foo: 'bar' } // ... } // 子组件注入 'foo' const Child = { inject: ['foo'], created() { console.log(this.foo) // => "bar" } // ... }
利用 ES2015 Symbols、函数 provide
和对象 inject
:
const s = Symbol() const Provider = { provide() { return { [s]: 'foo' } } } const Child = { inject: { s } // ... }
使用一个注入的值作为一个 property 的默认值:
const Child = { inject: ['foo'], props: { bar: { default() { return this.foo } } } }
使用一个注入的值作为数据入口:
const Child = { inject: ['foo'], data() { return { bar: this.foo } } }
注入可以通过设置默认值使其变成可选项:
const Child = { inject: { foo: { default: 'foo' } } }
如果它需要从一个不同名字的 property 注入,则使用 from
来表示其源 property:
const Child = { inject: { foo: { from: 'bar', default: 'foo' } } }
与 prop 的默认值类似,你需要对非原始值使用一个工厂方法:
const Child = { inject: { foo: { from: 'bar', default: () => [1, 2, 3] } } }
Function
setup
函数是一个新的组件选项。它作为在组件内部使用组合式 API 的入口点。
在创建组件实例时,在初始 prop 解析之后立即调用 setup
。在生命周期方面,它是在 beforeCreate 钩子之前调用的。
如果 setup
返回一个对象,则该对象的属性将合并到组件模板的渲染上下文中:
<template> <div>{{ count }} {{ object.foo }}</div> </template> <script> import { ref, reactive } from 'vue' export default { setup() { const count = ref(0) const object = reactive({ foo: 'bar' }) // 暴露到template中 return { count, object } } } </script>
请注意,从 setup
返回的 refs 在模板中访问时会自动展开,因此模板中不需要 .value
。
setup
还可以返回一个渲染函数,该函数可以直接使用在同一作用域中声明的响应式状态:
import { h, ref, reactive } from 'vue' export default { setup() { const count = ref(0) const object = reactive({ foo: 'bar' }) return () => h('div', [count.value, object.foo]) } }
该函数将接收到的 prop 作为其第一个参数:
export default { props: { name: String }, setup(props) { console.log(props.name) } }
请注意,此 props
对象是响应式的——即在传入新的 props 时会对其进行更新,并且可以通过使用 watchEffect
或 watch
进行观测和响应:
export default { props: { name: String }, setup(props) { watchEffect(() => { console.log(`name is: ` + props.name) }) } }
但是,请不要解构 props
对象,因为它会失去响应式:
export default { props: { name: String }, setup({ name }) { watchEffect(() => { console.log(`name is: ` + name) // 没有响应式 }) } }
props
对象在开发过程中对于用户区代码是不可变的 (如果用户代码尝试对其进行更改,则会发出警告)。
第二个参数提供了一个上下文对象,该对象暴露了以前在 this
上暴露的 property 的选择列表:
const MyComponent = { setup(props, context) { context.attrs context.slots context.emit } }
attrs
和 slots
是内部组件实例上相应值的代理。这样可以确保它们即使在更新后也始终会显示最新值,以便我们可以对它们进行结构分解,而不必担心访问老的引用:
const MyComponent = { setup(props, { attrs }) { // 稍后可能会调用的函数 function onClick() { console.log(attrs.foo) // 保证是最新引用 } } }
有很多理由将 props
作为单独的第一个参数而不是将其包含在上下文中:
props
比其他 property 更常见,并且很多情况下组件仅使用 props
。props
作为单独的参数可以使单独键入更容易,而不会弄乱上下文中其他 property 的类型。这也使得在具有 TSX 支持的 setup
、render
和普通功能组件之间保持一致的签名成为可能。string
允许组件模板递归地调用自身。注意,组件在全局用 Vue.createApp({}).component({})
注册时,全局 ID 自动作为组件的 name。
指定 name
选项的另一个好处是便于调试。有名字的组件有更友好的警告信息。另外,当在有 vue-devtools,未命名组件将显示成 <AnonymousComponent>
,这很没有语义。通过提供 name
选项,可以获得更有语义信息的组件树。
Array<string>
['{{', '}}']
Vue.createApp({ // Delimiters changed to ES6 template string style delimiters: ['${', '}'] })
boolean
true
默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs
到 false
,这些默认行为将会被去掉。而通过实例 property $attrs
可以让这些 attribute 生效,且可以通过 v-bind
显性的绑定到非根元素上。
app.component('base-input', { inheritAttrs: false, props: ['label', 'value'], emits: ['input'], template: ` <label> {{ label }} <input v-bind="$attrs" v-bind:value="value" v-on:input="$emit('input', $event.target.value)" > </label> ` })
Object
组件实例观察的数据对象。组件实例代理了对其 data 对象 property 的访问。
Object
当前组件接收到的 props 对象。组件实例代理了对其 props 对象 property 的访问。
any
组件实例使用的根 DOM 元素。对于使用片段的组件,$el
将是Vue用于跟踪组件在DOM中位置的占位符DOM节点。建议使用模板引用直接访问DOM元素,而不是依赖$el
。
Object
用于当前组件实例的初始化选项。需要在选项中包含自定义 property 时会有用处:
const app = Vue.createApp({ customOption: 'foo', created() { console.log(this.$options.customOption) // => 'foo' } })
Vue instance
父实例,如果当前实例有的话。
Vue instance
当前组件树的根组件实例。如果当前实例没有父实例,此实例将会是其自己。
{ [name: string]: (...args: any[]) => Array<VNode> | undefined }
用来访问被插槽分发的内容。每个具名插槽有其相应的 property (例如:v-slot:foo
中的内容将会在 this.$slots.foo
中被找到)。default
property 包括了所有没有被包含在具名插槽中的节点,或 v-slot:default
的内容。
在使用渲染函数书写一个组件时,访问 this.$slots
最有帮助。
<blog-post> <template v-slot:header> <h1>About Me</h1> </template> <template v-slot:default> <p> Here's some page content, which will be included in $slots.default. </p> </template> <template v-slot:footer> <p>Copyright 2020 Evan You</p> </template> </blog-post>
const app = Vue.createApp({}) app.component('blog-post', { render() { return Vue.h('div', [ Vue.h('header', this.$slots.header()), Vue.h('main', this.$slots.default()), Vue.h('footer', this.$slots.footer()) ]) } })
Object
一个对象,持有注册过 ref
attribute 的所有 DOM 元素和组件实例。
Object
包含了父作用域中不作为组件 props 或自定义事件。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定,并且可以通过 v-bind="$attrs"
传入内部组件——在创建高阶的组件时非常有用。
{string | Function} source
{Function | Object} callback
{Object} [options]
{boolean} deep
{boolean} immediate
{string} flush
{Function} unwatch
侦听组件实例上的响应式 property 或函数计算结果的变化。回调函数得到的参数为新值和旧值。我们只能将顶层的 data
、prop
或 computed
property 名作为字符串传递。对于更复杂的表达式,用一个函数取代。
const app = Vue.createApp({ data() { return { a: 1, b: 2, c: { d: 3, e: 4 } } }, created() { // 顶层property 名 this.$watch('a', (newVal, oldVal) => { // 做点什么 }) // 用于监视单个嵌套property 的函数 this.$watch( () => this.c.d, (newVal, oldVal) => { // 做点什么 } ) // 用于监视复杂表达式的函数 this.$watch( // 表达式 `this.a + this.b` 每次得出一个不同的结果时 // 处理函数都会被调用。 // 这就像监听一个未被定义的计算属性 () => this.a + this.b, (newVal, oldVal) => { // 做点什么 } ) } })
当侦听的值是一个对象或者数组时,对其属性或元素的任何更改都不会触发侦听器,因为它们引用相同的对象/数组:
const app = Vue.createApp({ data() { return { article: { text: 'Vue is awesome!' }, comments: ['Indeed!', 'I agree'] } }, created() { this.$watch('article', () => { console.log('Article changed!') }) this.$watch('comments', () => { console.log('Comments changed!') }) }, methods: { // 这些方法不会触发侦听器,因为我们只更改了Object/Array的一个property, // 不是对象/数组本身 changeArticleText() { this.article.text = 'Vue 3 is awesome' }, addComment() { this.comments.push('New comment') }, // 这些方法将触发侦听器,因为我们完全替换了对象/数组 changeWholeArticle() { this.article = { text: 'Vue 3 is awesome' } }, clearComments() { this.comments = [] } } })
$watch
返回一个取消侦听函数,用来停止触发回调:
const app = Vue.createApp({ data() { return { a: 1 } } }) const vm = app.mount('#app') const unwatch = vm.$watch('a', cb) // later, teardown the watcher unwatch()
为了发现对象内部值的变化,可以在选项参数中指定 deep: true
。注意监听数组的变更不需要这么做。
vm.$watch('someObject', callback, { deep: true }) vm.someObject.nestedValue = 123 // callback is fired
在选项参数中指定 immediate: true
将立即以表达式的当前值触发回调:
vm.$watch('a', callback, { immediate: true }) // 立即以 `a` 的当前值触发 `callback`
注意,在带有 immediate
选项时,你不能在第一次回调时取消侦听给定的 property。
// 这会导致报错 const unwatch = vm.$watch( 'value', function() { doSomething() unwatch() }, { immediate: true } )
如果你仍然希望在回调内部调用一个取消侦听的函数,你应该先检查其函数的可用性:
let unwatch = null unwatch = vm.$watch( 'value', function() { doSomething() if (unwatch) { unwatch() } }, { immediate: true } )
flush
选项允许更好地控制回调的时间。它可以设置为pre
、post
或sync
。
默认值为pre
,它指定在呈现之前应调用回调。这允许回调在模板运行之前更新其他值。
值post
可用于将回调延迟到呈现之后。如果回调需要通过$refs
访问更新的DOM或子组件,则应使用此选项。
如果flush
设置为sync
,则只要值发生更改,就会同步调用回调。
对于pre
和post
,回调都使用队列进行缓冲。回调只会添加到队列一次,即使关注的值更改多次。临时值将被跳过,不会传递给回调。
缓冲回调不仅可以提高性能,而且有助于确保数据一致性。在执行数据更新的代码完成之前,不会触发监视程序。
sync
观察者应该谨慎使用,因为他们没有这些好处。
有关`flush`的详细信息,请参见[效果刷新计时](https://www.51coolma.cn/vuejs3/vuejs3-35qs3f4h.html)。
{string} eventName
[...args]
触发当前实例上的事件。附加参数都会传给监听器回调。
只配合一个事件名使用 $emit:
<div id="emit-example-simple"> <welcome-button v-on:welcome="sayHi"></welcome-button> </div>
const app = Vue.createApp({ methods: { sayHi() { console.log('Hi!') } } }) app.component('welcome-button', { template: ` <button v-on:click="$emit('welcome')"> Click me to be welcomed </button> ` }) app.mount('#emit-example-simple')
配合额外的参数使用 $emit
:
<div id="emit-example-argument"> <advice-component v-on:give-advice="showAdvice"></advice-component> </div>
const app = Vue.createApp({ methods: { showAdvice(advice) { alert(advice) } } }) app.component('advice-component', { data() { return { adviceText: 'Some advice' } }, template: ` <div> <input type="text" v-model="adviceText"> <button v-on:click="$emit('give-advice', adviceText)"> Click me for sending advice </button> </div> ` })
迫使组件实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
{Function} [callback]
将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。它跟全局方法 nextTick
一样,不同的是回调的 this
自动绑定到调用它的实例上。
Vue.createApp({ // ... methods: { // ... example() { // modify data this.message = 'changed' // DOM is not updated yet this.$nextTick(function() { // DOM is now updated // `this` is bound to the current instance this.doSomethingElse() }) } }})
string
更新元素的 textContent。如果要更新部分的 textContent
,需要使用 Mustache 插值。
<span v-text="msg"></span> <!-- 等价于 --> <span>{{msg}}</span>
string
更新元素的 innerHTML。注意:内容按普通 HTML 插入 - 不会作为 Vue 模板进行编译。如果试图使用 v-html
组合模板,可以重新考虑是否通过使用组件来替代。
WARNING
在网站上动态渲染任意 HTML 是非常危险的,因为容易导致 XSS 攻击。只在可信内容上使用 v-html
,永不用在用户提交的内容上。
在单文件组件里,scoped
的样式不会应用在 v-html
内部,因为那部分 HTML 没有被 Vue 的模板编译器处理。如果你希望针对 v-html
的内容设置带作用域的 CSS,你可以替换为 CSS modules 或用一个额外的全局 <style>
元素手动设置类似 BEM 的作用域策略。
<div v-html="html"></div>
any
根据表达式的真假值,切换元素的 display
CSS property。
当条件变化时该指令触发过渡效果。
any
根据表达式的真假值来有条件地渲染元素。在切换时元素及它的数据绑定 / 组件被销毁并重建。如果元素是 <template>
,将提取它的内容作为条件块。
当条件变化时该指令触发过渡效果。
当和 v-if
一起使用时,v-for
的优先级比 v-if
更高。详见列表渲染教程
v-if
或 v-else-if
。为 v-if
或者 v-else-if
添加“else 块”。
<div v-if="Math.random() > 0.5"> Now you see me </div> <div v-else> Now you don't </div>
any
v-if
或 v-else-if
。表示 v-if
的“else if 块”。可以链式调用。
<div v-if="type === 'A'"> A </div> <div v-else-if="type === 'B'"> B </div> <div v-else-if="type === 'C'"> C </div> <div v-else> Not A/B/C </div>
Array | Object | number | string | Iterable
基于源数据多次渲染元素或模板块。此指令之值,必须使用特定语法 alias in expression
,为当前遍历的元素提供别名:
<div v-for="item in items"> {{ item.text }} </div>
另外也可以为数组索引指定别名 (或者用于对象的键):
<div v-for="(item, index) in items"></div> <div v-for="(value, key) in object"></div> <div v-for="(value, name, index) in object"></div>
v-for
的默认行为会尝试原地修改元素而不是移动它们。要强制其重新排序元素,你需要用特殊 attribute key
来提供一个排序提示:
<div v-for="item in items" :key="item.id"> {{ item.text }} </div>
v-for
也可以在实现了可迭代协议的值上使用,包括原生的 Map
和 Set
。
v-for
的详细用法可以通过以下链接查看教程详细说明。
@
Function | Inline Statement | Object
event
.stop
- 调用 event.stopPropagation()
。.prevent
- 调用 event.preventDefault()
。.capture
- 添加事件侦听器时使用 capture 模式。.self
- 只当事件是从侦听器绑定的元素本身触发时才触发回调。.{keyAlias}
- 仅当事件是从特定键触发时才触发回调。.once
- 只触发一次回调。.left
- 只当点击鼠标左键时触发。.right
- 只当点击鼠标右键时触发。.middle
- 只当点击鼠标中键时触发。.passive
- { passive: true }
模式添加侦听器绑定事件监听器。事件类型由参数指定。表达式可以是一个方法的名字或一个内联语句,如果没有修饰符也可以省略。
用在普通元素上时,只能监听原生 DOM 事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件。
监听原生 DOM 事件时,方法以事件为唯一的参数。如果使用内联语句,语句可以访问一个 $event
property:v-on:click="handle('ok', $event)"
。
v-on
同样支持不带参数绑定一个事件/监听器键值对的对象。注意当使用对象语法时,是不支持任何修饰器的。
<!-- 方法处理器 --> <button v-on:click="doThis"></button> <!-- 动态事件 --> <button v-on:[event]="doThis"></button> <!-- 内联语句 --> <button v-on:click="doThat('hello', $event)"></button> <!-- 缩写 --> <button @click="doThis"></button> <!-- 动态事件缩写 --> <button @[event]="doThis"></button> <!-- 停止冒泡 --> <button @click.stop="doThis"></button> <!-- 阻止默认行为 --> <button @click.prevent="doThis"></button> <!-- 阻止默认行为,没有表达式 --> <form @submit.prevent></form> <!-- 串联修饰符 --> <button @click.stop.prevent="doThis"></button> <!-- 键修饰符,键别名 --> <input @keyup.enter="onEnter" /> <!-- 点击回调只会触发一次 --> <button v-on:click.once="doThis"></button> <!-- 对象语法 --> <button v-on="{ mousedown: doThis, mouseup: doThat }"></button>
在子组件上监听自定义事件 (当子组件触发“my-event”时将调用事件处理器):
<my-component @my-event="handleThis"></my-component> <!-- 内联语句 --> <my-component @my-event="handleThis(123, $event)"></my-component>
:
any (with argument) | Object (without argument)
attrOrProp (optional)
.camel
- 将 kebab-case attribute 名转换为 camelCase。动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。
在绑定 class
或 style
attribute 时,支持其它类型的值,如数组或对象。可以通过下面的教程链接查看详情。
在绑定 prop 时,prop 必须在子组件中声明。可以用修饰符指定不同的绑定类型。
没有参数时,可以绑定到一个包含键值对的对象。注意此时 class
和 style
绑定不支持数组和对象。
<!-- 绑定 attribute --> <img v-bind:src="imageSrc" /> <!-- 动态 attribute 名 --> <button v-bind:[key]="value"></button> <!-- 缩写 --> <img :src="imageSrc" /> <!-- 动态 attribute 名缩写 --> <button :[key]="value"></button> <!-- 内联字符串拼接 --> <img :src="'/path/to/images/' + fileName" /> <!-- class 绑定 --> <div :class="{ red: isRed }"></div> <div :class="[classA, classB]"></div> <div :class="[classA, { classB: isB, classC: isC }]"> <!-- style 绑定 --> <div :style="{ fontSize: size + 'px' }"></div> <div :style="[styleObjectA, styleObjectB]"></div> <!-- 绑定一个全是 attribute 的对象 --> <div v-bind="{ id: someProp, 'other-attr': otherProp }"></div> <!-- prop 绑定。"prop" 必须在 my-component 声明 --> <my-component :prop="someThing"></my-component> <!-- 通过 $props 将父组件的 props 一起传给子组件 --> <child-component v-bind="$props"></child-component> <!-- XLink --> <svg><a :xlink:special="foo"></a></svg> </div>
.camel
修饰符允许在使用 DOM 模板时将 v-bind
property 名称驼峰化,例如 SVG 的 viewBox
property:
<svg :view-box.camel="viewBox"></svg>
在使用字符串模板或通过 vue-loader
/ vueify
编译时,无需使用 .camel
。
<input>
<select>
<textarea>
在表单控件或者组件上创建双向绑定。细节请看下面的教程链接。
#
default
)<template>
提供具名插槽或需要接收 prop 的插槽。
<!-- 具名插槽 --> <base-layout> <template v-slot:header> Header content </template> <template v-slot:default> Default slot content </template> <template v-slot:footer> Footer content </template> </base-layout> <!-- 接收 prop 的具名插槽 --> <infinite-scroll> <template v-slot:item="slotProps"> <div class="item"> {{ slotProps.item.text }} </div> </template> </infinite-scroll> <!-- 接收 prop 的默认插槽,使用了解构 --> <mouse-position v-slot="{ x, y }"> Mouse position: {{ x }}, {{ y }} </mouse-position>
更多细节请查阅以下链接。
跳过这个元素和它的子元素的编译过程。可以用来显示原始 Mustache 标签。跳过大量没有指令的节点会加快编译。
<span v-pre>{{ this will not be compiled }}</span>
这个指令保持在元素上直到关联组件实例结束编译。和 CSS 规则如 [v-cloak] { display: none }
一起用时,这个指令可以隐藏未编译的 Mustache 标签直到组件实例准备完毕。
[v-cloak] { display: none; }
<div v-cloak> {{ message }} </div>
<div> 不会显示,直到编译结束。
只渲染元素和组件一次。随后的重新渲染,元素/组件及其所有的子节点将被视为静态内容并跳过。这可以用于优化更新性能。
<!-- 单个元素 --> <span v-once>This will never change: {{msg}}</span> <!-- 有子元素 --> <div v-once> <h1>comment</h1> <p>{{msg}}</p> </div> <!-- 组件 --> <my-component v-once :comment="msg"></my-component> <!-- `v-for` 指令 --> <ul> <li v-for="i in list" v-once>{{i}}</li> </ul>
注意:本节仅影响直接在页面的 HTML 中写入 Vue 模板的情况。
<ul>
、<ol>
、<table>
和 <select>
等,对哪些元素可以出现在它们内部有限制,而某些元素 (如:<li>
、<tr>
和 <option>
只能出现在某些其他元素中。作为解决方法,我们可以对以下元素使用 v-is
指令:<table> <tr v-is="'blog-post-row'"></tr></table>
WARNING
v-is
函数类似于动态 2.x :is
绑定——因此要按组件的注册名称渲染组件,其值应为 JavaScript 字符串文本:
<!-- 不正确,不会渲染任何内容 --><tr v-is="blog-post-row"></tr><!-- 正确 --><tr v-is="'blog-post-row'"></tr>
number | string
key
的特殊 attribute 主要用在 Vue 的虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes。如果不使用 key,Vue 会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法。而使用 key 时,它会基于 key 的变化重新排列元素顺序,并且会移除/销毁 key 不存在的元素。
有相同父元素的子元素必须有独特的 key。重复的 key 会造成渲染错误。
最常见的用例是结合 v-for
:
<ul> <li v-for="item in items" :key="item.id">...</li> </ul>
它也可以用于强制替换元素/组件而不是重复使用它。当你遇到如下场景时它可能会很有用:
例如:
<transition> <span :key="text">{{ text }}</span> </transition>
当 text
发生改变时,<span>
总是会被替换而不是被修改,因此会触发过渡。
string | Function
ref
被用来给元素或子组件注册引用信息。引用信息将会注册在父组件的 $refs
对象上。如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例:
<!-- vm.$refs.p 会是 DOM 节点 --> <p ref="p">hello</p> <!-- vm.$refs.child 会是子组件实例 --> <child-component ref="child"></child-component> <!-- 当动态绑定时,我们可以将ref定义为回调函数,显式地传递元素或组件实例 --> <child-component :ref="(el) => child = el"></child-component>
当 v-for
用于元素或组件的时候,引用信息将是包含 DOM 节点或组件实例的数组。
关于 ref 注册时间的重要说明:因为 ref 本身是作为渲染结果被创建的,在初始渲染的时候你不能访问它们 - 它们还不存在!$refs
也是非响应式的,因此你不应该试图用它在模板中做数据绑定。
string | Object (component’s options object)
使用动态组件。
例如:
<!-- component changes when currentView changes --><component :is="currentView"></component>
更多的使用细节,请移步至下面的链接。
is
- string | Component
渲染一个“元组件”为动态组件。依 is
的值,来决定哪个组件被渲染。is
的值是一个字符串,它既可以是 HTML 标签名称也可以是组件名称。
<!-- 动态组件由 vm 实例的 `componentId` property 控制 --> <component :is="componentId"></component> <!-- 也能够渲染注册过的组件或 prop 传入的组件--> <component :is="$options.components.child"></component> <!-- 可以通过字符串引用组件 --> <component :is="condition ? 'FooComponent' : 'BarComponent'"></component> <!-- 可以用来渲染原生 HTML 元素 --> <component :is="href ? 'a' : 'span'"></component>
name
- string
用于自动生成 CSS 过渡类名。例如:name: 'fade'
将自动拓展为 .fade-enter
,.fade-enter-active
等。appear
- boolean
,是否在初始渲染时使用过渡。默认为 false
。persisted
- boolean
。如果是 true,表示这是一个不真实插入/删除元素的转换,而是切换显示/隐藏状态。过渡钩子被注入,但渲染器将跳过。相反,自定义指令可以通过调用注入的钩子 (例如 v-show
) 来控制转换。css
- boolean
。是否使用 CSS 过渡类。默认为 true
。如果设置为 false
,将只通过组件事件触发注册的 JavaScript 钩子。type
- string
。指定过渡事件类型,侦听过渡何时结束。有效值为 "transition"
和 "animation"
。默认 Vue.js 将自动检测出持续时间长的为过渡事件类型。mode
- string
控制离开/进入过渡的时间序列。有效的模式有 "out-in"
和 "in-out"
;默认同时进行。duration
- number | {
enter : number,
leave : number }
。指定过渡的持续时间。默认情况下,Vue 会等待过渡所在根元素的第一个 transitionend
或 animationend
事件。enter-from-class
- string
leave-from-class
- string
appear-class
- string
enter-to-class
- string
leave-to-class
- string
appear-to-class
- string
enter-active-class
- string
leave-active-class
- string
appear-active-class
- string
before-enter
before-leave
enter
leave
appear
after-enter
after-leave
after-appear
enter-cancelled
leave-cancelled
(仅 v-show
)appear-cancelled
<transition>
元素作为单个元素/组件的过渡效果。<transition>
只会把过渡效果应用到其包裹的内容上,而不会额外渲染 DOM 元素,也不会出现在可被检查的组件层级中。
<!-- 单个元素 --> <transition> <div v-if="ok">toggled content</div> </transition> <!-- 动态组件 --> <transition name="fade" mode="out-in" appear> <component :is="view"></component> </transition> <!-- 事件钩子 --> <div id="transition-demo"> <transition @after-enter="transitionComplete"> <div v-show="ok">toggled content</div> </transition> </div>
const app = Vue.createApp({ ... methods: { transitionComplete (el) { // 因为传递了'el'的DOM元素作为参数 } } ... }) app.mount('#transition-demo')
tag
- string
,默认为 span
。move-class
- 覆盖移动过渡期间应用的 CSS 类。mode
,其他 attribute 和 <transition>
相同。<transition>
相同。<transition-group>
元素作为多个元素/组件的过渡效果。<transition-group>
渲染一个真实的 DOM 元素。默认渲染 <span>
,可以通过 tag
attribute 配置哪个元素应该被渲染。
注意,每个 <transition-group>
的子节点必须有独立的 key,动画才能正常工作
<transition-group>
支持通过 CSS transform 过渡移动。当一个子节点被更新,从屏幕上的位置发生变化,它会被应用一个移动中的 CSS 类 (通过 name
attribute 或配置 move-class
attribute 自动生成)。如果 CSS transform
property 是“可过渡”property,当应用移动类时,将会使用 FLIP 技术使元素流畅地到达动画终点。
<transition-group tag="ul" name="slide"> <li v-for="item in items" :key="item.id"> {{ item.text }} </li> </transition-group>
include
- string | RegExp | Array
。只有名称匹配的组件会被缓存。exclude
- string | RegExp | Array
。任何名称匹配的组件都不会被缓存。max
- number | string
。最多可以缓存多少组件实例。<keep-alive>
包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。和 <transition>
相似,<keep-alive>
是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。
当组件在 <keep-alive>
内被切换,它的 activated
和 deactivated
这两个生命周期钩子函数将会被对应执行。
主要用于保留组件状态或避免重新渲染。
<!-- 基本 --> <keep-alive> <component :is="view"></component> </keep-alive> <!-- 多个条件判断的子组件 --> <keep-alive> <comp-a v-if="a > 1"></comp-a> <comp-b v-else></comp-b> </keep-alive> <!-- 和 `<transition>` 一起使用 --> <transition> <keep-alive> <component :is="view"></component> </keep-alive> </transition>
注意,<keep-alive>
是用在其一个直属的子组件被切换的情形。如果你在其中有 v-for
则不会工作。如果有上述的多个条件性的子元素,<keep-alive>
要求同时只有一个子元素被渲染。
include
和 exclude
The include
和 exclude
prop 允许组件有条件地缓存。二者都可以用逗号分隔字符串、正则表达式或一个数组来表示:
<!-- 逗号分隔字符串 --> <keep-alive include="a,b"> <component :is="view"></component> </keep-alive> <!-- regex (使用 `v-bind`) --> <keep-alive :include="/a|b/"> <component :is="view"></component> </keep-alive> <!-- Array (使用 `v-bind`) --> <keep-alive :include="['a', 'b']"> <component :is="view"></component> </keep-alive>
匹配首先检查组件自身的 name
选项,如果 name
选项不可用,则匹配它的局部注册名称 (父组件 components
选项的键值)。匿名组件不能被匹配。
max
最多可以缓存多少组件实例。一旦这个数字达到了,在新实例被创建之前,已缓存组件中最久没有被访问的实例会被销毁掉。
<keep-alive :max="10"> <component :is="view"></component> </keep-alive>
WARNING
<keep-alive>
不会在函数式组件中正常工作,因为它们没有缓存实例。
name
- string
,用于具名插槽<slot>
元素作为组件模板之中的内容分发插槽。<slot>
元素自身将被替换。
详细用法,请参考下面教程的链接。
to
- string
。需要 prop,必须是有效的查询选择器或 HTMLElement (如果在浏览器环境中使用)。指定将在其中移动 <teleport>
内容的目标元素 <!-- 正确 --> <teleport to="#some-id" /> <teleport to=".some-class" /> <teleport to="[data-teleport]" /> <!-- 错误 --> <teleport to="h1" /> <teleport to="some-string" />
disabled
- boolean
。此可选属性可用于禁用 <teleport>
的功能,这意味着其插槽内容将不会移动到任何位置,而是在您在周围父组件中指定了 <teleport>
的位置渲染。 <teleport to="#popup" :disabled="displayVideoInline"> <video src="./my-movie.mp4"> </teleport>
请注意,这将移动实际的 DOM 节点,而不是被销毁和重新创建,并且它还将保持任何组件实例的活动状态。所有有状态的 HTML 元素 (即播放的视频) 都将保持其状态。
本节例子中代码使用的单文件组件语法
reactive
返回对象的响应式副本
const obj = reactive({ count: 0 })
响应式转换是“深层”的——它影响所有嵌套 property。在基于 ES2015 Proxy 的实现中,返回的代理是不等于原始对象。建议只使用响应式代理,避免依赖原始对象。
类型声明:
function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
readonly
获取一个对象 (响应式或纯对象) 或 ref 并返回原始代理的只读代理。只读代理是深层的:访问的任何嵌套 property 也是只读的。
const original = reactive({ count: 0 })const copy = readonly(original)watchEffect(() => { // 适用于响应性追踪 console.log(copy.count)})// 变更original 会触发侦听器依赖副本original.count++// 变更副本将失败并导致警告copy.count++ // 警告!
isProxy
检查对象是 reactive
还是 readonly
创建的代理
isReactive
检查对象是否是 reactive
创建的响应式 proxy。
import { reactive, isReactive } from 'vue'export default { setup() { const state = reactive({ name: 'John' }) console.log(isReactive(state)) // -> true }}
如果 proxy 是 readonly
创建的,但还包装了由 reactive
创建的另一个 proxy,它也会返回 true
。
import { reactive, isReactive, readonly } from 'vue'export default { setup() { const state = reactive({ name: 'John' }) // 从普通对象创建的只读代理 const plain = readonly({ name: 'Mary' }) console.log(isReactive(plain)) // -> false // 从响应式代理创建的只读代理 const stateCopy = readonly(state) console.log(isReactive(stateCopy)) // -> true }}
isReadonly
检查对象是否是由readonly
创建的只读代理。
toRaw
返回 reactive
或 readonly
代理的原始对象。这是一个转义口,可用于临时读取而不会引起代理访问/跟踪开销,也可用于写入而不会触发更改。不建议保留对原始对象的持久引用。请谨慎使用。
const foo = {}const reactiveFoo = reactive(foo)console.log(toRaw(reactiveFoo) === foo) // true
markRaw
标记一个对象,使其永远不会转换为代理。返回对象本身。
const foo = markRaw({})console.log(isReactive(reactive(foo))) // false// 嵌套在其他响应式对象中时也可以使用const bar = reactive({ foo })console.log(isReactive(bar.foo)) // false
WARNING
下方的 markRaw
和 shallowXXX API 使你可以有选择地选择退出默认的深度响应式/只读转换,并将原始的,非代理的对象嵌入状态图中。它们可以在各种情况下使用:
它们被认为是高阶的,因为原始选择退出仅在根级别,因此,如果将嵌套的、未标记的原始对象设置为响应式对象,然后再次访问它,则可以得到代理版本。这可能会导致本源危害——即执行依赖于对象本身但同时使用同一对象的原始版本和代理版本的操作:
const foo = markRaw({ nested: {}})const bar = reactive({ // 虽然 `foo` 被标记为原始,foo.nested 不是。 nested: foo.nested})console.log(foo.nested === bar.nested) // false
本源危害通常很少见。然而,为了在安全地避免本源危害的同时正确地使用这些 API,需要对响应性系统的工作原理有一个坚实的理解。
shallowReactive
创建一个响应式代理,该代理跟踪其自身 property 的响应性,但不执行嵌套对象的深度响应式转换 (暴露原始值)。
const state = shallowReactive({ foo: 1, nested: { bar: 2 }})// 改变状态本身的性质是响应式的state.foo++// ...但是不转换嵌套对象isReactive(state.nested) // falsestate.nested.bar++ // 非响应式
shallowReadonly
创建一个代理,使其自身的 property 为只读,但不执行嵌套对象的深度只读转换 (暴露原始值)。
const state = shallowReadonly({ foo: 1, nested: { bar: 2 }})// 改变状态本身的property将失败state.foo++// ...但适用于嵌套对象isReadonly(state.nested) // falsestate.nested.bar++ // 适用
本节例子中代码使用的单文件组件语法
ref
接受一个内部值并返回一个响应式且可变的 ref 对象。ref 对象具有指向内部值的单个 property .value
。
示例:
const count = ref(0)console.log(count.value) // 0count.value++console.log(count.value) // 1
如果将对象分配为 ref 值,则可以通过 reactive 方法使该对象具有高度的响应式。
类型声明:
interface Ref<T> { value: T}function ref<T>(value: T): Ref<T>
有时我们可能需要为 ref 的内部值指定复杂类型。我们可以通过在调用 ref
来覆盖默认推断时传递一个泛型参数来简洁地做到这一点:
const foo = ref<string | number>('foo') // foo's type: Ref<string | number>foo.value = 123 // ok!
如果泛型的类型未知,建议将 ref
转换为 Ref<T>
:
function useState<State extends string>(initial: State) { const state = ref(initial) as Ref<State> // state.value -> State extends string return state}
unref
如果参数为 ref
,则返回内部值,否则返回参数本身。这是 val = isRef(val) ? val.value : val
。
function useFoo(x: number | Ref<number>) { const unwrapped = unref(x) // unwrapped 确保现在是数字类型}
toRef
可以用来为源响应式对象上的 property 性创建一个 ref
。然后可以将 ref 传递出去,从而保持对其源 property 的响应式连接。
const state = reactive({ foo: 1, bar: 2})const fooRef = toRef(state, 'foo')fooRef.value++console.log(state.foo) // 2state.foo++console.log(fooRef.value) // 3
当您要将 prop 的 ref 传递给复合函数时,toRef
很有用:
export default { setup(props) { useSomeFeature(toRef(props, 'foo')) }}
toRefs
将响应式对象转换为普通对象,其中结果对象的每个 property 都是指向原始对象相应 property 的ref
。
const state = reactive({ foo: 1, bar: 2})const stateAsRefs = toRefs(state)/*Type of stateAsRefs:{ foo: Ref<number>, bar: Ref<number>}*/// ref 和 原始property “链接”state.foo++console.log(stateAsRefs.foo.value) // 2stateAsRefs.foo.value++console.log(state.foo) // 3
当从合成函数返回响应式对象时,toRefs
非常有用,这样消费组件就可以在不丢失响应性的情况下对返回的对象进行分解/扩散:
function useFeatureX() { const state = reactive({ foo: 1, bar: 2 }) // 逻辑运行状态 // 返回时转换为ref return toRefs(state)}export default { setup() { // 可以在不失去响应性的情况下破坏结构 const { foo, bar } = useFeatureX() return { foo, bar } }}
isRef
检查值是否为ref对象。
customRef
创建一个自定义的 ref,并对其依赖项跟踪和更新触发进行显式控制。它需要一个工厂函数,该函数接收 track
和 trigger
函数作为参数,并应返回一个带有 get
和 set
的对象。
v-model
使用自定义 ref 实现 debounce
的示例: <input v-model="text" />
function useDebouncedRef(value, delay = 200) { let timeout return customRef((track, trigger) => { return { get() { track() return value }, set(newValue) { clearTimeout(timeout) timeout = setTimeout(() => { value = newValue trigger() }, delay) } } }) } export default { setup() { return { text: useDebouncedRef('hello') } } }
Typing:
function customRef<T>(factory: CustomRefFactory<T>): Ref<T>type CustomRefFactory<T> = ( track: () => void, trigger: () => void) => { get: () => T set: (value: T) => void}
shallowRef
创建一个 ref,它跟踪自己的 .value
更改,但不会使其值成为响应式的。
const foo = shallowRef({})// 改变 ref 的值是响应式的foo.value = {}// 但是这个值不会被转换。isReactive(foo.value) // false
triggerRef
手动执行与 shallowRef
](#shallowref) 关联的任何效果。
const shallow = shallowRef({ greet: 'Hello, world'})// 第一次运行时记录一次 "Hello, world"watchEffect(() => { console.log(shallow.value.greet)})// 这不会触发作用,因为 ref 很浅层shallow.value.greet = 'Hello, universe'// 记录 "Hello, universe"triggerRef(shallow)
本节例子中代码使用的单文件组件语法
computed
使用 getter 函数,并为从 getter 返回的值返回一个不变的响应式 ref 对象。
const count = ref(1)const plusOne = computed(() => count.value + 1)console.log(plusOne.value) // 2plusOne.value++ // error
或者,它可以使用具有 get
和 set
函数的对象来创建可写的 ref 对象。
const count = ref(1)const plusOne = computed({ get: () => count.value + 1, set: val => { count.value = val - 1 }})plusOne.value = 1console.log(count.value) // 0
类型声明:
// read-onlyfunction computed<T>(getter: () => T): Readonly<Ref<Readonly<T>>>// writablefunction computed<T>(options: { get: () => T; set: (value: T) => void }): Ref<T>
watchEffect
在响应式地跟踪其依赖项时立即运行一个函数,并在更改依赖项时重新运行它。
const count = ref(0)watchEffect(() => console.log(count.value))// -> logs 0setTimeout(() => { count.value++ // -> logs 1}, 100)
类型声明:
function watchEffect( effect: (onInvalidate: InvalidateCbRegistrator) => void, options?: WatchEffectOptions): StopHandleinterface WatchEffectOptions { flush?: 'pre' | 'post' | 'sync' // default: 'pre' onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void}interface DebuggerEvent { effect: ReactiveEffect target: any type: OperationTypes key: string | symbol | undefined}type InvalidateCbRegistrator = (invalidate: () => void) => voidtype StopHandle = () => void
watch
watch
API 与选项式 API this.$watch (以及相应的 watch 选项) 完全等效。watch
需要侦听特定的 data 源,并在单独的回调函数中副作用。默认情况下,它也是惰性的——即,回调是仅在侦听源发生更改时调用。
watch
允许我们:侦听器 data 源可以是返回值的 getter 函数,也可以是 ref:
// 侦听一个getterconst state = reactive({ count: 0 })watch( () => state.count, (count, prevCount) => { /* ... */ })// 直接侦听一个refconst count = ref(0)watch(count, (count, prevCount) => { /* ... */})
侦听器还可以使用数组同时侦听多个源:
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */})
watchEffect
共享行为watch
与 watchEffect
在手动停止,副作用无效 (将 onInvalidate
作为第三个参数传递给回调),flush timing 和 debugging 有共享行为。
类型声明:
// 侦听单一源function watch<T>( source: WatcherSource<T>, callback: ( value: T, oldValue: T, onInvalidate: InvalidateCbRegistrator ) => void, options?: WatchOptions): StopHandle// 侦听多个源function watch<T extends WatcherSource<unknown>[]>( sources: T callback: ( values: MapSources<T>, oldValues: MapSources<T>, onInvalidate: InvalidateCbRegistrator ) => void, options? : WatchOptions): StopHandletype WatcherSource<T> = Ref<T> | (() => T)type MapSources<T> = { [K in keyof T]: T[K] extends WatcherSource<infer V> ? V : never}// 参见 `watchEffect` 类型声明共享选项interface WatchOptions extends WatchEffectOptions { immediate?: boolean // default: false deep?: boolean}
参考:watch
指南
本节例子中代码使用的单文件组件语法
setup
一个组件选项,在创建组件之前执行,一旦 props
被解析,并作为组合式 API 的入口点
{Data} props
{SetupContext} context
interface Data { [key: string]: unknown}interface SetupContext { attrs: Data slots: Slots emit: (event: string, ...args: unknown[]) => void}function setup(props: Data, context: SetupContext): Data
TIP
若要获取传递给 setup()
的参数的类型推断,请使用 defineComponent 是需要的。
使用模板:
<!-- MyBook.vue --> <template> <div>{{ readersNumber }} {{ book.title }}</div> </template> <script> import { ref, reactive } from 'vue' export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // expose to template return { readersNumber, book } } } </script>
使用渲染函数:
// MyBook.vue import { h, ref, reactive } from 'vue' export default { setup() { const readersNumber = ref(0) const book = reactive({ title: 'Vue 3 Guide' }) // 请注意,我们需要在这里显式地暴露ref值 return () => h('div', [readersNumber.value, book.title]) } }
可以使用直接导入的 onX
函数注册生命周期钩子:
import { onMounted, onUpdated, onUnmounted } from 'vue'const MyComponent = { setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) onUnmounted(() => { console.log('unmounted!') }) }}
这些生命周期钩子注册函数只能在 setup()
期间同步使用,因为它们依赖于内部全局状态来定位当前活动实例 (此时正在调用其 setup()
的组件实例)。在没有当前活动实例的情况下调用它们将导致错误。
组件实例上下文也是在生命周期钩子的同步执行期间设置的,因此在生命周期钩子内同步创建的侦听器和计算属性也会在组件卸载时自动删除。
选项 API 生命周期选项和组合式 API 之间的映射
beforeCreate
-> use setup()
created
-> use setup()
beforeMount
-> onBeforeMount
mounted
-> onMounted
beforeUpdate
-> onBeforeUpdate
updated
-> onUpdated
beforeUnmount
-> onBeforeUnmount
unmounted
-> onUnmounted
errorCaptured
-> onErrorCaptured
renderTracked
-> onRenderTracked
renderTriggered
-> onRenderTriggered
provide
和 inject
启用依赖注入。只有在使用当前活动实例的 setup()
期间才能调用这两者。
interface InjectionKey<T> extends Symbol {}function provide<T>(key: InjectionKey<T> | string, value: T): void// without default valuefunction inject<T>(key: InjectionKey<T> | string): T | undefined// with default valuefunction inject<T>(key: InjectionKey<T> | string, defaultValue: T): T
Vue 提供了一个 InjectionKey
接口,该接口是扩展 Symbol
的泛型类型。它可用于在提供者和消费者之间同步注入值的类型:
import { InjectionKey, provide, inject } from 'vue'const key: InjectionKey<string> = Symbol()provide(key, 'foo') // 提供非字符串值将导致错误const foo = inject(key) // foo 的类型: string | undefined
如果使用字符串 key 或非类型化 symbols,则需要显式声明注入值的类型:
const foo = inject<string>('foo') // string | undefined
:
getCurrentInstance
getCurrentInstance
允许访问对高级使用或库创建者有用的内部组件实例。
import { getCurrentInstance } from 'vue'const MyComponent = { setup() { const internalInstance = getCurrentInstance() internalInstance.appContext.config.globalProperties // access to globalProperties }}
getCurrentInstance
仅在安装或生命周期挂钩期间有效。
在安装程序或生命周期挂钩之外使用时,请在
setup
上调用getCurrentInstance()
,并改用该实例。
const MyComponent = { setup() { const internalInstance = getCurrentInstance() // works const id = useComponentId() // works const handleClick = () => { getCurrentInstance() // doesn't work useComponentId() // doesn't work internalInstance // works } onMounted(() => { getCurrentInstance() // works }) return () => h( 'button', { onClick: handleClick }, `uid: ${id}` ) }}// also works if called on a composablefunction useComponentId() { return getCurrentInstance().uid}
这里是官方的 Vue 特有代码的风格指南。如果在工程中使用 Vue,为了回避错误、小纠结和反模式,该指南是份不错的参考。不过我们也不确信风格指南的所有内容对于所有的团队或工程都是理想的。所以根据过去的经验、周边的技术栈、个人价值观做出有意义的偏差是可取的。
对于其绝大部分,我们也总体上避免就 JavaScript 或 HTML 的本身提出建议。我们不介意你是否使用分号或结尾的逗号。我们不介意你在 HTML attribute 中使用单引号还是双引号。不过当我们发现在 Vue 的情景下有帮助的特定模式时,也会存在例外。
最终,我们把所有的规则归为了四个大类:
这些规则会帮你规避错误,所以学习并接受它们带来的全部代价吧。这里面可能存在例外,但应该非常少,且只有你同时精通 JavaScript 和 Vue 才可以这样做。
这些规则能够在绝大多数工程中改善可读性和开发体验。即使你违反了,代码还是能照常运行,但例外应该尽可能少且有合理的理由。
当存在多个同样好的选项,选任意一个都可以确保一致性。在这些规则里,我们描述了每个选项并建议一个默认的选择。也就是说只要保持一致且理由充分,你可以随意在你的代码库中做出不同的选择。请务必给出一个好的理由!通过接受社区的标准,你将会:
有些 Vue 特性的存在是为了照顾极端情况或帮助老代码的平稳迁移。当被过度使用时,这些特性会让你的代码难于维护甚至变成 bug 的来源。这些规则是为了给有潜在风险的特性敲个警钟,并说明它们什么时候不应该使用以及为什么。
组件名应该始终是多个单词的,根组件 App
以及 <transition>
、<component>
之类的 Vue 内置组件除外。
这样做可以避免跟现有的以及未来的 HTML 元素相冲突,因为所有的 HTML 元素名称都是单个单词的。
app.component('todo', { // ...})
export default { name: 'Todo', // ...}
app.component('todo-item', { // ...})
export default { name: 'TodoItem', // ...}
Prop 定义应尽量详细
在你提交的代码中,prop 的定义应该尽量详细,至少需要指定其类型。
详解
细致的 prop 定义有两个好处:
它们写明了组件的 API,所以很容易看懂组件的用法;
在开发环境下,如果向一个组件提供格式不正确的 prop,Vue 将会告警,以帮助你捕获潜在的错误来源。
// 这样做只有开发原型系统时可以接受props: ['status']
props: { status: String}
// 更好的例子props: { status: { type: String, required: true, validator: value => { return [ 'syncing', 'synced', 'version-conflict', 'error' ].includes(value) } }}
v-for
设置 key 值必要总是用 key
配合 v-for
在组件上总是必须用 key
配合 v-for
,以便维护内部组件及其子树的状态。甚至在元素上维护可预测的行为,比如动画中的对象固化 (object constancy) ,也是一种好的做法。
详解假设你有一个待办事项列表:
data() { return { todos: [ { id: 1, text: 'Learn to use v-for' }, { id: 2, text: 'Learn to use key' } ] }}
然后你把它们按照字母顺序排序。在更新 DOM 的时候,Vue 将会优化渲染把可能的 DOM 变更降到最低。即可能删掉第一个待办事项元素,然后把它重新加回到列表的最末尾。这里的问题在于,不要删除仍然会留在 DOM 中的元素。比如你想使用 <transition-group> 给列表加过渡动画,或想在被渲染元素是 <input> 时保持聚焦。在这些情况下,为每一个项目添加一个唯一的键值 (比如 :key="todo.id") 将会让 Vue 知道如何使行为更容易预测。
详解
假设你有一个待办事项列表:JavaScript
data() {
return {
todos: [
{
id: 1,
text: 'Learn to use v-for'
},
{
id: 2,
text: 'Learn to use key'
}
]
}
}
然后你把它们按照字母顺序排序。在更新 DOM 的时候,Vue 将会优化渲染把可能的 DOM 变更降到最低。即可能删掉第一个待办事项元素,然后把它重新加回到列表的最末尾。
这里的问题在于,不要删除仍然会留在 DOM 中的元素。比如你想使用 <transition-group> 给列表加过渡动画,或想在被渲染元素是 <input> 时保持聚焦。在这些情况下,为每一个项目添加一个唯一的键值 (比如 :key="todo.id") 将会让 Vue 知道如何使行为更容易预测。
根据我们的经验,最好始终添加一个唯一的键值,以便你和你的团队永远不必担心这些极端情况。也在少数对性能有严格要求的情况下,为了避免对象固化,你可以刻意做一些非常规的处理。根据我们的经验,最好始终添加一个唯一的键值,以便你和你的团队永远不必担心这些极端情况。也在少数对性能有严格要求的情况下,为了避免对象固化,你可以刻意做一些非常规的处理。
<ul> <li v-for="todo in todos"> {{ todo.text }} </li></ul>
<ul> <li v-for="todo in todos" :key="todo.id" > {{ todo.text }} </li></ul>
v-if
和 v-for
一起使用必要永远不要把 v-if
和 v-for
同时用在同一个元素上。
一般我们在两种常见的情况下会倾向于这样做:
v-for="user in users" v-if="user.isActive"
)。在这种情形下,请将 users
替换为一个计算属性 (比如 activeUsers
),让其返回过滤后的列表。v-for="user in users" v-if="shouldShowUsers"
)。这种情形下,请将 v-if
移动至容器元素上 (比如 ul
、ol
详解当 Vue 处理指令时,v-for
比 v-if
具有更高的优先级,所以这个模板:
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id" > {{ user.name }} </li></ul>
将会经过如下运算:
this.users.map(user => { if (user.isActive) { return user.name }})
因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。通过将其更换为在如下的一个计算属性上遍历:
computed: { activeUsers() { return this.users.filter(user => user.isActive) }}
<ul> <li v-for="user in activeUsers" :key="user.id" > {{ user.name }} </li></ul>
我们将会获得如下好处:
users
数组发生相关变化时才被重新运算,过滤更高效。v-for="user in activeUsers"
之后,我们在渲染的时候只遍历活跃用户,渲染更高效。<ul><liv-for="user in users"v-if="shouldShowUsers":key="user.id">{{ user.name }}</li></ul>
更新为:
<ul v-if="shouldShowUsers"><liv-for="user in users":key="user.id">{{ user.name }}</li></ul>
详解
当 Vue 处理指令时,v-for
比v-if
具有更高的优先级,所以这个模板:html
lt;ul&
lt;li
v-for="user in users"
v-if="user.isActive"
:key="user.id"
{{ user.name }}
lt;/li&
lt;/ul&
将会经过如下运算:js
this.users.map(user => {
if (user.isActive) {
return user.name
}
})
因此哪怕我们只渲染出一小部分用户的元素,也得在每次重渲染的时候遍历整个列表,不论活跃用户是否发生了变化。
通过将其更换为在如下的一个计算属性上遍历:js
computed: {
activeUsers() {
return this.users.filter(user => user.isActive)
}
}html
lt;ul&
lt;li
v-for="user in activeUsers"
:key="user.id"
{{ user.name }}
lt;/li&
lt;/ul&
我们将会获得如下好处:
users
数组发生相关变化时才被重新运算,过滤更高效。v-for="user in activeUsers"
之后,我们在渲染的时候只遍历活跃用户,渲染更高效。<ul><liv-for="user in users"v-if="shouldShowUsers":key="user.id">{{ user.name }}</li></ul>
更新为:
<ul v-if="shouldShowUsers"><liv-for="user in users":key="user.id">{{ user.name }}</li></ul>
通过将 v-if
移动到容器元素,我们不会再对列表中的每个用户检查 shouldShowUsers
。取而代之的是,我们只检查它一次,且不会在 shouldShowUsers
为否的时候运算 v-for
。通过将 v-if
移动到容器元素,我们不会再对列表中的每个用户检查 shouldShowUsers
。取而代之的是,我们只检查它一次,且不会在 shouldShowUsers
为否的时候运算 v-for
。
<ul> <li v-for="user in users" v-if="user.isActive" :key="user.id" > {{ user.name }} </li></ul>
<ul> <li v-for="user in users" v-if="shouldShowUsers" :key="user.id" > {{ user.name }} </li></ul>
<ul> <li v-for="user in activeUsers" :key="user.id" > {{ user.name }} </li></ul>
<ul v-if="shouldShowUsers"> <li v-for="user in users" :key="user.id" > {{ user.name }} </li></ul>
对于应用来说,顶级 App
组件和布局组件中的样式可以是全局的,但是其它所有组件都应该是有作用域的。
这条规则只和单文件组件有关。你不一定要使用 scoped
attribute。设置作用域也可以通过 CSS Modules,那是一个基于 class 的类似 BEM 的策略,当然你也可以使用其它的库或约定。
不管怎样,对于组件库,我们应该更倾向于选用基于 class 的策略而不是 scoped
attribute。
这让覆写内部样式更容易:使用了人类可理解的 class 名称且没有太高的选择器优先级,而且不太会导致冲突。
详解
如果你和其他开发者一起开发一个大型工程,或有时引入三方 HTML/CSS (比如来自 Auth0),设置一致的作用域会确保你的样式只会运用在它们想要作用的组件上。
不止要使用scoped
attribute,使用唯一的 class 名可以帮你确保那些三方库的 CSS 不会运用在你自己的 HTML 上。比如许多工程都使用了button
、btn
或icon
class 名,所以即便你不使用类似 BEM 的策略,添加一个 app 专属或组件专属的前缀 (比如ButtonClose-icon
) 也可以提供很多保护。
<template> <button class="btn btn-close">×</button></template><style>.btn-close { background-color: red;}</style>
<template> <button class="button button-close">×</button></template><!-- 使用 `scoped` attribute --><style scoped>.button { border: none; border-radius: 2px;}.button-close { background-color: red;}</style>
<template> <button :class="[$style.button, $style.buttonClose]">×</button></template><!-- 使用 CSS modules --><style module>.button { border: none; border-radius: 2px;}.buttonClose { background-color: red;}</style>
<template> <button class="c-Button c-Button--close">×</button></template><!-- 使用 BEM 约定 --><style>.c-Button { border: none; border-radius: 2px;}.c-Button--close { background-color: red;}</style>
使用模块作用域保持不允许外部访问的函数的私有性。如果无法做到这一点,就始终为插件、混入等不考虑作为对外公共 API 的自定义私有 property 使用 $_
前缀。并附带一个命名空间以回避和其它作者的冲突 (比如 $_yourPluginName_
)。
::: 详细
Vue 使用 _
前缀来定义其自身的私有 property,所以使用相同的前缀 (比如 _update
) 有覆写实例 property 的风险。即便你检查确认 Vue 当前版本没有用到这个 property 名,也不能保证和将来的版本没有冲突。
对于 $
前缀来说,其在 Vue 生态系统中的目的是暴露给用户的一个特殊的实例 property,所以把它用于私有 property 并不合适。
不过,我们推荐把这两个前缀结合为 $_
,作为一个用户定义的私有 property 的约定,以确保不会和 Vue 自身相冲突。
:::
const myGreatMixin = { // ... methods: { update() { // ... } }}
const myGreatMixin = { // ... methods: { _update() { // ... } }}
const myGreatMixin = { // ... methods: { $update() { // ... } }}
const myGreatMixin = { // ... methods: { $_update() { // ... } }}
const myGreatMixin = { // ... methods: { $_myGreatMixin_update() { // ... } }}
// Even better!const myGreatMixin = { // ... methods: { publicMethod() { // ... myPrivateFunction() } }}function myPrivateFunction() { // ...}export default myGreatMixin
只要有能够拼接文件的构建系统,就把每个组件单独分成文件。
当你需要编辑一个组件或查阅一个组件的用法时,可以更快速的找到它。
app.component('TodoList', { // ...})app.component('TodoItem', { // ...})
components/|- TodoList.js|- TodoItem.js
components/|- TodoList.vue|- TodoItem.vue
单文件组件的文件名应该要么始终是单词大写开头 (PascalCase),要么始终是横线连接 (kebab-case)。
单词大写开头对于代码编辑器的自动补全最为友好,因为这使得我们在 JS(X) 和模板中引用组件的方式尽可能的一致。然而,混用文件命名方式有的时候会导致大小写不敏感的文件系统的问题,这也是横线连接命名同样完全可取的原因。
components/|- mycomponent.vue
components/|- myComponent.vue
components/|- MyComponent.vue
components/|- my-component.vue
应用特定样式和约定的基础组件 (也就是展示类的、无逻辑的或无状态的组件) 应该全部以一个特定的前缀开头,比如 Base
、App
或 V
。
详解
这些组件为你的应用奠定了一致的基础样式和行为。它们可能只包括:
- HTML 元素
- 其它基础组件
- 第三方 UI 组件库
但是它们绝不会包括全局状态 (比如来自 Vuex store)。
它们的名字通常包含所包裹元素的名字 (比如BaseButton
、BaseTable
),除非没有现成的对应功能的元素 (比如BaseIcon
)。如果你为特定的上下文构建类似的组件,那它们几乎总会消费这些组件 (比如BaseButton
可能会用在ButtonSubmit
上)。
这样做的几个好处:
- 当你在编辑器中以字母顺序排序时,你的应用的基础组件会全部列在一起,这样更容易识别。
- 因为组件名应该始终是多个单词,所以这样做可以避免你在包裹简单组件时随意选择前缀 (比如MyButton
、VueButton
)。
- 因为这些组件会被频繁使用,所以你可能想把它们放到全局而不是在各处分别导入它们。使用相同的前缀可以让 webpack 这样工作:js
const requireComponent = require.context("./src", true, /Base[A-Z]w+.(vue|js)$/)
requireComponent.keys().forEach(function (fileName) {
let baseComponentConfig = requireComponent(fileName)
baseComponentConfig = baseComponentConfig.default || baseComponentConfig
const baseComponentName = baseComponentConfig.name || (
fileName
.replace(/^.+//, '')
.replace(/.w+$/, '')
)
app.component(baseComponentName, baseComponentConfig)
})
components/|- MyButton.vue|- VueTable.vue|- Icon.vue
components/|- BaseButton.vue|- BaseTable.vue|- BaseIcon.vue
components/|- AppButton.vue|- AppTable.vue|- AppIcon.vue
components/|- VButton.vue|- VTable.vue|- VIcon.vue
只应该拥有单个活跃实例的组件应该以 The
前缀命名,以示其唯一性。
这不意味着组件只可用于一个单页面,而是每个页面只使用一次。这些组件永远不接受任何 prop,因为它们是为你的应用定制的,而不是它们在你的应用中的上下文。如果你发现有必要添加 prop,那就表明这实际上是一个可复用的组件,只是目前在每个页面里只使用一次。
components/|- Heading.vue|- MySidebar.vue
components/|- TheHeading.vue|- TheSidebar.vue
和父组件紧密耦合的子组件应该以父组件名作为前缀命名。
如果一个组件只在某个父组件的场景下有意义,这层关系应该体现在其名字上。因为编辑器通常会按字母顺序组织文件,所以这样做可以把相关联的文件排在一起。
::: 详情 你可以试着通过在其父组件命名的目录中嵌套子组件以解决这个问题。比如:
components/|- TodoList/ |- Item/ |- index.vue |- Button.vue |- index.vue
或:
components/|- TodoList/ |- Item/ |- Button.vue |- Item.vue|- TodoList.vue
但是这种方式并不推荐,因为这会导致:
:::
components/|- TodoList.vue|- TodoItem.vue|- TodoButton.vue
components/|- SearchSidebar.vue|- NavigationForSearchSidebar.vue
components/|- TodoList.vue|- TodoListItem.vue|- TodoListItemButton.vue
components/|- SearchSidebar.vue|- SearchSidebarNavigation.vue
组件名称应该以高阶的 (通常是一般化描述的) 单词开头,以描述性的修饰词结尾。
详解你可能会疑惑:
“为什么我们给组件命名时不多遵从自然语言呢?”
在自然的英文里,形容词和其它描述语通常都出现在名词之前,否则需要使用连接词。比如:
- Coffee with milk
- Soup of the day
- Visitor to the museum
如果你愿意,你完全可以在组件名里包含这些连接词,但是单词的顺序很重要。
同样要注意在你的应用中所谓的“高阶”是跟语境有关的。比如对于一个带搜索表单的应用来说,它可能包含这样的组件:bash
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
你可能注意到了,我们很难看出来哪些组件是针对搜索的。现在我们来根据规则给组件重新命名:bash
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputExcludeGlob.vue
|- SearchInputQuery.vue
|- SettingsCheckboxLaunchOnStartup.vue
|- SettingsCheckboxTerms.vue
因为编辑器通常会按字母顺序组织文件,所以现在组件之间的重要关系一目了然。
你可能想换成多级目录的方式,把所有的搜索组件放到“search”目录,把所有的设置组件放到“settings”目录。我们只推荐在非常大型 (如有 100+ 个组件) 的应用下才考虑这么做,因为:
- 在多级目录间找来找去,要比在单个components
目录下滚动查找要花费更多的精力。
- 存在组件重名 (比如存在多个ButtonDelete.vue
组件) 的时候在编辑器里更难快速定位。
详解
你可能会疑惑:
“为什么我们给组件命名时不多遵从自然语言呢?”
在自然的英文里,形容词和其它描述语通常都出现在名词之前,否则需要使用连接词。比如:
- Coffee with milk
- Soup of the day
- Visitor to the museum
如果你愿意,你完全可以在组件名里包含这些连接词,但是单词的顺序很重要。
同样要注意在你的应用中所谓的“高阶”是跟语境有关的。比如对于一个带搜索表单的应用来说,它可能包含这样的组件:bash
components/
|- ClearSearchButton.vue
|- ExcludeFromSearchInput.vue
|- LaunchOnStartupCheckbox.vue
|- RunSearchButton.vue
|- SearchInput.vue
|- TermsCheckbox.vue
你可能注意到了,我们很难看出来哪些组件是针对搜索的。现在我们来根据规则给组件重新命名:bash
components/
|- SearchButtonClear.vue
|- SearchButtonRun.vue
|- SearchInputExcludeGlob.vue
|- SearchInputQuery.vue
|- SettingsCheckboxLaunchOnStartup.vue
|- SettingsCheckboxTerms.vue
因为编辑器通常会按字母顺序组织文件,所以现在组件之间的重要关系一目了然。
你可能想换成多级目录的方式,把所有的搜索组件放到“search”目录,把所有的设置组件放到“settings”目录。我们只推荐在非常大型 (如有 100+ 个组件) 的应用下才考虑这么做,因为:
- 在多级目录间找来找去,要比在单个components
目录下滚动查找要花费更多的精力。
- 存在组件重名 (比如存在多个ButtonDelete.vue
组件) 的时候在编辑器里更难快速定位。
- 让重构变得更难,因为为一个移动了的组件更新相关引用时,查找/替换通常并不高效。- 让重构变得更难,因为为一个移动了的组件更新相关引用时,查找/替换通常并不高效。
components/|- ClearSearchButton.vue|- ExcludeFromSearchInput.vue|- LaunchOnStartupCheckbox.vue|- RunSearchButton.vue|- SearchInput.vue|- TermsCheckbox.vue
components/|- SearchButtonClear.vue|- SearchButtonRun.vue|- SearchInputQuery.vue|- SearchInputExcludeGlob.vue|- SettingsCheckboxTerms.vue|- SettingsCheckboxLaunchOnStartup.vue
在单文件组件、字符串模板和 JSX 中没有内容的组件应该是自闭合的——但在 DOM 模板里永远不要这样做。
自闭合组件表示它们不仅没有内容,而且刻意没有内容。其不同之处就好像书上的一页白纸对比贴有“本页有意留白”标签的白纸。而且没有了额外的闭合标签,你的代码也更简洁。
不幸的是,HTML 并不支持自闭合的自定义元素——只有官方的“空”元素。所以上述策略仅适用于进入 DOM 之前 Vue 的模板编译器能够触达的地方,然后再产出符合 DOM 规范的 HTML。
<!-- 在单文件组件、字符串模板和 JSX 中 --><MyComponent></MyComponent>
<!-- 在 DOM 模板中 --><my-component/>
<!-- 在单文件组件、字符串模板和 JSX 中 --><MyComponent/>
<!-- 在 DOM 模板中 --><my-component></my-component>
对于绝大多数项目来说,在单文件组件和字符串模板中组件名称应该总是 PascalCase 的——但是在 DOM 模板中总是 kebab-case 的。
PascalCase 相比 kebab-case 有一些优势:
MyComponent>
视觉上比 <my-component>
更能够和单个单词的 HTML 元素区别开来,因为前者的不同之处有两个大写字母,后者只有一个横线。不幸的是,由于 HTML 是大小写不敏感的,在 DOM 模板中必须仍使用 kebab-case。
还请注意,如果你已经是 kebab-case 的重度用户,那么与 HTML 保持一致的命名约定且在多个项目中保持相同的大小写规则就可能比上述优势更为重要了。在这些情况下,在所有的地方都使用 kebab-case 同样是可以接受的。
<!-- 在单文件组件和字符串模板中 --><mycomponent/>
<!-- 在单文件组件和字符串模板中 --><myComponent/>
<!-- 在 DOM 模板中 --><MyComponent></MyComponent>
<!-- 在单文件组件和字符串模板中 --><MyComponent/>
<!-- 在 DOM 模板中 --><my-component></my-component>
或者
<!-- 在所有地方 --><my-component></my-component>
JS/JSX 中的组件名应该始终是 PascalCase 的,尽管在较为简单的应用中只使用 app.component
进行全局组件注册时,可以使用 kebab-case 字符串。
详解
在 JavaScript 中,PascalCase 是类和构造函数 (本质上任何可以产生多份不同实例的东西) 的命名约定。Vue 组件也有多份实例,所以同样使用 PascalCase 是有意义的。额外的好处是,在 JSX (和模板) 里使用 PascalCase 使得代码的读者更容易分辨 Vue 组件和 HTML 元素。
然而,对于只通过app.component
定义全局组件的应用来说,我们推荐 kebab-case 作为替代。原因是:
- 全局组件很少被 JavaScript 引用,所以遵守 JavaScript 的命名约定意义不大。
- 这些应用往往包含许多 DOM 内的模板,这种情况下是必须使用 kebab-case 的。
app.component('myComponent', { // ...})
import myComponent from './MyComponent.vue'
export default { name: 'myComponent', // ...}
export default { name: 'my-component', // ...}
app.component('MyComponent', { // ...})
app.component('my-component', { // ...})
import MyComponent from './MyComponent.vue'
export default { name: 'MyComponent', // ...}
组件名称应该倾向于完整单词而不是缩写。
编辑器中的自动补全已经让书写长命名的代价非常之低了,而其带来的明确性却是非常宝贵的。不常用的缩写尤其应该避免。
components/|- SdSettings.vue|- UProfOpts.vue
components/|- StudentDashboardSettings.vue|- UserProfileOptions.vue
在声明 prop 的时候,其命名应该始终使用 camelCase,而在模板和 JSX 中应该始终使用 kebab-case。
我们单纯的遵循每个语言的约定。在 JavaScript 中更自然的是 camelCase。而在 HTML 中则是 kebab-case。
props: { 'greeting-text': String}
<WelcomeMessage greetingText="hi"/>
props: { greetingText: String}
<WelcomeMessage greeting-text="hi"/>
多个 attribute 的元素应该分多行撰写,每个 attribute 一行。
在 JavaScript 中,用多行分隔对象的多个 property 是很常见的最佳实践,因为这样更易读。模板和 JSX 值得我们做相同的考虑。
<img src="https://vuejs.org/images/logo.png" rel="external nofollow" rel="external nofollow" alt="Vue Logo">
<MyComponent foo="a" bar="b" baz="c"/>
<img src="https://vuejs.org/images/logo.png" rel="external nofollow" rel="external nofollow" alt="Vue Logo">
<MyComponent foo="a" bar="b" baz="c"/>
组件模板应该只包含简单的表达式,复杂的表达式则应该重构为计算属性或方法。
复杂表达式会让你的模板变得不那么声明式。我们应该尽量描述应该出现的是什么,而非如何计算那个值。而且计算属性和方法使得代码可以重用。
{{ fullName.split(' ').map((word) => { return word[0].toUpperCase() + word.slice(1) }).join(' ')}}
<!-- 在模板中 -->{{ normalizedFullName }}
// 复杂表达式已经移入一个计算属性computed: { normalizedFullName() { return this.fullName.split(' ') .map(word => word[0].toUpperCase() + word.slice(1)) .join(' ') }}
应该把复杂计算属性分割为尽可能多的更简单的 property。
详解
更简单、命名得当的计算属性是这样的:
- 易于测试
当每个计算属性都包含一个非常简单且很少依赖的表达式时,撰写测试以确保其正确工作就会更加容易。
- 易于阅读
简化计算属性要求你为每一个值都起一个描述性的名称,即便它不可复用。这使得其他开发者 (以及未来的你) 更容易专注在他们关心的代码上并搞清楚发生了什么。
- 更好的“拥抱变化”
任何能够命名的值都可能用在视图上。举个例子,我们可能打算展示一个信息,告诉用户他们存了多少钱;也可能打算计算税费,但是可能会分开展现,而不是作为总价的一部分。
小的、专注的计算属性减少了信息使用时的假设性限制,所以需求变更时也用不着那么多重构了。
computed: { price() { const basePrice = this.manufactureCost / (1 - this.profitMargin) return ( basePrice - basePrice * (this.discountPercent || 0) ) }}
computed: { basePrice() { return this.manufactureCost / (1 - this.profitMargin) }, discount() { return this.basePrice * (this.discountPercent || 0) }, finalPrice() { return this.basePrice - this.discount }}
非空 HTML attribute 值应该始终带引号 (单引号或双引号,选你 JS 里不用的那个)。
在 HTML 中不带空格的 attribute 值是可以没有引号的,但这鼓励了大家在特征值里不写空格,导致可读性变差。
<input type=text>
<AppSidebar :style={width:sidebarWidth+'px'}>
<input type="text">
<AppSidebar :style="{ width: sidebarWidth + 'px' }">
指令缩写 (用 :
表示 v-bind:
,@
表示 v-on:
和用 #
表示 v-slot
) 应该要么都用要么都不用。
<input v-bind:value="newTodoText" :placeholder="newTodoInstructions">
<input v-on:input="onInput" @focus="onFocus">
<template v-slot:header> <h1>Here might be a page title</h1></template><template #footer> <p>Here's some contact info</p></template>
<input :value="newTodoText" :placeholder="newTodoInstructions">
<input v-bind:value="newTodoText" v-bind:placeholder="newTodoInstructions">
<input @input="onInput" @focus="onFocus">
<input v-on:input="onInput" v-on:focus="onFocus">
<template v-slot:header> <h1>Here might be a page title</h1></template><template v-slot:footer> <p>Here's some contact info</p></template>
<template #header> <h1>Here might be a page title</h1></template><template #footer> <p>Here's some contact info</p></template>
组件/实例的选项应该有统一的顺序。
这是我们推荐的组件选项默认顺序。它们被划分为几大类,所以你也能知道从插件里添加的新 property 应该放到哪里。
name
components
directives
extends
mixins
provide
/inject
inheritAttrs
props
emits
setup
data
computed
watch
beforeCreate
created
beforeMount
mounted
beforeUpdate
updated
activated
deactivated
beforeUnmount
unmounted
errorCaptured
renderTracked
renderTriggered
methods
template
/render
元素 (包括组件) 的 attribute 应该有统一的顺序。
这是我们为组件选项推荐的默认顺序。它们被划分为几大类,所以你也能知道新添加的自定义 attribute 和指令应该放到哪里。
is
v-for
v-if
v-else-if
v-else
v-show
v-cloak
v-pre
v-once
id
ref
key
v-model
v-on
v-html
v-text
你可能想在多个 property 之间增加一个空行,特别是在这些选项一屏放不下,需要滚动才能都看到的时候。
当你的组件开始觉得密集或难以阅读时,在多个 property 之间添加空行可以让其变得容易。在一些诸如 Vim 的编辑器里,这样格式化后的选项还能通过键盘被快速导航。
props: { value: { type: String, required: true }, focused: { type: Boolean, default: false }, label: String, icon: String},computed: { formattedValue() { // ... }, inputClasses() { // ... }}
// 没有空行在组件易于阅读和导航时也没问题。props: { value: { type: String, required: true }, focused: { type: Boolean, default: false }, label: String, icon: String},computed: { formattedValue() { // ... }, inputClasses() { // ... }}
单文件组件应该总是让 <script>
、<template>
和 <style>
标签的顺序保持一致。且 <style>
要放在最后,因为另外两个标签至少要有一个。
<style>/* ... */</style><script>/* ... */</script><template>...</template>
<!-- ComponentA.vue --><script>/* ... */</script><template>...</template><style>/* ... */</style><!-- ComponentB.vue --><template>...</template><script>/* ... */</script><style>/* ... */</style>
<!-- ComponentA.vue --><script>/* ... */</script><template>...</template><style>/* ... */</style><!-- ComponentB.vue --><script>/* ... */</script><template>...</template><style>/* ... */</style>
<!-- ComponentA.vue --><template>...</template><script>/* ... */</script><style>/* ... */</style><!-- ComponentB.vue --><template>...</template><script>/* ... */</script><style>/* ... */</style>
scoped
中的元素选择器谨慎使用元素选择器应该避免在 scoped
中出现。
在 scoped
样式中,类选择器比元素选择器更好,因为大量使用元素选择器是很慢的。
详解
为了给样式设置作用域,Vue 会为元素添加一个独一无二的 attribute,例如 data-v-f3f3eg9。然后修改选择器,使得在匹配选择器的元素中,只有带这个 attribute 才会真正生效 (比如 button[data-v-f3f3eg9])。
问题在于大量的元素和 attribute 组合的选择器 (比如 button[data-v-f3f3eg9]) 会比类和 attribute 组合的选择器慢,所以应该尽可能选用类选择器。
<template> <button>×</button></template><style scoped>button { background-color: red;}</style>
<template> <button class="btn btn-close">×</button></template><style scoped>.btn-close { background-color: red;}</style>
应该优先通过 prop 和事件进行父子组件之间的通信,而不是 this.$parent
或变更 prop。
一个理想的 Vue 应用是 prop 向下传递,事件向上传递的。遵循这一约定会让你的组件更易于理解。然而,在一些边界情况下 prop 的变更或 this.$parent
能够简化两个深度耦合的组件。
问题在于,这种做法在很多简单的场景下可能会更方便。但请当心,不要为了一时方便 (少写代码) 而牺牲数据流向的简洁性 (易于理解)。
app.component('TodoItem', { props: { todo: { type: Object, required: true } }, template: '<input v-model="todo.text">'})
app.component('TodoItem', { props: { todo: { type: Object, required: true } }, methods: { removeTodo() { this.$parent.todos = this.$parent.todos.filter(todo => todo.id !== vm.todo.id) } }, template: ` <span> {{ todo.text }} <button @click="removeTodo"> × </button> </span> `})
app.component('TodoItem', { props: { todo: { type: Object, required: true } }, template: ` <input :value="todo.text" @input="$emit('input', $event.target.value)" > `})
app.component('TodoItem', { props: { todo: { type: Object, required: true } }, template: ` <span> {{ todo.text }} <button @click="$emit('delete')"> × </button> </span> `})
应该优先通过 Vuex 管理全局状态,而不是通过 this.$root
或一个全局事件总线。
通过 this.$root
和/或全局事件总线管理状态在很多简单的情况下都是很方便的,但是并不适用于绝大多数的应用。
Vuex 是 Vue 的官方类 flux 实现,其提供的不仅是一个管理状态的中心区域,还是组织、追踪和调试状态变更的好工具。它很好地集成在了 Vue 生态系统之中 (包括完整的 Vue DevTools 支持)。
// main.jsimport { createApp } from 'vue'import mitt from 'mitt'const app = createApp({ data() { return { todos: [], emitter: mitt() } }, created() { this.emitter.on('remove-todo', this.removeTodo) }, methods: { removeTodo(todo) { const todoIdToRemove = todo.id this.todos = this.todos.filter(todo => todo.id !== todoIdToRemove) } }})
// store/modules/todos.jsexport default { state: { list: [] }, mutations: { REMOVE_TODO (state, todoId) { state.list = state.list.filter(todo => todo.id !== todoId) } }, actions: { removeTodo ({ commit, state }, todo) { commit('REMOVE_TODO', todo.id) } }}
<!-- TodoItem.vue --><template> <span> {{ todo.text }} <button @click="removeTodo(todo)"> X </button> </span></template><script>import { mapActions } from 'vuex'export default { props: { todo: { type: Object, required: true } }, methods: mapActions(['removeTodo'])}</script>
此章节假设你已经对组合式 API 有了基本的了解。如果你只学习过选项式 API,你可以使用左侧边栏上方的切换按钮将 API 风格切换为组合式 API 后,重新阅读响应性基础和生命周期钩子两个章节。
:::
在 Vue 应用的概念中,“组合式函数”(Composables) 是一个利用 Vue 的组合式 API 来封装和复用有状态逻辑的函数。
当构建前端应用时,我们常常需要复用公共任务的逻辑。例如为了在不同地方格式化时间,我们可能会抽取一个可复用的日期格式化函数。这个函数封装了无状态的逻辑:它在接收一些输入后立刻返回所期望的输出。复用无状态逻辑的库有很多,比如你可能已经用过的 lodash 或是 date-fns。
相比之下,有状态逻辑负责管理会随时间而变化的状态。一个简单的例子是跟踪当前鼠标在页面中的位置。在实际应用中,也可能是像触摸手势或与数据库的连接状态这样的更复杂的逻辑。
如果我们要直接在组件中使用组合式 API 实现鼠标跟踪功能,它会是这样的:
<script setup>import { ref, onMounted, onUnmounted } from 'vue'const x = ref(0)const y = ref(0)function update(event) { x.value = event.pageX y.value = event.pageY}onMounted(() => window.addEventListener('mousemove', update))onUnmounted(() => window.removeEventListener('mousemove', update))</script><template>Mouse position is at: {{ x }}, {{ y }}</template>
但是,如果我们想在多个组件中复用这个相同的逻辑呢?我们可以把这个逻辑以一个组合式函数的形式提取到外部文件中:
// mouse.jsimport { ref, onMounted, onUnmounted } from 'vue'// 按照惯例,组合式函数名以“use”开头export function useMouse() { // 被组合式函数封装和管理的状态 const x = ref(0) const y = ref(0) // 组合式函数可以随时更改其状态。 function update(event) { x.value = event.pageX y.value = event.pageY } // 一个组合式函数也可以挂靠在所属组件的生命周期上 // 来启动和卸载副作用 onMounted(() => window.addEventListener('mousemove', update)) onUnmounted(() => window.removeEventListener('mousemove', update)) // 通过返回值暴露所管理的状态 return { x, y }}
下面是它在组件中使用的方式:
<script setup>import { useMouse } from './mouse.js'const { x, y } = useMouse()</script><template>Mouse position is at: {{ x }}, {{ y }}</template>
<div class="demo">Mouse position is at: {{ x }}, {{ y }}</div>
如你所见,核心逻辑完全一致,我们做的只是把它移到一个外部函数中去,并返回需要暴露的状态。和在组件中一样,你也可以在组合式函数中使用所有的组合式 API。现在,useMouse()
的功能可以在任何组件中轻易复用了。
更酷的是,你还可以嵌套多个组合式函数:一个组合式函数可以调用一个或多个其他的组合式函数。这使得我们可以像使用多个组件组合成整个应用一样,用多个较小且逻辑独立的单元来组合形成复杂的逻辑。实际上,这正是为什么我们决定将实现了这一设计模式的 API 集合命名为组合式 API。
举例来说,我们可以将添加和清除 DOM 事件监听器的逻辑也封装进一个组合式函数中:
// event.jsimport { onMounted, onUnmounted } from 'vue'export function useEventListener(target, event, callback) { // 如果你想的话, // 也可以用字符串形式的 CSS 选择器来寻找目标 DOM 元素 onMounted(() => target.addEventListener(event, callback)) onUnmounted(() => target.removeEventListener(event, callback))}
有了它,之前的 useMouse()
组合式函数可以被简化为:
```js{3,9-12}// mouse.jsimport { ref } from 'vue'import { useEventListener } from './event'
export function useMouse() {const x = ref(0)const y = ref(0)
useEventListener(window, 'mousemove', (event) => {x.value = event.pageXy.value = event.pageY})
return { x, y }}
:::tip每一个调用 `useMouse()` 的组件实例会创建其独有的 `x`、`y` 状态拷贝,因此他们不会互相影响。如果你想要在组件之间共享状态,请阅读[状态管理](/guide/scaling-up/state-management)这一章。:::## 异步状态示例 {#async-state-example}`useMouse()` 组合式函数没有接收任何参数,因此让我们再来看一个需要接收一个参数的组合式函数示例。在做异步数据请求时,我们常常需要处理不同的状态:加载中、加载成功和加载失败。
<script setup>import { ref } from 'vue'
const data = ref(null)const error = ref(null)
fetch('...').then((res) => res.json()).then((json) => (data.value = json)).catch((err) => (error.value = err))</script>
<template><div v-if="error">Oops! Error encountered: {{ error.message }}</div><div v-else-if="data">Data loaded:<pre>{{ data }}</pre></div><div v-else>Loading...</div></template>
如果在每个需要获取数据的组件中都要重复这种模式,那就太繁琐了。让我们把它抽取成一个组合式函数:```js// fetch.jsimport { ref } from 'vue'export function useFetch(url) { const data = ref(null) const error = ref(null) fetch(url) .then((res) => res.json()) .then((json) => (data.value = json)) .catch((err) => (error.value = err)) return { data, error }}
现在我们在组件里只需要:
<script setup>import { useFetch } from './fetch.js'const { data, error } = useFetch('...')</script>
useFetch()
接收一个静态 URL 字符串作为输入——因此它只会执行一次 fetch 并且就此结束。如果我们想要在 URL 改变时重新 fetch 呢?为了实现这一点,我们需要将响应式状态传入组合式函数,并让它基于传入的状态来创建执行操作的侦听器。
举例来说,useFetch()
应该能够接收一个 ref:
const url = ref('/initial-url')const { data, error } = useFetch(url)// 这将会重新触发 fetchurl.value = '/new-url'
或者接收一个 getter 函数:
// 当 props.id 改变时重新 fetchconst { data, error } = useFetch(() => `/posts/${props.id}`)
我们可以用 watchEffect()
和 toValue()
API 来重构我们现有的实现:
```js{8,13}// fetch.jsimport { ref, watchEffect, toValue } from 'vue'
export function useFetch(url) {const data = ref(null)const error = ref(null)
watchEffect(() => {// 在 fetch 之前重置状态data.value = nullerror.value = null// toValue() 将可能的 ref 或 getter 解包fetch(toValue(url)).then((res) => res.json()).then((json) => (data.value = json)).catch((err) => (error.value = err))})
return { data, error }}
`toValue()` 是一个在 3.3 版本中新增的 API。它的设计目的是将 ref 或 getter 规范化为值。如果参数是 ref,它会返回 ref 的值;如果参数是函数,它会调用函数并返回其返回值。否则,它会原样返回参数。它的工作方式类似于 [`unref()`](/api/reactivity-utilities.html#unref),但对函数有特殊处理。注意 `toValue(url)` 是在 `watchEffect` 回调函数的**内部**调用的。这确保了在 `toValue()` 规范化期间访问的任何响应式依赖项都会被侦听器跟踪。这个版本的 `useFetch()` 现在能接收静态 URL 字符串、ref 和 getter,使其更加灵活。watch effect 会立即运行,并且会跟踪 `toValue(url)` 期间访问的任何依赖项。如果没有跟踪到依赖项(例如 url 已经是字符串),则 effect 只会运行一次;否则,它将在跟踪到的任何依赖项更改时重新运行。这是[更新后的 `useFetch()`](https://play.vuejs.org/#eNptVMFu2zAM/RXOFztYZncodgmSYAPWnTZsKLadfFFsulHrSIZEJwuC/PtIyXaTtkALxxT5yPf45FPypevyfY/JIln6yumOwCP13bo0etdZR3ACh80cKrvresIaztA4u4OUi9KLpN7jN6RqO53nxRjKHz1nlqayxhNslMc/roUVpFuizi+K4tFb07Wqwq1ta3Q5HTtd2RpzblqQra0vGCCW65oreaIs/ZjOxmAf8MYRs2wGq/XU6D3X5HvV9sj5Y8UJakVqDuicdXMGJHfk0VcTj4wxOX9ZRFVYD34h3PGchPwG8N2qGjobZlpIYLnpiayB/YfGulWZaNAGPpUJfK5aXT1JRIbXZbI+nUDD+bwsYklAL2lZ6z1X64ZTw2CcKcAM3a1/2s6/gzsJAzKL3hA6rBfAWCE536H36gEDriwwFA4zTSMEpox7L8+L/pxacPv4K86Brcc4jGjFNV/5AS3TlrbLzqHwkLPYkt/fxFiLUto85Hk+ni+LScpknlwYhX147buD4oO7psGK5kD2r+zxhQdLg/9CSdObijSzvVoinGSeuPYwbPSP6VtZ8HgSJHx5JP8XA2TKH00F0V4BFaAouISvDHhiNrBB3j1CI90D5ZglfaMHuYXAx3Dc2+v4JbRt9wi0xWDymCpTbJ01tvftEbwFTakHcqp64guqPKgJoMYOTc1+OcLmeMUlEBzZM3ZUdjVqPPj/eRq5IAPngKwc6UZXWrXcpFVH4GmVqXkt0boiHwGog9IEpHdo+6GphBmgN6L1DA66beUC9s4EnhwdeOomMlMSkwsytLac5g7aR11ibkDZSLUABRk+aD8QoMiS1WSCcaKwISEZ2MqXIaBfLSpmchUb05pRsTNUIiNkOFjr9SZxyJTHOXx1YGR49eGRDP4rzRt6lmay86Re7DcgGTzAL74GrEOWDUaRL9kjb/fSoWzO3wPAlXNB9M1+KNrmcXF8uoab/PaCljQLwCN5oS93+jpFWmYyT/g8Zel9NEJ4S2fPpYMsc7i9uQlREeecnP8DWEwr0Q==),为了便于演示,添加了人为延迟和随机错误。## 约定和最佳实践 {#conventions-and-best-practices}### 命名 {#naming}组合式函数约定用驼峰命名法命名,并以“use”作为开头。### 输入参数 {#input-arguments}即便不依赖于 ref 或 getter 的响应性,组合式函数也可以接收它们作为参数。如果你正在编写一个可能被其他开发者使用的组合式函数,最好处理一下输入参数是 ref 或 getter 而非原始值的情况。可以利用 [`toValue()`](/api/reactivity-utilities#tovalue) 工具函数来实现:```jsimport { toValue } from 'vue'function useFeature(maybeRefOrGetter) { // 如果 maybeRefOrGetter 是一个 ref 或 getter, // 将返回它的规范化值。 // 否则原样返回。 const value = toValue(maybeRefOrGetter)}
如果你的组合式函数在输入参数是 ref 或 getter 的情况下创建了响应式 effect,为了让它能够被正确追踪,请确保要么使用 watch()
显式地监视 ref 或 getter,要么在 watchEffect()
中调用 toValue()
。
前面讨论过的 useFetch() 实现提供了一个接受 ref、getter 或普通值作为输入参数的组合式函数的具体示例。
你可能已经注意到了,我们一直在组合式函数中使用 ref()
而不是 reactive()
。我们推荐的约定是组合式函数始终返回一个包含多个 ref 的普通的非响应式对象,这样该对象在组件中被解构为 ref 之后仍可以保持响应性:
// x 和 y 是两个 refconst { x, y } = useMouse()
从组合式函数返回一个响应式对象会导致在对象解构过程中丢失与组合式函数内状态的响应性连接。与之相反,ref 则可以维持这一响应性连接。
如果你更希望以对象属性的形式来使用组合式函数中返回的状态,你可以将返回的对象用 reactive()
包装一次,这样其中的 ref 会被自动解包,例如:
const mouse = reactive(useMouse())// mouse.x 链接到了原来的 x refconsole.log(mouse.x)
Mouse position is at: {{ mouse.x }}, {{ mouse.y }}
在组合式函数中的确可以执行副作用 (例如:添加 DOM 事件监听器或者请求数据),但请注意以下规则:
onMounted()
。这些钩子仅会在浏览器中被调用,因此可以确保能访问到 DOM。onUnmounted()
时清理副作用。举例来说,如果一个组合式函数设置了一个事件监听器,它就应该在 onUnmounted()
中被移除 (就像我们在 useMouse()
示例中看到的一样)。当然也可以像之前的 useEventListener()
示例那样,使用一个组合式函数来自动帮你做这些事。组合式函数只能在 <script setup>
或 setup()
钩子中被调用。在这些上下文中,它们也只能被同步调用。在某些情况下,你也可以在像 onMounted()
这样的生命周期钩子中调用它们。
这些限制很重要,因为这些是 Vue 用于确定当前活跃的组件实例的上下文。访问活跃的组件实例很有必要,这样才能:
:::tip<script setup>
是唯一在调用 await
之后仍可调用组合式函数的地方。编译器会在异步操作之后自动为你恢复当前的组件实例。:::
抽取组合式函数不仅是为了复用,也是为了代码组织。随着组件复杂度的增高,你可能会最终发现组件多得难以查询和理解。组合式 API 会给予你足够的灵活性,让你可以基于逻辑问题将组件代码拆分成更小的函数:
<script setup>import { useFeatureA } from './featureA.js'import { useFeatureB } from './featureB.js'import { useFeatureC } from './featureC.js'const { foo, bar } = useFeatureA()const { baz } = useFeatureB(foo)const { qux } = useFeatureC(baz)</script>
在某种程度上,你可以将这些提取出的组合式函数看作是可以相互通信的组件范围内的服务。
如果你正在使用选项式 API,组合式函数必须在 setup()
中调用。且其返回的绑定必须在 setup()
中返回,以便暴露给 this
及其模板:
import { useMouse } from './mouse.js'import { useFetch } from './fetch.js'export default { setup() { const { x, y } = useMouse() const { data, error } = useFetch('...') return { x, y, data, error } }, mounted() { // setup() 暴露的属性可以在通过 `this` 访问到 console.log(this.x) } // ...其他选项}
Vue 2 的用户可能会对 mixins 选项比较熟悉。它也让我们能够把组件逻辑提取到可复用的单元里。然而 mixins 有三个主要的短板:
基于上述理由,我们不再推荐在 Vue 3 中继续使用 mixin。保留该功能只是为了项目迁移的需求和照顾熟悉它的用户。
在组件插槽一章中,我们讨论过了基于作用域插槽的无渲染组件。我们甚至用它实现了一样的鼠标追踪器示例。
组合式函数相对于无渲染组件的主要优势是:组合式函数不会产生额外的组件实例开销。当在整个应用中使用时,由无渲染组件产生的额外组件实例会带来无法忽视的性能开销。
我们推荐在纯逻辑复用时使用组合式函数,在需要同时复用逻辑和视图布局时使用无渲染组件。
如果你有 React 的开发经验,你可能注意到组合式函数和自定义 React hooks 非常相似。组合式 API 的一部分灵感正来自于 React hooks,Vue 的组合式函数也的确在逻辑组合能力上与 React hooks 相近。然而,Vue 的组合式函数是基于 Vue 细粒度的响应性系统,这和 React hooks 的执行模型有本质上的不同。这一话题在组合式 API 的常见问题中有更细致的讨论。