🎊 处理边缘情况

处理边缘情况

处理边缘情况

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

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

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

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

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

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

// Get root datathis.$root.foo// Set root datathis.$root.foo = 2// Access root computed propertiesthis.$root.bar// Call root methodsthis.$root.baz()

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

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

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

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

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

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

然后在 中,您可能会发现自己需要使用类似这样的 hack

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

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

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

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

this.$refs.usernameInput

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

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

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

从而允许父组件使用以下方法聚焦 中的输入

this.$refs.usernameInput.focus()

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

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

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

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

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

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

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

inject: ['getMap']

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

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

祖先组件不需要知道哪些后代使用它提供的属性

后代组件不需要知道注入的属性来自哪里

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

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

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

使用 $on(eventName, eventHandler) 监听事件

使用 $once(eventName, eventHandler) 仅监听一次事件

使用 $off(eventName, eventHandler) 停止监听事件

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

// 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()}

这有两个潜在问题

它需要将 picker 保存到组件实例中,而实际上可能只有生命周期钩子需要访问它。这并不糟糕,但可以被认为是杂乱无章。

我们的设置代码与我们的清理代码分开,这使得以编程方式清理我们设置的任何内容变得更加困难。

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

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() }) }}

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

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

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

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

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

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

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

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

name: 'stack-overflow',template: '

'

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

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

{{ folder.name }}

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

  • {{ child.name }}

仔细观察,您会发现这些组件实际上将是彼此的子代和祖先在渲染树中 - 一个悖论!当使用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特殊属性时,组件将使用其内部内容作为其模板,而不是将其视为分布式内容。这允许更灵活的模板创作。

These are compiled as the component's own template.

Not parent's transclusion content.

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

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