diff --git a/src/v2/guide/components.md b/src/v2/guide/components.md index 512457671..875c8688c 100644 --- a/src/v2/guide/components.md +++ b/src/v2/guide/components.md @@ -1,1476 +1,596 @@ --- -title: 组件 +title: 组件基础 type: guide order: 11 --- -## 什么是组件? +## 基本示例 -组件 (Component) 是 Vue.js 最强大的功能之一。组件可以扩展 HTML 元素,封装可重用的代码。在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能。在有些情况下,组件也可以表现为用 `is` 特性进行了扩展的原生 HTML 元素。 - -所有的 Vue 组件同时也都是 Vue 的实例,所以可接受相同的选项对象 (除了一些根级特有的选项) 并提供相同的生命周期钩子。 - -## 使用组件 - -### 全局注册 - -我们已经知道,可以通过以下方式创建一个 Vue 实例: +这里有一个 Vue 组件的示例: ``` js -new Vue({ - el: '#some-element', - // 选项 +// 定义一个名为 button-counter 的新组件 +Vue.component('button-counter', { + data: function () { + return { + count: 0 + } + }, + template: '' }) ``` -要注册一个全局组件,可以使用 `Vue.component(tagName, options)`。例如: +组件是可复用的 Vue 实例,且带有一个名字:在这个例子中是 ``。我们可以在一个通过 `new Vue` 创建的 Vue 根实例中,把这个组件作为自定义元素来使用: -``` js -Vue.component('my-component', { - // 选项 -}) +```html +
+ +
``` -

请注意,对于自定义标签的命名 Vue.js 不强制遵循 [W3C 规则](https://www.w3.org/TR/custom-elements/#concepts) (小写,并且包含一个短杠),尽管这被认为是最佳实践。

- -组件在注册之后,便可以作为自定义元素 `` 在一个实例的模板中使用。注意确保在初始化根实例**之前**注册组件: - -``` html -
- -
+```js +new Vue({ el: '#components-demo' }) ``` -``` js -// 注册 -Vue.component('my-component', { - template: '
A custom component!
' +{% raw %} +
+ +
+ +{% endraw %} -// 创建根实例 -new Vue({ - el: '#example' -}) -``` +因为组件是可复用的 Vue 实例,所以它们与 `new Vue` 接收相同的选项,例如 `data`、`computed`、`watch`、`methods` 以及生命周期钩子等。仅有的例外是像 `el` 这样根实例特有的选项。 -渲染为: +## 组件的复用 -``` html -
-
A custom component!
+你可以将组件进行任意次数的复用: + +```html +
+ + +
``` {% raw %} -
- +
+ + +
{% endraw %} -### 局部注册 +注意当点击按钮时,每个组件都会各自独立维护它的 `count`。因为你每用一次组件,就会有一个它的新**实例**被创建。 -你不必把每个组件都注册到全局。你可以通过某个 Vue 实例/组件的实例选项 `components` 注册仅在其作用域中可用的组件: +### `data` 必须是一个函数 -``` js -var Child = { - template: '
A custom component!
' +当我们定义这个 `` 组件时,你可能会发现它的 `data` 并不是像这样直接提供一个对象: + +```js +data: { + count: 0 } +``` -new Vue({ - // ... - components: { - // 将只在父组件模板中可用 - 'my-component': Child +取而代之的是,**一个组件的 `data` 选项必须是一个函数**,因此每个实例可以维护一份被返回对象的独立的拷贝: + +```js +data: function () { + return { + count: 0 } -}) +} ``` -这种封装也适用于其它可注册的 Vue 功能,比如指令。 +如果 Vue 没有这条规则,点击一个按钮就可能会像如下代码一样影响到*其它所有实例*: + +{% raw %} +
+ + + +
+ +{% endraw %} -### DOM 模板解析注意事项 +## 组件的组织 -当使用 DOM 作为模板时 (例如,使用 `el` 选项来把 Vue 实例挂载到一个已有内容的元素上),你会受到 HTML 本身的一些限制,因为 Vue 只有在浏览器解析、规范化模板**之后**才能获取其内容。尤其要注意,像 `
    `、`
      `、``、`
      - ... -
      -``` +例如,你可能会有页头、侧边栏、内容区等组件,每个组件又包含了其它的像导航链接、博文之类的组件。 -自定义组件 `` 会被当作无效的内容,因此会导致错误的渲染结果。变通的方案是使用特殊的 `is` 特性: +为了能在模板中使用,这些组件必须先注册以便 Vue 能够识别。这里有两种组件的注册类型:**全局注册**和**局部注册**。至此,我们的组件都只是通过 `Vue.component` 全局注册的: -``` html - - -
      +```js +Vue.component('my-component-name', { + // ... options ... +}) ``` -**应当注意,如果使用来自以下来源之一的字符串模板,则没有这些限制:** +全局注册的组件可以用在其被注册之后的任何 (通过 `new Vue`) 新创建的 Vue 根实例,也包括其组件树中的所有子组件的模板中。 -- ` {% endraw %} -由于这三个组件实例共享了同一个 `data` 对象,因此递增一个 counter 会影响所有组件!这就错了。我们可以通过为每个组件返回全新的数据对象来修复这个问题: +然而在一个典型的应用中,你可能在 `data` 里有一个博文的数组: -``` js -data: function () { - return { - counter: 0 +```js +new Vue({ + el: '#blog-post-demo', + data: { + posts: [ + { id: 1, title: 'My journey with Vue' }, + { id: 2, title: 'Blogging with Vue' }, + { id: 3, title: 'Why Vue is so fun' }, + ] } -} +}) ``` -现在每个 counter 都有它自己内部的状态了: +并想要为每篇博文渲染一个组件: -{% raw %} -
      - - - -
      - -{% endraw %} +```html + +``` -### 组件组合 +如上所示,你会发现我们可以使用 `v-bind` 来动态传递 prop。这在你一开始不清楚要渲染的具体内容,比如[从一个 API 获取博文列表](https://jsfiddle.net/chrisvfritz/sbLgr0ad)的时候,是非常有用的。 -组件设计初衷就是要配合使用的,最常见的就是形成父子组件的关系:组件 A 在它的模板中使用了组件 B。它们之间必然需要相互通信:父组件可能要给子组件下发数据,子组件则可能要将它内部发生的事情告知父组件。然而,通过一个良好定义的接口来尽可能将父子组件解耦也是很重要的。这保证了每个组件的代码可以在相对隔离的环境中书写和理解,从而提高了其可维护性和复用性。 +到目前为止,关于 prop 你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把 [prop](components-props.html) 读完。 -在 Vue 中,父子组件的关系可以总结为 **prop 向下传递,事件向上传递**。父组件通过 **prop** 给子组件下发数据,子组件通过**事件**给父组件发送消息。看看它们是怎么工作的。 +## 单个根元素 -

      - prop 向下传递,事件向上传递 -

      +当构建一个 `` 组件时,你的模板最终会包含的东西远不止一个标题: -## Prop +```html +

      {{ post.title }}

      +``` -### 使用 Prop 传递数据 +最最起码,你会包含着篇博文的正文: -组件实例的作用域是**孤立的**。这意味着不能 (也不应该) 在子组件的模板内直接引用父组件的数据。父组件的数据需要通过 **prop** 才能下发到子组件中。 +```html +

      {{ post.title }}

      +
      +``` -子组件要显式地用 [`props` 选项](../api/#props)声明它预期的数据: +然而如果你在模板中尝试这样写,Vue 会显示一个错误,并解释道 **every component must have a single root element (每个组件必须只有一个根元素)**。你可以将模板的内容包裹在一个父元素内,来修复这个问题,例如: -``` js -Vue.component('child', { - // 声明 props - props: ['message'], - // 就像 data 一样,prop 也可以在模板中使用 - // 同样也可以在 vm 实例中通过 this.message 来使用 - template: '{{ message }}' -}) +```html +
      +

      {{ post.title }}

      +
      +
      ``` -然后我们可以这样向它传入一个普通字符串: +## 通过事件向父级组件发送消息 -``` html - -``` +在我们开发 `` 组件时,它的一些功能可能要求我们和父级组件进行沟通。例如我们可能会引入一个可访问性的功能来放大博文的字号,同时让页面的其它部分保持默认的字号。 -结果: +在其父组件中,我们可以通过添加一个 `postFontSize` 数据属性来支持这个功能: -{% raw %} -
      - -
      - -{% endraw %} - -### camelCase vs. kebab-case +``` -HTML 特性是不区分大小写的。所以,当使用的不是字符串模板时,camelCase (驼峰式命名) 的 prop 需要转换为相对应的 kebab-case (短横线分隔式命名): +它可以在模板中用来控制所有博文的字号: -``` js -Vue.component('child', { - // 在 JavaScript 中使用 camelCase - props: ['myMessage'], - template: '{{ myMessage }}' -}) +```html +
      +
      + +
      +
      ``` -``` html - - -``` +现在我们在每篇博文正文之前添加一个按钮来放大字号: -如果你使用字符串模板,则没有这些限制。 +```js +Vue.component('blog-post', { + props: ['post'], + template: ` +
      +

      {{ post.title }}

      + +
      +
      + ` +}) +``` -### 动态 Prop +

      上述的这个和一些接下来的示例使用了 JavaScript 的[模板字符串](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Template_literals)来让多行的模板更易读。它们在 IE 下并没有被支持,所以如果你需要在不 (经过 Babel 或 TypeScript 之类的工具) 编译的情况下支持 IE,请使用[折行转义字符](https://css-tricks.com/snippets/javascript/multiline-string-variables-in-javascript/)取而代之。

      -与绑定到任何普通的 HTML 特性相类似,我们可以用 `v-bind` 来动态地将 prop 绑定到父组件的数据。每当父组件的数据变化时,该变化也会传导给子组件: +问题是这个按钮不会做任何事: -``` html -
      - -
      - -
      +```html + ``` -``` js -new Vue({ - el: '#prop-example-2', - data: { - parentMsg: 'Message from parent' - } -}) +当点击这个按钮时,我们需要告诉父级组件放大所有博文的文本。幸好 Vue 实例提供了一个自定义事件的系统来解决这个问题。我们可以调用内建的 [**`$emit`** 方法](../api/实例方法-事件)并传入事件的名字,来向父级组件触发一个事件: + +```html + ``` -你也可以使用 `v-bind` 的缩写语法: +然后我们可以用 `v-on` 在博文组件上监听这个事件,就像监听一个原生 DOM 事件一样: -``` html - +```html + ``` -结果: - {% raw %} -
      - -
      - +
      +
      + +
      {% endraw %} -如果你想把一个对象的所有属性作为 prop 进行传递,可以使用不带任何参数的 `v-bind` (即用 `v-bind` 而不是 `v-bind:prop-name`)。例如,已知一个 `todo` 对象: - -``` js -todo: { - text: 'Learn Vue', - isComplete: false -} -``` +### 使用事件抛出一个值 -然后: +有的时候用一个事件来抛出一个特定的值是非常有用的。例如我们可能想让 `` 组件决定它的文本要放大多少。这时可以使用 `$emit` 的第二个参数来提供这个值: -``` html - +```html + ``` -将等价于: +然后当在父级组件监听这个事件的时候,我们可以通过 `$event` 访问到被抛出的这个值: -``` html - +```html + ``` -### 字面量语法 vs 动态语法 - -初学者常犯的一个错误是使用字面量语法传递数值: +或者,如果这个事件处理函数是一个方法: -``` html - - +```html + ``` -因为它是一个字面量 prop,它的值是字符串 `"1"` 而不是一个数值。如果想传递一个真正的 JavaScript 数值,则需要使用 `v-bind`,从而让它的值被当作 JavaScript 表达式计算: +那么这个值将会作为第一个参数传入这个方法: -``` html - - +```js +methods: { + onEnlargeText: function (enlargeAmount) { + this.postFontSize += enlargeAmount + } +} ``` -### 单向数据流 - -Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。 - -另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你**不应该**在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。 - -在两种情况下,我们很容易忍不住想去修改 prop 中数据: - -1. Prop 作为初始值传入后,子组件想把它当作局部数据来用; - -2. Prop 作为原始数据传入,由子组件处理成其它数据输出。 +### 在组件上使用 `v-model` -对这两种情况,正确的应对方式是: +自定义事件也可以用于创建支持 `v-model` 的自定义输入组件。记住: -1. 定义一个局部变量,并用 prop 的值初始化它: +```html + +``` - ``` js - props: ['initialCounter'], - data: function () { - return { counter: this.initialCounter } - } - ``` +等价于: -2. 定义一个计算属性,处理 prop 的值并返回: +```html + +``` - ``` js - props: ['size'], - computed: { - normalizedSize: function () { - return this.size.trim().toLowerCase() - } - } - ``` +当用在组件上时,`v-model` 则会这样: -

      注意在 JavaScript 中对象和数组是引用类型,指向同一个内存空间,如果 prop 是一个对象或数组,在子组件内部改变它**会影响**父组件的状态。

      +``` html + +``` -### Prop 验证 +为了让它正常工作,这个组件内的 `` 必须: -我们可以为组件的 prop 指定验证规则。如果传入的数据不符合要求,Vue 会发出警告。这对于开发给他人使用的组件非常有用。 +- 将其 `value` 特性绑定到一个名叫 `value` 的 prop 上 +- 在其 `input` 事件被触发时,将新的值通过自定义的 `input` 事件抛出 -要指定验证规则,需要用对象的形式来定义 prop,而不能用字符串数组: +写成代码之后是这样的: -``` js -Vue.component('example', { - props: { - // 基础类型检测 (`null` 指允许任何类型) - 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 value > 10 - } - } - } +```js +Vue.component('custom-input', { + props: ['value'], + template: ` + +``` -所谓非 prop 特性,就是指它可以直接传入组件,而不需要定义相应的 prop。 +到目前为止,关于组件自定义事件你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把[自定义事件](components-custom-events.html)读完。 -尽管为组件定义明确的 prop 是推荐的传参方式,组件的作者却并不总能预见到组件被使用的场景。所以,组件可以接收任意传入的特性,这些特性都会被添加到组件的根元素上。 +## 通过插槽分发内容 -例如,假设我们使用了第三方组件 `bs-date-input`,它包含一个 Bootstrap 插件,该插件需要在 `input` 上添加 `data-3d-date-picker` 这个特性。这时可以把特性直接添加到组件上 (不需要事先定义 `prop`): +和 HTML 元素一样,我们经常需要向一个组件传递内容,像这样: ``` html - + + Something bad happened. + ``` -添加属性 `data-3d-date-picker="true"` 之后,它会被自动添加到 `bs-date-input` 的根元素上。 +可能会渲染出这样的东西: -### 替换/合并现有的特性 +{% raw %} +
      + + Something bad happened. + +
      + + +{% endraw %} -假设这是 `bs-date-input` 的模板: +幸好,Vue 自定义的 `` 元素让这变得非常简单: -``` html - +```js +Vue.component('alert-box', { + template: ` +
      + Error! + +
      + ` +}) ``` -为了给该日期选择器插件增加一个特殊的主题,我们可能需要增加一个特殊的 class,比如: +如你所见,我们只要在需要的地方加入插槽就行了——就这么简单! -``` html - -``` +到目前为止,关于插槽你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把[插槽](components-slots.html)读完。 -在这个例子当中,我们定义了两个不同的 `class` 值: +## 动态组件 -- `form-control`,来自组件自身的模板 -- `date-picker-theme-dark`,来自父组件 +有的时候,在不同组件之间进行动态切换是非常有用的,比如在一个多标签的界面里: -对于多数特性来说,传递给组件的值会覆盖组件本身设定的值。即例如传递 `type="large"` 将会覆盖 `type="date"` 且有可能破坏该组件!所幸我们对待 `class` 和 `style` 特性会更聪明一些,这两个特性的值都会做合并 (merge) 操作,让最终生成的值为:`form-control date-picker-theme-dark`。 +{% raw %} +
      + + +
      + + +{% endraw %} -## 自定义事件 +上述内容可以通过 Vue 的 `` 元素加一个特殊的 `is` 特性来实现: -我们知道,父组件使用 prop 传递数据给子组件。但子组件怎么跟父组件通信呢?这个时候 Vue 的自定义事件系统就派得上用场了。 +```html + + +``` -### 使用 `v-on` 绑定自定义事件 +In the example above, `currentTabComponent` can contain either: +在上述示例中,`currentTabComponent` 可以包括 -每个 Vue 实例都实现了[事件接口](../api/#实例方法-事件),即: +- 已注册组件的名字,或 +- 一个组件的选项对象 -- 使用 `$on(eventName)` 监听事件 -- 使用 `$emit(eventName, optionalPayload)` 触发事件 +你可以在[这里](https://jsfiddle.net/chrisvfritz/o3nycadu/)查阅并体验完整的代码,或在[这个版本](https://jsfiddle.net/chrisvfritz/b2qj69o1/)了解绑定组件选项对象,而不是已注册组件名的示例。 -

      Vue 的事件系统与浏览器的 [EventTarget API](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) 有所不同。尽管它们运行起来类似,但是 `$on` 和 `$emit` __并不是__`addEventListener` 和 `dispatchEvent` 的别名。

      +到目前为止,关于动态组件你需要了解的大概就这些了,如果你阅读完本页内容并掌握了它的内容,我们会推荐你再回来把[动态和异步组件](components-dynamic-async.html)读完。 -另外,父组件可以在使用子组件的地方直接用 `v-on` 来监听子组件触发的事件。 +## 解析 DOM 模板时的注意事项 -

      不能用 `$on` 监听子组件释放的事件,而必须在模板里直接用 `v-on` 绑定,参见下面的例子。

      +有些 HTML 元素,诸如 `
        `、`
          `、`` 和 `` 和 `
          + +
          ``` -``` js -Vue.component('button-counter', { - template: '', - data: function () { - return { - counter: 0 - } - }, - methods: { - incrementCounter: function () { - this.counter += 1 - this.$emit('increment') - } - }, -}) +这个自定义组件 `` 会被作为无效的内容提升到外部,并导致最终渲染结果出错。幸好这个特殊的 `is` 特性给了我们一个变通的办法: -new Vue({ - el: '#counter-event-example', - data: { - total: 0 - }, - methods: { - incrementTotal: function () { - this.total += 1 - } - } -}) +``` html + + +
          ``` -{% raw %} -
          -

          {{ total }}

          - - -
          - -{% endraw %} - -在本例中,子组件已经和它外部完全解耦了。它所做的只是报告自己的内部事件,因为父组件可能会关心这些事件。请注意这一点很重要。 - -这里有一个如何使用载荷 (payload) 数据的示例: - -``` html -
          -

          {{ msg }}

          - -
          -``` - -``` js -Vue.component('button-message', { - template: `
          - - -
          `, - data: function () { - return { - message: 'test message' - } - }, - methods: { - handleSendMessage: function () { - this.$emit('message', { message: this.message }) - } - } -}) - -new Vue({ - el: '#message-event-example', - data: { - messages: [] - }, - methods: { - handleMessage: function (payload) { - this.messages.push(payload.message) - } - } -}) -``` - -{% raw %} -
          -

          {{ msg }}

          - -
          - -{% endraw %} - -第二个示例的重点在于子组件仍然是完全和外界解耦的。它做的事情全都是记录其自身的活动,活动记录是包括一份传入事件触发器的载荷数据在内的,只是为了展示父组件可以不关注的一个场景。 - -### 给组件绑定原生事件 - -有时候,你可能想在某个组件的根元素上监听一个原生事件。可以使用 `v-on` 的修饰符 `.native`。例如: - -``` html - -``` - -### `.sync` 修饰符 - -> 2.3.0+ - -在一些情况下,我们可能会需要对一个 prop 进行“双向绑定”。事实上,这正是 Vue 1.x 中的 `.sync` 修饰符所提供的功能。当一个子组件改变了一个带 `.sync` 的 prop 的值时,这个变化也会同步到父组件中所绑定的值。这很方便,但也会导致问题,因为它破坏了单向数据流。由于子组件改变 prop 的代码和普通的状态改动代码毫无区别,当光看子组件的代码时,你完全不知道它何时悄悄地改变了父组件的状态。这在 debug 复杂结构的应用时会带来很高的维护成本。 - -上面所说的正是我们在 2.0 中移除 `.sync` 的理由。但是在 2.0 发布之后的实际应用中,我们发现 `.sync` 还是有其适用之处,比如在开发可复用的组件库时。我们需要做的只是**让子组件改变父组件状态的代码更容易被区分**。 - -从 2.3.0 起我们重新引入了 `.sync` 修饰符,但是这次它只是作为一个编译时的语法糖存在。它会被扩展为一个自动更新父组件属性的 `v-on` 监听器。 - -如下代码 - -``` html - -``` - -会被扩展为: - -``` html - -``` - -当子组件需要更新 `foo` 的值时,它需要显式地触发一个更新事件: - -``` js -this.$emit('update:foo', newValue) -``` - -当使用一个对象一次性设置多个属性的时候,这个 `.sync` 修饰符也可以和 `v-bind` 一起使用: - -```html - -``` - -这个例子会为 `foo` 和 `bar` 同时添加用于更新的 `v-on` 监听器。 - -### 使用自定义事件的表单输入组件 - -自定义事件可以用来创建自定义的表单输入组件,使用 `v-model` 来进行数据双向绑定。要牢记: - -``` html - -``` - -这不过是以下示例的语法糖: - -``` html - -``` - -所以在组件中使用时,它相当于下面的简写: - -``` html - - -``` - -所以要让组件的 `v-model` 生效,它应该 (从 2.2.0 起是可配置的): - -- 接受一个 `value` prop -- 在有新的值时触发 `input` 事件并将新值作为参数 - -我们来看一个非常简单的货币输入的自定义控件: - -``` html - -``` - -``` js -Vue.component('currency-input', { - template: '\ - \ - $\ - \ - \ - ', - props: ['value'], - methods: { - // 不是直接更新值,而是使用此方法来对输入值进行格式化和位数限制 - updateValue: function (value) { - var formattedValue = value - // 删除两侧的空格符 - .trim() - // 保留 2 位小数 - .slice( - 0, - value.indexOf('.') === -1 - ? value.length - : value.indexOf('.') + 3 - ) - // 如果值尚不合规,则手动覆盖为合规的值 - if (formattedValue !== value) { - this.$refs.input.value = formattedValue - } - // 通过 input 事件带出数值 - this.$emit('input', Number(formattedValue)) - } - } -}) -``` - -{% raw %} -
          - -
          - -{% endraw %} - -当然,上面的例子还是比较初级的。比如,用户输入多个小数点或句号也是允许的,好恶心吧!因此我们需要一个复杂一些的例子,下面是一个更加完善的货币过滤器: - - - -### 自定义组件的 `v-model` - -> 2.2.0 新增 - -默认情况下,一个组件的 `v-model` 会使用 `value` prop 和 `input` 事件。但是诸如单选框、复选框之类的输入类型可能把 `value` 用作了别的目的。`model` 选项可以避免这样的冲突: - -``` js -Vue.component('my-checkbox', { - model: { - prop: 'checked', - event: 'change' - }, - props: { - checked: Boolean, - // 这样就允许拿 `value` 这个 prop 做其它事了 - value: String - }, - // ... -}) -``` - -``` html - -``` - -上述代码等价于: - -``` html - - -``` - -

          注意你仍然需要显式声明 `checked` 这个 prop。

          - -### 非父子组件的通信 - -有时候,非父子关系的两个组件之间也需要通信。在简单的场景下,可以使用一个空的 Vue 实例作为事件总线: - -``` js -var bus = new Vue() -``` - -``` js -// 触发组件 A 中的事件 -bus.$emit('id-selected', 1) -``` - -``` js -// 在组件 B 创建的钩子中监听事件 -bus.$on('id-selected', function (id) { - // ... -}) -``` - -在复杂的情况下,我们应该考虑使用专门的[状态管理模式](state-management.html)。 - -## 使用插槽分发内容 - -在使用组件时,我们常常要像这样组合它们: - -``` html - - - - -``` - -注意两点: - -1. `` 组件不知道它会收到什么内容。这是由使用 `` 的父组件决定的。 - -2. `` 组件很可能有它自己的模板。 - -为了让组件可以组合,我们需要一种方式来混合父组件的内容与子组件自己的模板。这个过程被称为**内容分发** (即 Angular 用户熟知的“transclusion”)。Vue.js 实现了一个内容分发 API,参照了当前 [Web Components 规范草案](https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md),使用特殊的 `` 元素作为原始内容的插槽。 - -### 编译作用域 - -在深入内容分发 API 之前,我们先明确内容在哪个作用域里编译。假定模板为: - -``` html - - {{ message }} - -``` - -`message` 应该绑定到父组件的数据,还是绑定到子组件的数据?答案是父组件。组件作用域简单地说是: - -> 父组件模板的内容在父组件作用域内编译;子组件模板的内容在子组件作用域内编译。 - -一个常见错误是试图在父组件模板内将一个指令绑定到子组件的属性/方法: - -``` html - - -``` - -假定 `someChildProperty` 是子组件的属性,上例不会如预期那样工作。父组件模板并不感知子组件的状态。 - -如果要绑定子组件作用域内的指令到一个组件的根节点,你应当在子组件自己的模板里做: - -``` js -Vue.component('child-component', { -  // 有效,因为是在正确的作用域内 - template: '
          Child
          ', - data: function () { - return { - someChildProperty: true - } - } -}) -``` - -类似地,被分发的内容会在父作用域内编译。 - -### 单个插槽 - -除非子组件模板包含至少一个 `` 插口,否则父组件的内容将会被**丢弃**。当子组件模板只有一个没有属性的插槽时,父组件传入的整个内容片段将插入到插槽所在的 DOM 位置,并替换掉插槽标签本身。 - -最初在 `` 标签中的任何内容都被视为**备用内容**。备用内容在子组件的作用域内编译,并且只有在宿主元素为空,且没有要插入的内容时才显示备用内容。 - -假定 `my-component` 组件有如下模板: - -``` html -
          -

          我是子组件的标题

          - - 只有在没有要分发的内容时才会显示。 - -
          -``` - -父组件模板: - -``` html -
          -

          我是父组件的标题

          - -

          这是一些初始内容

          -

          这是更多的初始内容

          -
          -
          -``` - -渲染结果: - -``` html -
          -

          我是父组件的标题

          -
          -

          我是子组件的标题

          -

          这是一些初始内容

          -

          这是更多的初始内容

          -
          -
          -``` - -### 具名插槽 - -`` 元素可以用一个特殊的特性 `name` 来进一步配置如何分发内容。多个插槽可以有不同的名字。具名插槽将匹配内容片段中有对应 `slot` 特性的元素。 - -仍然可以有一个匿名插槽,它是**默认插槽**,作为找不到匹配的内容片段的备用插槽。如果没有默认插槽,这些找不到匹配的内容片段将被抛弃。 - -例如,假定我们有一个 `app-layout` 组件,它的模板为: - -``` html -
          -
          - -
          -
          - -
          -
          - -
          -
          -``` - -父组件模板: - -``` html - -

          这里可能是一个页面标题

          - -

          主要内容的一个段落。

          -

          另一个主要段落。

          - -

          这里有一些联系信息

          -
          -``` - -渲染结果为: - -``` html -
          -
          -

          这里可能是一个页面标题

          -
          -
          -

          主要内容的一个段落。

          -

          另一个主要段落。

          -
          -
          -

          这里有一些联系信息

          -
          -
          -``` - -在设计组合使用的组件时,内容分发 API 是非常有用的机制。 - -### 作用域插槽 - -> 2.1.0 新增 - -作用域插槽是一种特殊类型的插槽,用作一个 (能被传递数据的) 可重用模板,来代替已经渲染好的元素。 - -在子组件中,只需将数据传递到插槽,就像你将 prop 传递给组件一样: - -``` html -
          - -
          -``` - -在父级中,具有特殊特性 `slot-scope` 的 `