组件
什么是组件?
组件是 Vue.js 最强大的功能之一。它们帮助您扩展基本 HTML 元素以封装可重用的代码。从高层次上讲,组件是 Vue.js 编译器将附加指定行为的自定义元素。在某些情况下,它们也可能以使用特殊 is
属性扩展的原生 HTML 元素的形式出现。
使用组件
注册
我们在前面的部分中了解到,我们可以使用 Vue.extend()
创建组件构造函数。
|
要将此构造函数用作组件,您需要使用 Vue.component(tag, constructor)
将其注册。
|
请注意,Vue.js 不强制执行 W3C 规则 用于自定义标签名称(全部小写,必须包含连字符),尽管遵循此约定被认为是最佳实践。
注册后,该组件现在可以在父实例的模板中用作自定义元素 <my-component>
。确保在实例化根 Vue 实例之前注册该组件。以下是完整的示例
|
|
这将渲染
|
请注意,组件的模板替换了自定义元素,自定义元素仅用作挂载点。此行为可以使用 replace
实例选项进行配置。
还要注意,组件提供了一个模板,而不是使用 el
选项进行挂载!只有根 Vue 实例(使用 new Vue
定义)将包含一个 el
用于挂载。
本地注册
您不必全局注册每个组件。您可以通过使用 components
实例选项在另一个组件的范围内使组件可用。
|
相同的封装适用于其他资产类型,例如指令、过滤器和过渡。
注册语法糖
为了简化操作,您可以直接将选项对象而不是实际构造函数传递给 Vue.component()
和 component
选项。Vue.js 会在幕后自动为您调用 Vue.extend()
。
|
组件选项注意事项
大多数可以传递给 Vue 构造函数的选项都可以在 Vue.extend()
中使用,有两个特殊情况:data
和 el
。假设我们只是将一个对象作为 data
传递给 Vue.extend()
|
这样做的问题是,相同的 data
对象将在 MyComponent
的所有实例之间共享!这很可能不是我们想要的,因此我们应该使用一个返回新对象的函数作为 data
选项。
|
el
选项在 Vue.extend()
中使用时也需要一个函数值,原因完全相同。
模板解析
Vue.js 模板引擎是基于 DOM 的,它使用浏览器提供的原生解析器,而不是提供自定义解析器。与基于字符串的模板引擎相比,这种方法有很多优点,但也有一些注意事项。模板必须是单独有效的 HTML 片段。某些 HTML 元素对其中可以出现的元素有限制。最常见的限制是
a
不能包含其他交互式元素(例如按钮和其他链接)li
应该是ul
或ol
的直接子元素,ul
和ol
只能包含li
option
应该是select
的直接子元素,select
只能包含option
(和optgroup
)table
只能包含thead
、tbody
、tfoot
和tr
,这些元素应该是table
的直接子元素tr
只能包含th
和td
,这些元素应该是tr
的直接子元素
在实践中,这些限制会导致意外行为。虽然在简单的情况下它可能看起来有效,但您不能依赖于自定义元素在浏览器验证之前扩展。例如,<my-select><option>...</option></my-select>
不是有效的模板,即使 my-select
组件最终扩展为 <select>...</select>
。
另一个结果是,您不能在 select
、table
和其他具有类似限制的元素中使用自定义标签(包括自定义元素和特殊标签,如 <component>
、<template>
和 <partial>
)。自定义标签将被提升,因此无法正确渲染。
对于自定义元素,您应该使用 is
特殊属性
|
对于 <table>
中的 <template>
,您应该使用 <tbody>
,因为表允许有多个 tbody
|
Props
使用 Props 传递数据
每个组件实例都有自己的隔离作用域。这意味着您不能(也不应该)直接在子组件的模板中引用父数据。可以使用 props 将数据传递给子组件。
“prop”是组件数据上的一个字段,预期从其父组件传递下来。子组件需要使用 props
选项 明确声明它期望接收的 props。
|
然后,我们可以像这样传递一个普通字符串给它
|
结果
camelCase vs. kebab-case
HTML 属性不区分大小写。当使用 camelCased prop 名称作为属性时,您需要使用它们的 kebab-case(连字符分隔)等效项
|
|
动态 Props
与将普通属性绑定到表达式类似,我们也可以使用 v-bind
将 props 动态绑定到父级的数据。每当父级的数据更新时,它也会向下传递给子级
|
使用 v-bind
的简写语法通常更简单
|
结果
字面量 vs. 动态
初学者常犯的一个错误是尝试使用字面量语法传递一个数字
|
但是,由于这是一个字面量 prop,因此其值作为普通字符串 "1"
传递下来,而不是实际的数字。如果我们想传递一个实际的 JavaScript 数字,我们需要使用动态语法使其值被评估为 JavaScript 表达式
|
Prop 绑定类型
默认情况下,所有 props 在子属性和父属性之间形成单向向下绑定:当父属性更新时,它将向下传递给子属性,但反之则不行。此默认值旨在防止子组件意外地改变父级的状态,这会使您的应用程序的数据流更难推理。但是,也可以使用 .sync
和 .once
绑定类型修饰符 明确强制执行双向绑定或一次性绑定
比较语法
|
双向绑定将同步子级 msg
属性的变化回父级的 parentMsg
属性。一次性绑定一旦设置,就不会在父级和子级之间同步未来的更改。
请注意,如果传递下来的 prop 是一个对象或一个数组,则它是按引用传递的。在子级中改变对象或数组本身将影响父级状态,无论您使用哪种绑定类型。
Prop 验证
组件可以指定它接收的 props 的要求。当您编写一个旨在供其他人使用的组件时,这很有用,因为这些 prop 验证要求本质上构成了您的组件的 API,并确保您的用户正确使用您的组件。而不是将 props 定义为字符串数组,您可以使用包含验证要求的对象哈希格式
|
type
可以是以下本机构造函数之一
- String
- Number
- Boolean
- Function
- Object
- Array
此外,type
也可以是自定义构造函数,断言将使用 instanceof
检查进行。
当 prop 验证失败时,Vue 将拒绝在子组件上设置该值,并在使用开发版本时抛出一个警告。
父子通信
父链
子组件可以访问其父组件作为 this.$parent
。根 Vue 实例将作为 this.$root
可用于其所有后代。每个父组件都有一个数组 this.$children
,其中包含其所有子组件。
虽然可以访问父链中的任何实例,但您应该避免直接依赖子组件中的父数据,而是更喜欢使用 props 明确传递数据。此外,从子组件中改变父状态是一个非常糟糕的做法,因为
它使父级和子级紧密耦合;
当单独查看父状态时,它使父状态更难推理,因为它的状态可能会被任何子级修改!理想情况下,只有组件本身应该被允许修改自己的状态。
自定义事件
所有 Vue 实例都实现了一个自定义事件接口,该接口有助于在组件树中进行通信。此事件系统独立于原生 DOM 事件,并且工作方式不同。
每个 Vue 实例都是一个事件发射器,可以
使用
$on()
监听事件;使用
$emit()
在自身上触发事件;使用
$dispatch()
派发一个向上沿着父链传播的事件;使用
$broadcast()
广播一个向下传播到所有后代的事件。
与 DOM 事件不同,Vue 事件会在沿着传播路径第一次触发回调后自动停止传播,除非回调显式返回 true
。
一个简单的例子
|
|
消息:{{ messages | json }}
v-on 用于自定义事件
上面的例子很好,但是当我们查看父组件的代码时,并不明显 "child-msg"
事件来自哪里。如果我们可以在模板中声明事件处理程序,就在使用子组件的地方,会更好。为了实现这一点,v-on
可以用于监听自定义事件,当它用于子组件时
|
这使得事情非常清晰:当子组件触发 "child-msg"
事件时,父组件的 handleIt
方法将被调用。任何影响父组件状态的代码都将在 handleIt
父方法中;子组件只关注触发事件。
子组件引用
尽管存在 props 和事件,但有时你可能仍然需要在 JavaScript 中直接访问子组件。为了实现这一点,你必须使用 v-ref
为子组件分配一个引用 ID。例如
|
|
当 v-ref
与 v-for
一起使用时,你得到的引用将是一个数组或一个对象,包含反映数据源的子组件。
使用插槽进行内容分发
使用组件时,通常希望像这样组合它们
|
这里有两点需要注意
<app>
组件不知道其挂载目标中可能存在什么内容。它由使用<app>
的任何父组件决定。<app>
组件很可能拥有自己的模板。
为了使组合工作,我们需要一种方法来交织父“内容”和组件自己的模板。这是一个称为**内容分发**(如果你熟悉 Angular,则称为“转录”)的过程。Vue.js 实现了一个内容分发 API,该 API 模仿了当前的 Web Components 规范草案,使用特殊的 <slot>
元素作为原始内容的分发出口。
编译范围
在我们深入研究 API 之前,让我们先澄清一下内容是在哪个范围内编译的。想象一下这样的模板
|
msg
应该绑定到父组件的数据还是子组件的数据?答案是父组件。关于组件范围的一个简单经验法则:
父模板中的所有内容都在父组件范围内编译;子模板中的所有内容都在子组件范围内编译。
一个常见的错误是试图在父模板中将指令绑定到子组件的属性/方法
|
假设 someChildProperty
是子组件上的一个属性,上面的例子将无法按预期工作。父组件的模板不应该了解子组件的状态。
如果你需要在组件根节点上绑定子组件范围的指令,你应该在子组件自己的模板中这样做
|
同样,分发的内容将在父组件范围内编译。
单个插槽
父组件内容将被**丢弃**,除非子组件模板包含至少一个 <slot>
出口。当只有一个没有属性的插槽时,整个内容片段将被插入到 DOM 中的其位置,替换插槽本身。
最初位于 <slot>
标签内的任何内容都被认为是**回退内容**。回退内容在子组件范围内编译,并且只有在托管元素为空且没有要插入的内容时才会显示。
假设我们有一个具有以下模板的组件
|
使用该组件的父组件标记
|
渲染结果将是
|
命名插槽
<slot>
元素有一个特殊的属性 name
,它可以用来进一步定制内容应该如何分发。你可以有多个具有不同名称的插槽。命名插槽将匹配内容片段中具有对应 slot
属性的任何元素。
仍然可以有一个未命名的插槽,它是**默认插槽**,作为任何不匹配内容的万能出口。如果没有默认插槽,不匹配的内容将被丢弃。
例如,假设我们有一个 multi-insertion
组件,其模板如下
|
父组件标记
|
渲染结果将是
|
内容分发 API 是设计用于组合在一起的组件时非常有用的机制。
动态组件
你可以使用相同的挂载点,并通过使用保留的 <component>
元素并动态绑定到其 is
属性来动态地在多个组件之间切换
|
|
keep-alive
如果你想让切换出的组件保持活动状态,以便你可以保留其状态或避免重新渲染,你可以添加一个 keep-alive
指令参数
|
activate
钩子
切换组件时,传入的组件可能需要执行一些异步操作,然后才能将其交换进来。为了控制组件交换的时机,在传入的组件上实现 activate
钩子
|
请注意,activate
钩子只在动态组件交换或静态组件的初始渲染期间生效 - 它不会影响使用实例方法的手动插入。
transition-mode
transition-mode
参数属性允许你指定两个动态组件之间的过渡应该如何执行。
默认情况下,传入和传出组件的过渡同时发生。此属性允许你配置另外两种模式
in-out
:新组件先过渡进来,当前组件在传入过渡完成后过渡出去。out-in
:当前组件先过渡出去,新组件在传出过渡完成后过渡进来。
示例
|
|
其他
组件和 v-for
你可以直接在自定义组件上使用 v-for
,就像任何普通元素一样
|
但是,这不会将任何数据传递给组件,因为组件拥有自己的独立作用域。为了将迭代数据传递到组件中,我们也应该使用 props
|
不自动将 item
注入组件的原因是,这会使组件与 v-for
的工作方式紧密耦合。明确其数据来源,使组件在其他情况下可重用。
编写可重用组件
编写组件时,要牢记你是否打算稍后在其他地方重用此组件。对于一次性组件,彼此之间存在一些紧密耦合是可以的,但可重用组件应该定义一个干净的公共接口。
Vue.js 组件的 API 本质上分为三个部分 - props、事件和插槽
**Props** 允许外部环境向组件提供数据;
**事件** 允许组件在外部环境中触发操作;
**插槽** 允许外部环境将内容插入组件的视图结构中。
使用 v-bind
和 v-on
的专用简写语法,可以在模板中清晰简洁地表达意图
|
异步组件
在大型应用程序中,我们可能需要将应用程序划分为更小的块,并且只有在实际需要时才从服务器加载组件。为了使这更容易,Vue.js 允许你将组件定义为一个异步解析组件定义的工厂函数。Vue.js 只有在组件实际需要渲染时才会触发工厂函数,并将结果缓存起来以供将来重新渲染。例如
|
工厂函数接收一个 resolve
回调函数,当从服务器检索到组件定义时,应该调用该回调函数。你也可以调用 reject(reason)
来表示加载失败。这里的 setTimeout
只是为了演示;如何检索组件完全取决于你。一种推荐的方法是将异步组件与 Webpack 的代码拆分功能 结合使用
|
资产命名约定
一些资产,例如组件和指令,以 HTML 属性或 HTML 自定义标签的形式出现在模板中。由于 HTML 属性名和标签名是**不区分大小写**的,我们经常需要使用 kebab-case 而不是 camelCase 来命名我们的资产,这可能有点不方便。
Vue.js 实际上支持使用 camelCase 或 PascalCase 命名你的资产,并在模板中自动将它们解析为 kebab-case(类似于 props 的名称转换)
|
|
这与 ES6 对象字面量简写 很好地配合使用
|
递归组件
组件可以在其自己的模板中递归调用自身,但是,只有当它具有 name
选项时才能这样做
|
像上面的组件将导致“最大堆栈大小超出”错误,因此请确保递归调用是有条件的。当你使用 Vue.component()
全局注册组件时,全局 ID 会自动设置为组件的 name
选项。
片段实例
当你使用 template
选项时,模板的内容将替换 Vue 实例挂载的元素。因此,建议在模板中始终只有一个根级别的普通元素。
不要使用这样的模板
|
更喜欢这样
|
有多种情况会导致 Vue 实例变成**片段实例**
- 模板包含多个顶级元素。
- 模板只包含纯文本。
- 模板只包含另一个组件(它可能本身就是一个片段实例)。
- 模板包含一个元素指令,例如
<partial>
或 vue-router 的<router-view>
。 - 模板根节点有一个流控制指令,例如
v-if
或v-for
。
原因是以上所有情况都会导致实例具有未知数量的顶级元素,因此它必须将 DOM 内容作为片段进行管理。片段实例仍然会正确渲染内容。但是,它**不会**有根节点,并且它的 $el
将指向一个“锚节点”,它是一个空的文本节点(或在调试模式下是一个注释节点)。
更重要的是,非流控制指令、非道具属性和组件元素上的过渡将被忽略,因为没有根元素可以绑定它们。
|
当然,片段实例有其有效的用例,但通常来说,为你的组件模板提供一个单一的、普通的根元素是一个好主意。这确保了组件元素上的指令和属性被正确地传递,并且也导致了略微更好的性能。
内联模板
当子组件上存在inline-template
特殊属性时,组件将使用其内部内容作为其模板,而不是将其视为分布式内容。这允许更灵活的模板编写。
|
然而,inline-template
使你的模板范围更难理解,并且使组件的模板编译不可缓存。作为最佳实践,最好使用template
选项在组件内部定义模板。