上一章我们讲到,一个 Vue 应用由根实例以及组件树(可复用的 Vue 实例)组成。我们讲了单个的 Vue 实例,本章我们来介绍多个 Vue 实例(组件)的组成——组件系统。
1. 组件系统
关于组件的划分,不同的人有不同的方式。而组件系统是一种抽象,允许我们使用小型、独立和通常可复用的组件构建大型应用。
1.1 什么是组件
简单来说,组件可以扩展 HTML 元素,封装可重用的代码。
<!--长这样--> <my-component></my-component>
别看它什么都没有,这只是我们最后使用的样子,逻辑都封装在组件里面了。一个组件,它的呈现可能会千奇百怪,因为不管怎么说,组件虽然可视为个体或是实例,但也是一种抽象。
图 4-1 一只猫也可以是一个组件
正如图 4-1 中的这只猫,它可以拖动,也可以鼠标悬浮弹出一个提示框,还可以双击或长按让它消失,这些逻辑也都封装在组件里,最终我们使用的时候,只需要引入组件,然后在页面里插入<Kitty></Kitty>
这样一个组件就可以了。
合理的抽象
几乎任意类型的应用界面都可以抽象为一个组件树,例如 Github 上 Vue 主页,我们能看到页面能划分成一块块的内容块,其中有些也可以看作组件:
图 4-2 Vue Github 主页
一般来说,这样的一个管理页面,我们可以抽象成这样的组件树:
图 4-3 Vue Github 主页组件树
以代码的方式来表达这样的组件树,通常的代码呈现是:
<div id="app"> <app-header> <header-search></header-search> <header-nav></header-nav> <header-aside></header-aside> </app-header> <app-view> <group-info></group-info> <app-tab></app-tab> <app-tab-container> <project-card></project-card> <card-list></card-list> </app-tab-container> </app-view> </div>
其实这个页面抽象的方法很多,在不同的开发者眼里,可以完全抽象成不同的树状。我们在实际开发中,具体要以怎样的逻辑或者思维去进行抽象,这些内容会在后面详述。这里我们就直接讲一些和组件相关的基础内容。
树状的组件管理
上面我们看到,几乎所有的页面都可以抽象成组件树,但为什么是树状呢?因为我们的页面组成、DOM 节点、HTML 元素也都是树状结构的,组件说白了也就是将一部分的 HTML+CSS+Javascript 代码组合成一个可复用、更简洁表达的代码片段,因此最终呈现也是树状结构的。
树状结构是组织和管理中十分快捷、方便又清晰的结构,我们的文件管理、思维导图等也都是基于树状结构来进行的。第1章中有讲,AngularJS 最初的 watch 机制是环状的,导致了一些性能问题,在升级成 Angular 之后,也调整为树状结构,性能也上去了。组件系统使用这样的方式,可以由上到下的方式来管理,不管是事件的通知、数据的检测、父子组件的通信等,都会更高效。
讲了这么多,我们来看一下组件的定义和使用方式。
2. 组件使用
组件是可复用的 Vue 实例,所以它们与new Vue()
接收相同的选项,例如data
、computed
、watch
、methods
以及生命周期钩子等(生命周期一致,可以参考第3章中 Vue 实例的生命周期)。除了el
这种是根实例特有的选项。
2.1 注册方式
为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。Vue 中有两种组件的注册类型:全局注册和局部注册。
组件名
HTML 中的特性名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。在 Vue 中,组件命名有两种方式:
(1) 使用短横线分隔(kebab-case)命名。当使用 kebab-case 定义一个组件时,你也必须在引用这个自定义元素时使用 kebab-case,如<my-component></my-component>
。
(2) 使用大驼峰(PascalCase)。当使用 PascalCase 定义一个组件时,既可以使用 PascalCase 也可以使用 kebab-case 来引用,像<my-component></my-component>
和<MyComponent></MyComponent>
。
全局注册
全局注册可以通过Vue.component()
的方式进行,该方法第一个参数要传入组件的名称,第二个参数传入该组件的选项:
Vue.component("my-button", { // 选项 // 除了 el 以外,组件的选项与 Vue 实例相同 });
局部注册
局部注册可通过在实例中的components
选项进行配置:
// 获取组件 import MyButton from "../components/my-button"; new Vue({ components: { MyButton } });
而以这种方式使用组件的时候,则需要在组件里通过name
选项进行命名:
// my-button.vue new Vue({ name: "my-button" });
这种方式定义的组件,如果也进行了全局注册,其命名会以全局注册的名字为准,也就是全局注册的命名优先级更高。
2.2 单文件组件
前面讲到,一个组件是一些逻辑和功能完整的代码片段组成的,同时也包括了 HTML、CSS 和 Javascript 的代码。在 Vue 里,我们常常使用单文件组件,使用.vue 后缀命名的文件,一般也包括这三部分:
<template> <!-- 组件模板 --> </template> <script> // 组件逻辑 // 在.vue文件中,需要默认export一个Vue实例 export default { name: "MyComponent" }; </script> <style> /* 组件样式 */ </style>
通过这种方式,我们可以更方便地在项目中管理组件和文件。这里的样式,我们还可以通过添加scoped
属性的方式<style scoped>
,来增加组件的样式作用域。具体的实现是,通过使用 PostCSS 来实现以下转换:
<template> <div class="example">hi</div> </template> <style scoped> .example { color: red; } </style> <!-- 会转换成以下 --> <!-- 通过增加局部随机属性,来匹配到具体的组件 --> <template> <div class="example" data-v-f3f3eg9>hi</div> </template> <style> .example[data-v-f3f3eg9] { color: red; } </style>
除了scoped
之外,前面我们也讲到官方的脚手架 Vue CLI 中内置了 CSS Loader 一大堆,包括 extract-css-loader、vue-style-loader、css-loader、cssnano、postcss-loader、sass-loader、less-loader 等。所以我们在使用官方脚手架的时候,也可以尽情地使用这些 CSS 预处理器,使用 lang 属性就可以:<style lang="scss"></style>
。
单文件组件是 Vue 里推荐的使用方式,如果开发者不习惯在一个页面中同时维护着 HTML、CSS 和 Javascript 的话,也可以通过 src 属性等方式来引入:
<!-- my-component.vue --> <template> <div>This will be pre-compiled</div> </template> <script src="./my-component.js"></script> <style src="./my-component.css"></style>
3. 组件间通信
第3章中我们介绍了 Vue 实例常用的选项和模板语法,而 Vue 组件其实也是 Vue 实例,但组件系统由于树状结构而构成的父子组件等关系,则是单个 Vue 实例中没有的。因此,这里我们重点介绍一下 Vue 组件间的数据传递和通信等。
3.1 Prop 数据传递
大多数情况下,我们会将部分的代码抽象成组件,是因为该部分的内容在别处也有使用到,而抽象成组件,可以提供更简单的复用方式。例如常用的按钮,我们可以对它进行封装。在 Vue 实例的选项中,我们可以用一个props
选项将其包含在该组件可接受的 prop 列表中:
<template> <button class="my-button" @click="handleClick" :disabled="disabled || loading" :type="type" :class="[ type ? 'my-button--' + type : '', buttonSize ? 'my-button--' + buttonSize : '', { 'is-disabled': disabled, 'is-loading': loading } ]" > <i class="el-icon-loading" v-if="loading"></i> <i :class="icon" v-if="icon && !loading"></i> <span v-if="$slots.default"><slot></slot></span> </button> </template> <script> export default { name: "MyButton", props: { // 按钮类型,如info、warn、error等 type: { type: String, default: "default" }, // icon类型,匹配样式 icon: { type: String, default: "" }, loading: Boolean, // 是否在加载中 disabled: Boolean // 是否不可用 }, methods: { // 点击触发click事件 handleClick(evt) { this.$emit("click", evt); } } }; </script>
可以看到,该组件封装了按钮类型、图标和状态,通过 prop 提供给外部配置使用。Prop 是我们可以在组件上注册的一些自定义特性,常常用于接收来自父组件的数据/属性值,我们可以直接在需要的地方使用:
<my-button>原按钮</my-button> <!-- 可以像这样给 prop 传入一个静态的值 --> <my-button type="info" icon="config">提示样式按钮(带管理图标)</my-button> <!-- 也可以通过 v-bind 动态赋值 --> <my-button type="info" :loading="true">提示样式按钮(加载中)</my-button> <my-button type="error" :disabled="true">错误样式按钮(不可用)</my-button>
一个组件默认可以拥有任意数量的 prop,任何值都可以传递给任何 prop。
Prop 命名
前面我们讲过在 Vue 中组件的命名,包括短横线分隔(kebab-case)和大驼峰(PascalCase)。我们知道,HTML 中的特性名是大小写不敏感的,因此 PascalCase 的 prop 名需要使用其等价的 kebab-case 来使用:
Vue.component("my-component", { // 在 JavaScript 中是 camelCase 的 props: ["myMessage"], template: "<p>{{ myMessage }}</p>" });
<!-- 在 HTML 中是 kebab-case 的 --> <my-component my-message="test"></my-component>
Prop 类型
props
可以是简单的数组,或者使用对象作为替代,对象允许配置高级选项,如类型检测、自定义验证和设置默认值等等:
表 4-1 props
高级选项
props 高级选项 |
作用 | 说明 |
---|---|---|
type |
会检查该 prop 是否是给定的类型,否则抛出警告 | 可以是下列原生构造函数中的一种:String 、Number 、Boolean 、Array 、Object 、Date 、Function 、Symbol 、任何自定义构造函数、或上述内容组成的数组 |
default |
为该 prop 指定一个默认值 | 如果该 prop 没有被传入,则换做用这个值,对象或数组的默认值必须从一个工厂函数返回 |
required |
定义该 prop 是否是必填项,默认为false
|
在非生产环境中,如果这个值为true 且该prop 没有被传入的,则一个控制台警告将会被抛出 |
validator |
自定义验证函数会将该 prop 的值作为唯一的参数代入 | 在非生产环境下,如果该函数返回一个false 的值 (也就是验证失败),一个控制台警告将会被抛出 |
我们来直接看看代码更直观:
Vue.component("my-component", { // 简单的数组 props: ["propA", "propB", "propC", "propD", "propE", "propF"], // 高级配置 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) { // 这个值必须匹配下列字符串中的一个 // 当 prop 验证失败的时候,(开发环境构建版本的) Vue 将会产生一个控制台的警告。 return ["success", "warning", "danger"].indexOf(value) !== -1; } } } });
3.2 父子组件通信
所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。除了父组件给子组件传递数据之外,有时候我们也需要在子组件中和父级组件进行沟通。在 Vue 中,父级组件可以像处理原生的 DOM 事件一样通过v-on
监听子组件实例的任意事件:
<my-button @click="handleClick">自定义按钮</my-button>
自定义事件
上一章我们在介绍 Vue 常用的模板语法时,也讲到了事件绑定。除了原生的 DOM 事件之外,Vue 里还提供了自定义事件系统。子组件可以通过调用内建的$emit
方法,并传入事件名称来触发一个事件:
<button v-on:click="$emit('test')">test</button> <!-- $emit 可以通过参数传值 --> <button v-on:click="$emit('plus', 1)">Plus One</button>
这时候,父组件可以通过v-on
监听(参数值将会作为参数传入这个方法):
<my-button @plus="handlePlus">自定义按钮</my-button> <script> export default { methods: { handlePlus(num) { this.menberNum += num; } } }; </script>
通过 prop 和自定义事件,已经可以实现了父子组件间的双向通信了。而其实在 Vue 中还可以有更多的组件间、全局的通信方式,会在后面章节中阐述。
3.3 slot 插槽
前面我们在自定义<my-button>
组件中,有使用到 slot 这个 DOM 元素。Vue 实现了一套内容分发的 API,将<slot>
元素作为承载分发内容的出口。这个自定义的<slot>
元素,可以让我们向一个组件传递自定义的内容。
还是回到<my-button>
组件:
<template> <button class="my-button"> <i class="el-icon-loading" v-if="loading"></i> <i :class="icon" v-if="icon && !loading"></i> <span> <slot></slot> </span> </button> </template>
我们可以给该组件添加内容,添加的内容会替换<slot></slot>
:
<my-button>按钮</my-button> <!-- 最终会变成这样 --> <button><span>按钮</span></button> <my-button><a>test</a>按钮</my-button> <!-- 最终会变成这样 --> <button><span><a>test</a>按钮<span></button> <my-button loading><a>test</a>按钮</my-button> <!-- 最终会变成这样 --> <button><i class="el-icon-loading"></i><span><a>test</a>按钮</span></button>
默认内容
我们可以在<slot></slot>
元素里,为一个插槽设置具体的后备 (也就是默认的) 内容:
<template> <button class="my-button"> <slot>button</slot> </button> </template>
我们在使用的时候,会这样渲染:
<my-button>按钮</my-button> <!-- 最终会变成这样 --> <button>按钮</button> <my-button></my-button> <!-- 在没有提供内容的时候被渲染 --> <!-- 最终会变成这样 --> <button>button</button>
具名插槽
有时我们需要多个插槽,<slot>
元素可以通过 name 特性来定义额外的插槽。例如我们封装了一个弹窗组件:
<!-- my-dialog.vue --> <div class="dialog-container"> <header> <!-- 弹窗头部内容 --> <slot name="header">提示</slot> </header> <main> <!-- 弹窗主要内容 --> <!-- 一个不带 name 的 <slot> 出口会带有隐含的名字“default” --> <!-- 相当于 <slot name="default"></slot> --> <slot></slot> </main> <footer> <!-- 弹窗底部内容,通常是按钮 --> <slot name="footer"> <!-- 设置了默认内容 --> <button @click="$emit('confirm')">确定</button> </slot> </footer> </div>
使用的时候,我们可以在一个<template>
元素上使用v-slot
指令(注意v-slot
只能添加在一个<template>
上,只有默认插槽时,组件的标签才可以被当作插槽的模板来使用):
<my-dialog>确认要关闭吗?</my-dialog> <!-- 任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为默认插槽的内容 --> <!-- 最终会变成这样 --> <div class="dialog-container"> <header>提示</header> <main>确认要关闭吗?</main> <footer> <button @click="$emit('confirm')">确定</button> </footer> </div> <my-dialog> <template v-slot:header>请确认</template> <template>确认要关闭吗?</template> <template v-slot:footer> <button>确定</button> <button>取消</button> </template> </my-dialog> <!-- 最终会变成这样 --> <div class="dialog-container"> <header>请确认</header> <main>确认要关闭吗?</main> <footer> <button>确定</button> <button>取消</button> </footer> </div>
v-slot
也有缩写,即把参数之前的所有内容 (v-slot:
) 替换为字符#
:
<my-dialog> <template v-slot:header>请确认</template> <template>确认要关闭吗?</template> <template v-slot:footer> <button>确定</button> <button>取消</button> </template> </my-dialog> <!-- 可以这么进行缩写 --> <my-dialog> <template #header>请确认</template> <template>确认要关闭吗?</template> <template #footer> <button>确定</button> <button>取消</button> </template> </my-dialog>
以上内容是 Vue 中组件的基础内容,而在真实工作中,通常烦扰我们的不是一些基本语法的使用,而是如何进行组件的划分。关于组件的划分,我们来介绍一些好用的经验吧。
4. 组件的自我修养
组件的划分通常分成两个方式:
(1) 【设计上】视觉和交互上是一个完整的组件。
(2) 【实现上】写代码的时候,可重复的内容即可视为一个组件。
4.1 通过视觉和交互划分
第一个角度,(设计上)视觉和交互上是一个完整的组件。
通常来说,组件的划分,与视觉、交互等密切相关,我们可通过功能、独立性来判断是否适合作为一个组件。这里挑了个视频网站的样式,如图 4-4:
图 4-4 某视频网站首页组件划分方式一
我们第一眼看上去可区分出独立的视图和功能的,则可以列为这种组件的划分。或许也可以成为区块,是为视觉和交互上完整的组件。其实这样的划分,也是需要不断地在实现过程中思考和总结。而通常情况下,设计、交互以及各种开发,都能将这样的一块内容划分出来,我们可以作为一张带图片、标题和描述的卡片来看待。
如果用代码表示,这样一张卡片的使用应该是:
<card url="xxx" title="喵喵喵" text="喵喵喵喵喵" info="58:43"></card>
4.2 通过代码复用划分
第二个角度,(实现上)写代码的时候,可重复的内容即可视为一个组件。还是这个页面,我们看:
图 4-5 某视频网站首页组件划分方式二
这里,我们能看到这种卡片形式的内容,存在页面中的各个地方。而框住的这一整块内容,在代码实现上其实是一致的,所以我们也可以同样将他们划分为一个组件。这样的话,用代码表示应该是:
<card-container title="你喜欢的类型,这里都有"> <card v-for="card in cardList1" :url="card.url" :title="card.title" :text="card.text" :info="card.info" ></card> </card-container> <card-container title="你不喜欢的类型,这里也有"> <card v-for="card in cardList2" :url="card.url" :title="card.title" :text="card.text" :info="card.info" ></card> </card-container>
这种情况下,我们将卡片封装成一个简单的组件,包括图片+文字描述。同时,也将有重复的部分代码,我们将它抽象成一个<card-container>
的组件。这种方式的组件划分,在理解上或许没有从业务上划分的直观,会有人觉得这里就一行字,为什么我还得封装成一个组件呢?我完全可用这么写:
<p>你喜欢的类型,这里都有</p> <card v-for="card in cardList1" :url="card.url" :title="card.title" :text="card.text" :info="card.info" ></card> <p>你不喜欢的类型,这里也有</p> <card v-for="card in cardList2" :url="card.url" :title="card.title" :text="card.text" :info="card.info" ></card>
的确这样写也是很简单的,而且还减少了一个组件的维护。至于到哪种程度才需要单独封装一个组件呢,这需要你不断地探索和实践,同时也需要和合作伙伴一起商量讨论。很多时候,我们要正确地做好抽象和组件划分,也少不了设计同学的配合。
怎样才能算是一个合格的组件呢?我们在设计的时候,经常要考虑解耦,但很多时候,过度的解耦反而会导致项目复杂度变高,维护性降低。总的来看,使用设计角度或者实现角度来进行封装,哪种方式效率高一些也都不好说。(温馨提示:在一个团队内,最好是使用一种方式来进行划分。因为对于成员的相互配合和项目的维护来说,统一的规范是比较重要的。)
4.3 组件的封装
个人认为,一个称职的组件,是以下形式的:
表 4-2 怎样才算一个称职的组件
能力描述 | Vue 中对应属性 |
---|---|
组件内维护自身的数据和状态 | data |
组件内维护自身的事件 | methods |
通过提供配置的方式,来控制展示,或者控制执行逻辑 | props |
通过一定的方式(事件触发/监听、API 提供),提供与外界(如父组件)通信的方式 |
$emit 、$on 、ref
|
笼统地概括下,就是我们尽量需要把组件进行隔离,拥有独立的个体空间,同时保持与外界适当的联系。
组件内维护自身的数据和状态
这个比较好理解,以这样一个视频的小卡片为例子:
图 4-6 视频小卡片
这个小卡片,它维护着自己的数据:封面图、描述、头像、作者。还有一个初始的状态,就是目前我们看到的样子。这些内容保存在组件自己的 scope 里,每个卡片组件都拥有自己的数据和状态。
组件内维护自身的事件
我们在把鼠标放在卡片上,随着鼠标的位置,顶部会有个小小的进度条,同时封面图会改变,如图:
图 4-7 视频小卡片进度条改变
每个小卡片都有自己 mousemove 事件。当然,这里面维护了一个鼠标位置的状态,同时根据鼠标位置来控制图片的展示。
通过初始化事件,来初始化组件状态,激活组件
组件的数据,大多数需要外界提供并传入,故可通过初始化事件来激活。
对外提供配置项,来控制展示以及具体功能
可以通过配置的方式,来控制组件的展示和功能。
我们看这个小卡片:
图 4-8 另一种视频小卡片
和上面的不一样,左下角展示的是视频时长,而不是头像和名字,这种我们可以通过配置的方式来控制。
通过对外提供查询接口,可获取组件状态
很多时候,组件独立维护着自身的数据和状态,但有些场景下,父组件或者应用需要知道组件当前的状态,故我们需要对外提供接口,以供查询。像 Vue 中,便可以通过vm.$refs
来获取子组件的实例。
4.4 独立的组件
组件的独立性,可以包括以下几个方面:
- 维护自身的数据和状态、作用域
- 维护自身的事件
拿一个常见的内容卡片来看:
图 4-9 内容小卡片
这是个独立的卡片,它拥有以下的数据、状态和事件:
(1) 内容数据,包括标题、文字、图片、点赞数、评论数、日期等。
(2) 状态数据,是否已点赞、是否已收藏、是否详细展示内容、是否展示评论等。
(3) 事件,包括点击分享、收藏、点赞、回复等等。
<template> <div> <h2>{{cardInfo.question}}</h2> <div :class="isContextShown ? 'content-detail' : 'content-brief'"> <div v-if="cardInfo.withImage"><img :url="cardInfo.imageUrl" /></div> <div>{{cardInfo.content}}</div> </div> <div> <span @click="likeIt()">点赞</span> <span @click="keepIt()">收藏</span> </div> </div> </template> <script> export default { name: "my-card", props: { // 传入卡片信息 cardInfo: { type: Object, default: () => {} } }, data() { return { isContextShown: false // 是否省略 }; }, methods: { likeIt() {}, // 点赞 keepIt() {} // 收藏 }, mounted() {} }; </script>
简化后大概就是这样一个组件。我们来看看数据这部分,正如前面说过组件和数据抽离,那么我们可以获取到两部分的内容:
(1) 通过props
从外部注入的数据,例如这个卡片的问题、回答、是否带图片、是否已收藏、是否已点赞等等。
(2) 组件自己维护的数据data
,例如这个卡片是被展开还是被省略。
两部分的数据划分,区别常常在于这个状态是否依赖外界,或是与外界有联系。如果我们一整个页面里有很多的卡片,但同时只允许展开一个,这种情况下我们可以选择把isContextShown
也放到props
里,通过外部控制。
4.5 组件与外界
我们在保持组件独立性的时候,当然还需要考虑它与外界的交互,主要包括两个方面。
(1) 对外提供配置项,来控制展示以及具体功能。
这里最简单的,我们每个卡片都需要传入内容,我们一次性拉取列表数据,并传入每个卡片,在 Vue 中可以使用 props。
(2) 对外提供查询接口,可从外界获取组件事件和状态。
这个的话,更多时候我们是通过事件等方式来告诉外界一些事情。我们前面也做了个假设,一个页面只允许一个卡片内容处于详细展开状态,除了把isContextShown
放到外部控制的方法之外,我们还需要通过事件通知外部该状态的变更:
<template> <div> <h2>{{cardInfo.question}}</h2> <div @click="toggleContext()" :class="cardInfo.isContextShown ? 'content-detail' : 'content-brief'" > <div v-if="cardInfo.withImage"><img :url="cardInfo.imageUrl" /></div> <div>{{cardInfo.content}}</div> </div> <div> <span @click="likeIt()">点赞</span> <span @click="keepIt()">收藏</span> </div> </div> </template> <script> export default { name: "my-card", props: { // (1) 对外提供配置项,来控制展示以及具体功能 cardInfo: { type: Object, default: () => {} } }, methods: { likeIt() {}, keepIt() {}, toggleContext() { // (2) 通过事件通知,通知外界状态变更,以及最新的状态 this.$emit("toggle", this.cardInfo); } }, mounted() {} }; </script>
简单调整之后,我们会这样使用:
<template> <my-card v-for="card in cardList" :cardInfo="card" @toggle="toggleCard" ></my-card> </template> <script> export default { data() { return { cardList: [ // ...此处省略 ] }; }, methods: { toggleCard($event) { const cardInfo = $event.target.value; if (cardInfo.isContextShown) { // 如果该卡片原先未省略状态,展开后把其他卡片也省略 this.cardList = this.cardList.map(card => { return { ...card, isContextShown: cardInfo.id === card.id }; }); } else { // 如果该卡片原先展开状态,则把该卡片省略就可以 const cardIndex = this.cardList.findIndex(x => x.id === cardInfo.id); // Vue不能检测以下数组的变动,故需要使用Vue.set Vue.set(this.cardList, cardIndex, { ...cardInfo, isContextShown: false }); } } } }; </script>
这是最简单的对内和对外的联系,对一个组件来说,它也有 in 和 out 两个方向的流动。在 Vue 里,如果父组件需要获取子组件的实例,也可以通过vm.$refs
来获取。
以上,结束了本章 Vue 组件的内容,包括基本语法、API,还有常见的组件封装原则和划分方式。当然,这些内容并不是一定的,书中的内容永远是死的,而我们面临的问题往往却是奇形怪状的,但是思考依然是一刻不能少。所以,本章内容更重要的是给大家一个参考的逻辑思维,思考和总结才是不断进步的方式。