1.背景
一些需要对 Vue 的规则做一些小调整的特殊情况,这些功能都是有劣势或危险的场景的
2.访问元素&组件
在绝大多数情况下,最好不要触达另一个组件实例内部或手动操作 DOM 元素。不过在一些情况下做这些事情是合适的;
(1) 访问根实例
- 在每个 new Vue 实例的子组件中,其根实例可以通过
$root
属性进行访问; - 所有的子组件都可以将这个实例作为一个全局
store
来访问或使用; - 【注意】对于 demo 或非常小型的有少量组件的应用来说这是很方便的。不过这个模式扩展到中大型应用来说就不然了。因此在绝大多数情况下,推荐使用
Vuex
来管理应用的状态;
// Vue 根实例
new Vue({
data: {
foo: 1
},
computed: {
bar: function () { /* ... */ }
},
methods: {
baz: function () { /* ... */ }
}
})
//==== 子组件访问根组件 ====
// 获取根组件的数据
this.$root.foo
// 写入根组件的数据
this.$root.foo = 2
// 访问根组件的计算属性
this.$root.bar
// 调用根组件的方法
this.$root.baz()
(2) 访问父级组件实例
- 和
$root
类似,$parent
属性 可以用来从一个子组件访问父组件的实例; $parent
提供了一种机会,可以在后期随时触达父级组件,以替代将数据以 属性 的方式传入子组件的方式。- 【注意】访问父组件会使得应用更难调试和理解,尤其是当变更了父级组件的数据的时候。当稍后回看那个组件的时候,很难找出那个变更是从哪里发起的;
- 共享组件库时,如在和 JavaScript API 进行交互而不渲染 HTML 的抽象组件内,
<google-map>
组件可以定义一个map
property,所有的子组件都需要访问它;
<google-map>
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-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
- 很快它就会失控(组件嵌套加深时),所以当需要向更深层级的组件提供上下文信息时推荐依赖注入;
(3) 访问子组件实例或子元素
- 需要在javascript里直接访问一个子组件,可以通过
ref
这个属性为子组件赋予一个ID引用
<!-- 定义ref -->
<base-input ref="usernameInput"></base-input>
// 使用ref访问<base-input>实例
this.$refs.usernameInput
- 在
<base-input>
中,可以使用一个类似的ref
提供对内部这个指定元素的访问,并在父组件定义focus()
方法:
<!-- base-input组件 -->
<input ref="input">
// 父组件中定义focus方法
methods: {
// 用来从父级组件聚焦输入框
focus: function () {
this.$refs.input.focus()
}
}
// 在父组件聚焦<base-input>里的输入框:
this.$refs.usernameInput.focus()
- 【注意1】当
ref
和v-for
一起使用的时候,得到的ref
将会是一个包含了对应数据源的这些子组件的数组
。 - 【注意2】
$refs
只会在组件渲染完成之后生效,并且它们不是响应式的。这仅作为一个用于直接操作子组件的“逃生舱”,应该避免在模板或计算属性中访问 $refs。
(4).依赖注入
1) 场景
- 组件嵌套较深,使用
$parent
无法很好的访问父组件数据和方法; - 如下所示,所有
<google-map>
的后代都需要访问一个getMap
方法,以便知道要跟那个地图进行交互,但是使用$parent
property无法很好的扩展到更深层及的嵌套组件上;
<google-map>
<google-map-region v-bind:shape="cityBoundaries">
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map-region>
</google-map>
2) provide
provide
选项允许指定想要提供给后代组件的数据/方法;- 在上述案例中,
<google-map>
内部的getMap
方法即为想提供给后代的方法
provide: function () {
return {
getMap: this.getMap
}
}
3) inject
- 使用
inject
选项来接收指定的想要添加在这个实例上的propert:
// 在子组件中添加
inject:['getMap']
4) 总结
- 相比
$parent
来说,provide
和inject
可以实现在任意后代组件中访问getMap
,而不需要暴露整个<google-map>
实例
3.程序化的事件侦听器
(1) 场景
当需要在一个组件实例上手动侦听事件时
(2)事件侦听器
$on(eventName,eventHandler)
侦听一个事件;$once(eventName,eventHandler)
一次性侦听一个事件;$off(eventName,eventHanlder)
停止侦听一个事件;- Vue 的事件系统不同于浏览器的 EventTarget API。尽管它们工作起来是相似的,但是
$emit
、$on
, 和$off
并不是dispatchEvent
、addEventListener
和removeEventListener
的别名; - 用于代码组织工具,举例:在某个第三方库模式下:
// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted: function () {
// Pikaday 是一个第三方日期选择器的库
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
// 在组件被销毁之前,也销毁这个日期选择器。
beforeDestroy: function () {
this.picker.destroy()
}
- 上述代码存在两个问题:
1) 它需要在这个组件实例中保存这个picker
,如果可以的话最好只有生命周期钩子可以访问到它;
2) 建立代码独立于清理代码,这使得难于程序化地清理建立的所有东西; - 通过程序化的侦听器解决:
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()
})
}
}
4.循环引用
- 组件是可以在它们自己的模板中调用自身的。只能通过 name 选项来做这件事;
name: 'unique-name-of-my-component'
- 当使用
Vue.component
全局注册一个组件时,这个全局的 ID 会自动设置为该组件的 name 选项:
Vue.component('unique-name-of-my-component', {
// ...
})
- 组件可能出现无限循环,导致“max stack size exceeded”错误,所以一定要确保递归调用是条件性的(如使用一个最终会得到
false
的v-if
)
name: 'stack-overflow',
template: '<div><stack-overflow></stack-overflow></div>'
(1) 组件之间的循环引用
1) 场景
构建一个文件目录树,像资源管理器一样,如<tree-folder>
组件,内部渲染<tree-folder-contents>
组件,但是<tree-folder-contents>
组件内部又依赖于<tree-folder>
组件,如此产生循环引用;
<!-- 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>
2) 解决方案
- 需要给模块系统一个点,在那里
<tree-folder>
是需要<tree-folder-contents>
的,但是我们不需要先解析<tree-folder-contents>
; - 在例子中,
<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')
}
5.模板定义的替代品
(1) 内联模板
- 当
inline-template
这个特殊的 attribute 出现在一个子组件上时,这个组件将会使用其里面的内容作为模板,而不是将其作为被分发的内容;
<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>
元素来定义模板;
(2) X-Template
- 另一个定义模板的方式是在一个
<script>
元素中,并为其带上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-template
需要定义在 Vue 所属的 DOM 元素外;
6.控制更新
(1) 强制更新
- 通过
$forceUpdate
实现
(2) 通过v-once
创建低开销的静态组件
1) 场景
当某个组件包含大量静态内容,希望这些内容只计算一次然后缓存起来
2) 使用
- 在根元素上添加
v-once
可以确保这些内容只计算一次然后缓存起来
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
`
})
- 【注意】除非在页面需要的大量渲染静态内容并且发现渲染变慢的情况下,否则应该尽量避免使用
v-once
,否则可能会因为开发人员漏看v-once
导致无法发现模板未正确更新的原因。