Close
升级到 Vue 3 | Vue 2 EOL

处理边缘情况

本页假设您已阅读 组件基础。如果您不熟悉组件,请先阅读该部分。

本页上的所有功能都记录了边缘情况的处理,即有时需要稍微弯曲 Vue 规则的异常情况。但是请注意,它们都有缺点或可能存在危险的情况。这些缺点在每种情况下都有说明,因此在决定使用每个功能时请牢记它们。

元素和组件访问

在大多数情况下,最好避免访问其他组件实例或手动操作 DOM 元素。但是,在某些情况下,这样做可能是合适的。

访问根实例

new Vue 实例的每个子组件中,可以使用 $root 属性访问此根实例。例如,在这个根实例中

// The root Vue instance
new Vue({
data: {
foo: 1
},
computed: {
bar: function () { /* ... */ }
},
methods: {
baz: function () { /* ... */ }
}
})

现在所有子组件都将能够访问此实例并将其用作全局存储

// Get root data
this.$root.foo

// Set root data
this.$root.foo = 2

// Access root computed properties
this.$root.bar

// Call root methods
this.$root.baz()

这对于演示或只有少量组件的非常小的应用程序来说很方便。但是,这种模式不适合中型或大型应用程序,因此我们强烈建议在大多数情况下使用 Vuex 来管理状态。

访问父组件实例

$root 类似,$parent 属性可用于从子组件访问父实例。这可能很诱人,因为它可以作为使用 prop 传递数据的懒惰替代方案。

在大多数情况下,访问父组件会使您的应用程序更难调试和理解,尤其是在您修改父组件中的数据时。当您稍后查看该组件时,将很难弄清楚该修改来自哪里。

但是,在某些情况下,特别是共享组件库,这可能是合适的。例如,在与 JavaScript API 交互而不是渲染 HTML 的抽象组件中,例如这些假设的 Google 地图组件

<google-map>
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>

<google-map> 组件可能会定义一个 map 属性,所有子组件都需要访问它。在这种情况下,<google-map-markers> 可能希望使用类似 this.$parent.getMap 的方法访问该地图,以便向其添加一组标记。您可以查看 此处的实际示例

但是请记住,使用这种模式构建的组件本质上仍然很脆弱。例如,假设我们添加了一个新的 <google-map-region> 组件,当 <google-map-markers> 出现在该组件中时,它应该只渲染落在该区域内的标记

<google-map>
<google-map-region v-bind:shape="cityBoundaries">
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map-region>
</google-map>

然后在 <google-map-markers> 中,您可能会发现自己需要使用类似这样的 hack

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

这已经变得难以控制。这就是为什么为了向任意深度的后代组件提供上下文信息,我们建议使用 依赖注入

访问子组件实例和子元素

尽管存在 props 和事件,但有时您可能仍然需要在 JavaScript 中直接访问子组件。要实现这一点,您可以使用 ref 属性为子组件分配一个引用 ID。例如

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

现在,在您定义了此 ref 的组件中,您可以使用

this.$refs.usernameInput

访问 <base-input> 实例。这在您想要从父组件以编程方式聚焦此输入时可能很有用。在这种情况下,<base-input> 组件可能会类似地使用 ref 来提供对其中特定元素的访问权限,例如

<input ref="input">

甚至定义供父组件使用的方法

methods: {
// Used to focus the input from the parent
focus: function () {
this.$refs.input.focus()
}
}

从而允许父组件使用以下方法聚焦 <base-input> 中的输入

this.$refs.usernameInput.focus()

refv-for 一起使用时,您获得的 ref 将是一个数组,其中包含反映数据源的子组件。

$refs 仅在组件渲染后填充,并且它们不是响应式的。它仅作为直接子组件操作的逃生舱 - 您应该避免从模板或计算属性中访问 $refs

依赖注入

之前,当我们描述 访问父组件实例 时,我们展示了类似这样的示例

<google-map>
<google-map-region v-bind:shape="cityBoundaries">
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map-region>
</google-map>

在此组件中,<google-map> 的所有后代都需要访问 getMap 方法,以便知道要与哪个地图交互。不幸的是,使用 $parent 属性不能很好地扩展到更深层的嵌套组件。这就是依赖注入在使用两个新的实例选项时派上用场的地方:provideinject

provide 选项允许我们指定要提供给后代组件的数据/方法。在这种情况下,即 <google-map> 中的 getMap 方法

provide: function () {
return {
getMap: this.getMap
}
}

然后,在任何后代中,我们可以使用 inject 选项接收我们想要添加到该实例的特定属性

inject: ['getMap']

您可以查看 此处的完整示例。与使用 $parent 相比,它的优势在于我们可以在任何后代组件中访问 getMap,而无需公开 <google-map> 的整个实例。这使我们能够更安全地继续开发该组件,而不必担心我们可能会更改/删除后代组件依赖的某些内容。这些组件之间的接口仍然清晰定义,就像 props 一样。

实际上,您可以将依赖注入视为一种“远程 props”,但

但是,依赖注入也有缺点。它将应用程序中的组件耦合到它们当前的组织方式,从而使重构更加困难。提供的属性也不是响应式的。这是有意为之,因为使用它们来创建中央数据存储的扩展性与 使用 $root 用于相同目的的扩展性一样差。如果您想要共享的属性特定于您的应用程序,而不是通用的,或者如果您想在祖先中更新提供的任何数据,那么这可能表明您可能需要一个真正的状态管理解决方案,例如 Vuex

API 文档 中了解有关依赖注入的更多信息。

编程事件监听器

到目前为止,您已经看到了 $emit 的用法,并使用 v-on 监听它,但 Vue 实例在其事件接口中也提供了其他方法。我们可以

您通常不需要使用这些方法,但它们可用于您需要手动监听组件实例上的事件的情况。它们也可以用作代码组织工具。例如,您可能会经常看到这种用于集成第三方库的模式

// Attach the datepicker to an input once
// it's mounted to the DOM.
mounted: function () {
// Pikaday is a 3rd-party datepicker library
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
// Right before the component is destroyed,
// also destroy the datepicker.
beforeDestroy: function () {
this.picker.destroy()
}

这有两个潜在问题

您可以使用编程监听器来解决这两个问题

mounted: function () {
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}

使用这种策略,我们甚至可以使用 Pikaday 与多个输入元素一起使用,每个新实例都会自动清理自身

mounted: function () {
this.attachDatepicker('startDateInput')
this.attachDatepicker('endDateInput')
},
methods: {
attachDatepicker: function (refName) {
var picker = new Pikaday({
field: this.$refs[refName],
format: 'YYYY-MM-DD'
})

this.$once('hook:beforeDestroy', function () {
picker.destroy()
})
}
}

查看 此示例以获取完整代码。但是请注意,如果您发现自己需要在一个组件中进行大量设置和清理,那么最好的解决方案通常是创建更多模块化的组件。在这种情况下,我们建议创建一个可重用的 <input-datepicker> 组件。

要了解有关编程监听器的更多信息,请查看 事件实例方法 的 API。

请注意,Vue 的事件系统不同于浏览器的 EventTarget API。尽管它们的工作方式相似,但 $emit$on$off 不是 dispatchEventaddEventListenerremoveEventListener 的别名。

循环引用

递归组件

组件可以在其自身模板中递归调用自身。但是,它们只能使用name选项来执行此操作。

name: 'unique-name-of-my-component'

当您使用Vue.component全局注册组件时,全局 ID 会自动设置为组件的name选项。

Vue.component('unique-name-of-my-component', {
// ...
})

如果您不小心,递归组件也会导致无限循环。

name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'

像上面这样的组件会导致“最大堆栈大小超出”错误,因此请确保递归调用是有条件的(即使用一个最终将为falsev-if)。

组件之间的循环引用

假设您正在构建一个文件目录树,就像在 Finder 或文件资源管理器中一样。您可能有一个带有此模板的tree-folder组件

<p>
<span>{{ folder.name }}</span>
<tree-folder-contents :children="folder.children"/>
</p>

然后是一个带有此模板的tree-folder-contents组件

<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>

仔细观察,您会发现这些组件实际上将是彼此的子代祖先在渲染树中 - 一个悖论!当使用Vue.component全局注册组件时,此悖论会自动为您解决。如果是您,您可以停止阅读。

但是,如果您使用模块系统(例如通过 Webpack 或 Browserify)来要求/导入组件,您将收到错误

Failed to mount component: template or render function not defined.

为了解释正在发生的事情,让我们将我们的组件称为 A 和 B。模块系统看到它需要 A,但首先 A 需要 B,但 B 需要 A,但 A 需要 B,等等。它陷入了循环中,不知道如何在不首先解析另一个组件的情况下完全解析任何一个组件。为了解决这个问题,我们需要为模块系统提供一个点,它可以在该点说,“A 最终需要 B,但没有必要首先解析 B。”

在我们的例子中,让我们将该点设为tree-folder组件。我们知道创建悖论的子组件是tree-folder-contents组件,因此我们将等到beforeCreate生命周期钩子来注册它

beforeCreate: function () {
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者,您可以在本地注册组件时使用 Webpack 的异步import

components: {
TreeFolderContents: () => import('./tree-folder-contents.vue')
}

问题解决!

备用模板定义

内联模板

当子组件上存在inline-template特殊属性时,组件将使用其内部内容作为其模板,而不是将其视为分布式内容。这允许更灵活的模板创作。

<my-component inline-template>
<div>
<p>These are compiled as the component's own template.</p>
<p>Not parent's transclusion content.</p>
</div>
</my-component>

您的内联模板需要定义在 Vue 附加到的 DOM 元素内部。

但是,inline-template 使您的模板范围更难推理。作为最佳实践,建议使用template选项在组件内部定义模板,或在.vue文件中的<template>元素中定义模板。

X-模板

另一种定义模板的方法是在类型为text/x-template的脚本元素内部,然后通过 ID 引用模板。例如

<script type="text/x-template" id="hello-world-template">
<p>Hello hello hello</p>
</script>
Vue.component('hello-world', {
template: '#hello-world-template'
})

您的 x-模板需要定义在 Vue 附加到的 DOM 元素外部。

这些对于具有大型模板的演示或极小的应用程序很有用,但在其他情况下应避免,因为它们将模板与组件定义的其余部分分开。

控制更新

由于 Vue 的响应式系统,它始终知道何时更新(如果您正确使用它)。但是,在某些情况下,您可能希望强制更新,即使没有响应式数据发生更改。然后,在其他情况下,您可能希望阻止不必要的更新。

强制更新

如果您发现自己需要在 Vue 中强制更新,在 99.99% 的情况下,您在某个地方犯了一个错误。

您可能没有考虑到与数组相关的更改检测注意事项或与对象相关的更改检测注意事项,或者您可能依赖于 Vue 的响应式系统未跟踪的状态,例如使用data

但是,如果您排除了上述情况,并且发现自己处于这种极少数情况下需要手动强制更新的情况下,您可以使用$forceUpdate来执行此操作。

使用v-once的廉价静态组件

在 Vue 中渲染纯 HTML 元素非常快,但有时您可能有一个包含大量静态内容的组件。在这些情况下,您可以通过将v-once指令添加到根元素来确保它只被评估一次,然后被缓存,如下所示

Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})

再次,尽量不要过度使用这种模式。虽然在您必须渲染大量静态内容的那些罕见情况下很方便,但除非您确实注意到渲染缓慢,否则它根本没有必要 - 此外,它可能会在以后造成很多混乱。例如,想象一下另一个不熟悉v-once或只是在模板中遗漏它的开发人员。他们可能会花费数小时试图弄清楚为什么模板没有正确更新。