【17】处理边界情况

文章目录

  • 1. 访问元素 & 组件
    • 1.1 访问根实例
    • 1.2 访问父级组件实例
    • 1.3 访问子组件实例或子元素
    • 1.4 依赖注入
  • 2. 程序化的事件侦听器
  • 3. 循环引用
    • 3.1 递归组件
    • 3.2 组件之间的循环引用
  • 4. 模板定义的替代品
    • 4.1 内联模板
    • 4.2 X-Template
  • 5. 控制更新
    • 5.1 强制更新
    • 5.2 通过 v-once 创建低开销的静态组件

这里记录的都是和处理边界情况有关的功能,即一些需要对 Vue 的规则做一些小调整的特殊情况。不过注意这些功能都是有劣势或危险的场景的。我们会在每个案例中注明,所以当你使用每个功能的时候请稍加留意。

1. 访问元素 & 组件

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

1.1 访问根实例

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

// Vue 根实例
new Vue({
	data: {
		foo: 1
	},
	computed: {
		bar: function () { /* ... */ }
	},
	methods: {
		baz: function () { /* ... */ }
	}
})

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

// 获取根组件的数据
this.$root.foo

// 写入根组件的数据
this.$root.foo = 2

// 访问根组件的计算属性
this.$root.bar

// 调用根组件的方法
this.$root.baz()

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

1.2 访问父级组件实例

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

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

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

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

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

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

<google-map>
	<goole-map-region v-bind:shape="cityBounaries">
		<google-map-markers v-bind:places="iceCreamShops">google-map-markers>
	goole-map-region>
google-map>

那么在 内部你可能发现自己需要一些类似这样的 hack:

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

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

1.3 访问子组件实例或子元素

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

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

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

this.$refs.usernameInput

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

<input ref="input">

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

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

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

this.$refs.usernameInput.focus()

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

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

1.4 依赖注入

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

<google-map>
	<goole-map-region v-bind:shape="cityBounaries">
		<google-map-markers v-bind:places="iceCreamShops">google-map-markers>
	goole-map-region>
google-map>

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

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

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

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

inject: ['getMap']

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

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

  • 祖先组件不需要知道哪些后代组件使用它提供的属性
  • 后代最贱不需要知道被注入的属性来自哪里

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

2. 程序化的事件侦听器

现在,你已经知道了 $emit 的用法,它可以被 v-on 侦听,但是 Vue 实例同时在其事件接口中提供了其它的方法。我们可以:

  • 通过 $on(eventName, eventHandler) 侦听一个事件
  • 通过 $once(eventName, eventHandler) 一次性侦听一个事件
  • 通过 $off(eventName, eventHandler) 停止侦听一个事件

你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的。它们也可以用于代码组织工具。例如,你可能经常看到这种集成一个第三方库的模式:

// 一次性将这个日期选择器附加到一个输入框上]
// 它会挂在到 DOM 上
mounted: function () {
	// Pikaday 是一个第三方日期选择的库
	this.picker = new Pickaday({
		field: this.$refs.input,
		format: 'YYYY-MM-DD'
	})
},
// 在组件被销毁之前,也销毁这个日期选择器
beforeDestory: function () {
	this.picker.destory()
}

这里有两个潜在的问题:

  • 它需要在这个组件实例中保存这个 picker,如果可以的话最好只有生命周期钩子可以访问到它。这并不算严重的问题,但是它可以被视为杂物。
  • 我们的建立代码独立于我们的清理代码,这使得我们比较难于程序化地清理我们建立的所有东西。

你应该通过一个程序化的侦听器解决两个问题:

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

使用这个策略,我甚至可以让多个输入框元素同时使用不同的 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:beforeDestory', function () {
			picker.destory()
		})
	}
}

查阅这个 fiddle 可以了解到完整的代码。注意,即便如此,如果你发现自己不得不在单个组件里做很多简历和清理的工作,最好的方式通常还是创建更多的模块化组件。在这个例子中,我们推荐创建一个可复用的 组件。

想了解更多程序化侦听器的内容,请查阅实例方法/事件相关的API。

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

3. 循环引用

3.1 递归组件

组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name 选项来做这件事:

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

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

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

稍有不慎,递归钻进就可能导致无限循环:

name: 'stack-overflow',
template: '
'

类似上述的组件将会导致 “max stack size exceeded” 错误,所以请确保递归调用是条件性的(例如使用一个最终得到的 falsev-if)。

3.2 组件之间的循环引用

假设你需要构建一个文件目录树,像访达或资源管理器那样的。你可能有一个 组件,模板是这样的:

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

还有一个 组件,模板是这样的:

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

当你解释这里发生了什么,我们先把两个组件称为 A 和 B。模块系统发现它需要 A。但是首先 A 依赖 B,但是 B 又依赖 A,但是 A 又依赖 B。如此往复。这变成了一个循环,不知道如何不经过其中一个组件,而完全解析出另一个组件。为了解决这个问题,我们需要给模块系统一个点,在那里 “A反正是需要 B的,但是我们不需要先解析 B”。

在我们的例子中,把 组件设为了那个点。我们知道那个产生悖论的子组件是 组件,所以我们会等到生命周期钩子 beforeCreate 时区注册它:

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

这样问题就解决了。

4. 模板定义的替代品

4.1 内联模板

inline-template 这个特殊的特性出现在一个子组件上时,这个组件将会使用里面的内容作为模板,而不是将其作为被分发的内容。这使得模板的撰写工作更加灵活。

<my-component>
	<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 文件里的一个