0%

vue处理边界情况

访问元素&组件

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

访问根实例

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

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

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

1
2
3
4
5
6
7
8
9
10
11
// 获取根组件的数据
this.$root.foo

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

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

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

访问父级组件实例

$root 类似, $parent 属性可以用来从一个子组件访问父组件的实例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>JS Bin</title>
</head>
<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.0.3/vue.js"></script>
<div id="app">
{{ msg }}
<hr />
<my-com />
</div>
<script>
Vue.component('my-com', {
template: '<h1>M</h1>',
mounted() {
// 访问父级
console.log(this.$parent.msg) // G
this.$parent.foo() // foo
}
})
var vm = new Vue({
el: '#app',
data() {
return {
msg: 'G'
}
},
methods: {
foo() {
console.log('foo')
}
}
})
</script>
</body>
</html>

访问子组件实例或子元素

有时候, 需要直接访问子组件, 可以通过 ref 这个 attribute 为子组件赋予一个 ID 引用

1
2
<!-- 子组件 -->
<my-component ref="my-com"></my-component>

可以在定义了这个 ref 的组件里使用

1
this.$refs['my-com'].msg

获取指定 DOM 元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<div class="hello">
<hr />
<button @click="click">click</button>
<hr />
<input ref="input" />
</div>
</template>

<script>
export default {
methods: {
click() {
this.$refs['input'].focus()
}
}
}
</script>

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

依赖注入

我们可以虽然可以通过 $parent$refs、属性、事件等方式获取获取父组件或子组件的实例或属性, 但是无法扩展到更深层次级的嵌套组件上.
Vue 给我们提供了两个新的实例选项 provideinject, 选项允许我们指定我们想要提供给后代组件的数据/方法

1
2
3
4
5
6
7
8
9
10
11
// 根组件
provide: function() {
return {
getMap: this.getMap
}
}

// ... 第一层嵌套组件

// 第二层嵌套组件
inject: ["getMap"]

程序化的事件侦听器

除了 $emit可以被v-on侦听, Vue 同时还在事件接口中提供了其他方法:

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

循环引用

递归组件

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

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

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

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

稍有不慎, 递归组件就可能导致无限循环

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

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

组件之间的循环引用

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

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

还有一个<tree-folder-contents>组件

1
2
3
4
5
6
<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,你会遇到一个错误:

1
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 时去注册它:

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

或者,在本地注册组件的时候,你可以使用 webpack 的异步 import:

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

模板定义的替代品

内联模板

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

1
2
3
4
5
6
<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-Template

另一个定义模板的方式是在一个 <script> 元素中, 并为其带上 ext/x-template 的类型, 然后通过一个 id 将模板引用过去

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

x-template 需要定义在 Vue 所属的 DOM 元素外。
这些可以用于模板特别大的 demo 或极小型的应用,但是其它情况下请避免使用,因为这会将模板和该组件的其它定义分离开。

控制更新

感谢 Vue 的响应式系统,它始终知道何时进行更新 (如果你用对了的话)。不过还是有一些边界情况,你想要强制更新,尽管表面上看响应式的数据没有发生改变。也有一些情况是你想阻止不必要的更新。

强制更新

你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。

然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate 来做这件事。

通过 v-once 创建低开销的静态组件

渲染普通的 HTML 元素在 Vue 中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容。在这种情况下,你可以在根元素上添加 v-once attribute 以确保这些内容只计算一次然后缓存起来,就像这样:

1
2
3
4
5
6
7
8
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})
请作者喝杯咖啡