访问元素 & 组件

在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过也确实在一些情况下做这些事情是合适的。

访问根实例

在每个 new Vue 实例的子组件中,其根实例可以通过 $root 属性进行访问。例如,在这个根实例中:

  1. // Vue 根实例
  2. new Vue({
  3. data: {
  4. foo: 1
  5. },
  6. computed: {
  7. bar: function () { /* ... */ }
  8. },
  9. methods: {
  10. baz: function () { /* ... */ }
  11. }
  12. })

所有的子组件都可以将这个实例作为一个全局 store 来访问或使用。

  1. // 获取根组件的数据
  2. this.$root.foo
  3. // 写入根组件的数据
  4. this.$root.foo = 2
  5. // 访问根组件的计算属性
  6. this.$root.bar
  7. // 调用根组件的方法
  8. this.$root.baz()

对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,我们强烈推荐使用 Vuex 来管理应用的状态。

访问父级组件实例

$root 类似,$parent 属性可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 prop 的方式传入子组件的方式。

在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候。当我们稍后回看那个组件的时候,很难找出那个变更是从哪里发起的。

另外在一些可能适当的时候,你需要特别地共享一些组件库。举个例子,在和 JavaScript API 进行交互而不渲染 HTML 的抽象组件内,诸如这些假设性的 Google 地图组件一样:

  1. <google-map>
  2. <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  3. </google-map>

这个 <google-map> 组件可以定义一个 map 属性,所有的子组件都需要访问它。在这种情况下 <google-map-markers> 可能想要通过类似 this.$parent.getMap 的方式访问那个地图,以便为其添加一组标记。你可以在这里查阅这种模式。

请留意,尽管如此,通过这种模式构建出来的那个组件的内部仍然是容易出现问题的。比如,设想一下我们添加一个新的 <google-map-region> 组件,当 <google-map-markers> 在其内部出现的时候,只会渲染那个区域内的标记:

  1. <google-map>
  2. <google-map-region v-bind:shape="cityBoundaries">
  3. <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  4. </google-map-region>
  5. </google-map>

那么在 <google-map-markers> 内部你可能发现自己需要一些类似这样的 hack:

  1. var map = this.$parent.map || this.$parent.$parent.map

很快它就会失控。这也是我们针对需要向任意更深层级的组件提供上下文信息时推荐依赖注入的原因。

访问子组件实例或子元素

尽管存在 prop 和事件,有的时候你仍可能需要在 JavaScript 里直接访问一个子组件。为了达到这个目的,你可以通过 ref 特性为这个子组件赋予一个 ID 引用。例如:

  1. <base-input ref="usernameInput"></base-input>

现在在你已经定义了这个 ref 的组件里,你可以使用:

  1. this.$refs.usernameInput

来访问这个 <base-input> 实例,以便不时之需。比如程序化地从一个父级组件聚焦这个输入框。在刚才那个例子中,该 <base-input> 组件也可以使用一个类似的 ref 提供对内部这个指定元素的访问,例如:

  1. <input ref="input">

甚至可以通过其父级组件定义方法:

  1. methods: {
  2. // 用来从父级组件聚焦输入框
  3. focus: function () {
  4. this.$refs.input.focus()
  5. }
  6. }

这样就允许父级组件通过下面的代码聚焦 <base-input> 里的输入框:

  1. this.$refs.usernameInput.focus()

refv-for 一起使用的时候,你得到的引用将会是一个包含了对应数据源的这些子组件的数组。

$refs 只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”——你应该避免在模板或计算属性中访问 $refs

依赖注入

在此之前,在我们描述访问父级组件实例的时候,展示过一个类似这样的例子:

  1. <google-map>
  2. <google-map-region v-bind:shape="cityBoundaries">
  3. <google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
  4. </google-map-region>
  5. </google-map>

在这个组件里,所有 <google-map> 的后代都需要访问一个 getMap 方法,以便知道要跟哪个地图进行交互。不幸的是,使用 $parent 属性无法很好的扩展到更深层级的嵌套组件上。这也是依赖注入的用武之地,它用到了两个新的实例选项:provideinject

provide 选项允许我们指定我们想要提供给后代组件的数据/方法。在这个例子中,就是 <google-map> 内部的 getMap 方法:

  1. provide: function () {
  2. return {
  3. getMap: this.getMap
  4. }
  5. }

然后在任何后代组件里,我们都可以使用 inject 选项来接收指定的我们想要添加在这个实例上的属性:

  1. inject: ['getMap']

你可以在这里看到完整的示例。相比 $parent 来说,这个用法可以让我们在任意后代组件中访问 getMap,而不需要暴露整个 <google-map> 实例。这允许我们更好的持续研发该组件,而不需要担心我们可能会改变/移除一些子组件依赖的东西。同时这些组件之间的接口是始终明确定义的,就和 props 一样。

实际上,你可以把依赖注入看作一部分“大范围有效的 prop”,除了:

  • 祖先组件不需要知道哪些后代组件使用它提供的属性
  • 后代组件不需要知道被注入的属性来自哪里 然而,依赖注入还是有负面影响的。它将你应用程序中的组件与它们当前的组织方式耦合起来,使重构变得更加困难。同时所提供的属性是非响应式的。这是出于设计的考虑,因为使用它们来创建一个中心化规模化的数据跟使用 $root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的,或者如果你想在祖先组件中更新所提供的数据,那么这意味着你可能需要换用一个像 Vuex 这样真正的状态管理方案了。

你可以在 API 参考文档学习更多关于依赖注入的知识。