这里记录的都是和处理边界情况有关的功能,即一些需要对vue的规则做一些小调整的特殊情况。不过注意这些功能都是有劣势或危险的场景的,我们会在每个案例中注明。所以当你使用每个功能的时候请稍加留意
访问元素和组件
在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作dom元素。不过也确实在一些情况下做这些事情是合适的
访问根实例
在每个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来管理应用的状态
访问父级组件实例
和$root类似,$parent属性可以用来从一个子组件访问父组件的实例。它提供了一种机会,可以在后期随时触达父级组件,以替代将数据已prop的方式传入子组件的方式。
在绝大多数情况下,触达父级组件会使得你的应用更难调试和理解,尤其是当你变更了父级组件的数据的时候,当我们稍后会看那个组建的时候,很难找出那个变更是从哪里发起的
另外在一些可能适当的时候,你炫耀特别的共享一些组件库。
在和javascriptapi进行交互而不渲染html的抽象组件内,诸如这些假设性的goole地图组建一样。
<google-map>
<google-map-markers v-bind:places="iceCreamShops"></google-map-markers>
</google-map>
这个<goole-map>组件可以定义一个map属性,所有的组组件都需要访问他。在这种情况下,<google-map-markers>可能想要通过类似this.$parent.getMap的方式访问那个地图,以便为期添加一组标记。
通过这种模式构建出来的那个组件的内部仍然是容易出现问题的,比如,设想一下我们添加一个新的<google-map-region>组件,当<google-map-makers>在其内部出现的时候,只会渲染那个区域内的标记<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
很快他就会失控,这也是我们针对需要向任意更深层及的组件提供上下文信息时推荐依赖注入的原因。
访问子组件实例或子元素
尽管存在prop和事件,有的时候你仍然可能需要在javascript里直接访问一个子组件,为了达到这个目的,你可以通过ref特性为这个子组件赋予一个id引用。
<base-input ref="username"></>
现在你已经定义了这个ref的组件里
可以使用
this.$refs.username
来访问这个<base-input>实例,以便不时之需,比如程序化的从一个父级组件聚焦这个输入框。在刚才的那个例子中,该<base-input>组件可以使用一个类似的ref提供对内部这个指定元素的访问。
<input ref="input">
甚至可以通过其父级组件定义方法
methods:{
focus:function(){
this.$refs.input.focus()
}}
这样就允许父级组件通过下面的代码聚焦<base-input>的输入框
this.$refs.username.focus()
当ref和v-for一起使用的时候,你得到的引用将会是一个包含了对应数据源的这些子组件的数组。
$ref只会在组件渲染完成之后生效,并且他们不是响应式的,这只意味着一个直接的子组件封装的逃生舱——你应该避免在模板或计算属性中访问$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>
在这个组件里,所有<goole-map>的后代都需要访问一个getMap方法,以便知道要跟那个地图进行交互,不幸的是,使用$parent属性无法很好的扩展到更深层及的嵌套组件上。这也是依赖注入的用武之地,他用到了两个新的实例选项:provide(提供)和inject(注入)
provide选项允许我们制定我们想要提供给后台组件的数据方法。在这个例子中,就是 <google-map> 内部的 getMap 方法:
provide: function () {
return {
getMap: this.getMap
}
}
然后再任何后代组件里,我们都可以使用inject选项来接受制定的我们想要添加在这个实例上的属性
inject:['getMap']
你可以在这里看到完整的实例,相比$parent来说,这个用法可以让我们在任意后代组件中访问getmap,而不需要暴露整个<goole-map>实例,这允许我们更好地持续研发该组件,而不需要担心我们可能会改变移除一些子组件依赖的东西。同时这些组件之间的接口始终明确定义。就和props一样。
实际上,可以把依赖注入看做一部分大范围有效地prop,除了
祖先组件不需要知道哪些后代组件使用它提供的属性
后代组件不需要知道被注入的属性来自哪里
然而,依赖注入还有负面影响,它将你的应用以目前的组件组织方式耦合了起来,是重构变得困难,同时所提供的属性是非响应式的,这是出于设计的考虑,因为使用他们来创建一个中心化规模化的数据跟使用$root做这件事都是不够好的。如果你想要共享的这个属性是你的应用特有的,而不是通用化的。或者如果你想在组件中更新所提供的数据,那么这意味着你可能需要换用一个像vuex这样真正的状态管理方案
程序化事件监听器
现在,你已经知道了$emit的用法,他可以被v-on侦听,但是vue实例同时在其事件接口中提供了其他方法。
- 通过$on(eventName,eventHandler)侦听一个事件
- 通过$once(eventName,eventHandler)一次性侦听一个事件
- 通过$off(eventName,eventHandler)停止侦听一个事件
通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,他们是排的上用唱的,他们也可以用于代码组织工具,例如,你可能经常看到这种继承一个第三方库的模式
一次性将这个日期选择器附加到一个输入框上,
他会被挂载到dom上,
mounted:function(){
this.picker=new Pikaday({
field:this.$refs.input,
format:'yyyy-mm-dd'
})
},在组件被销毁之前,
也销毁这个日期选择框
beforeDestroy:function(){
this.picker.destory()}
这里有两个潜在的问题
- 他需要在这个组件实例中保存这个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()
})
}
}
注意,即便如此,如果你发现自己不得不在单个组件里做很多建立和清理的工作,最好的方式通常还是创建更多的模块化组件。
注意vue的事件系统不同于浏览器的eventtargetapi,尽管他们工作起来是相似的,但是$emit,$on,和$off并不是dispatchEvent,addEventListener和removelistener的别名。
循环引用
组件是可以在他们自己的模板中调用自身的,不过他们只能通过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></>'
类似于上述的组件将会导致“max stack size exceeded”错误,所以请确保递归调用是条件性的(例如使用一个最终会得到false的v-if)
组件之间的循环引用
假设你需要构建一个文件目录树,像访达或资源管理器那样的,你可能有一个<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-content>组件,所以我们会等到生命周期钩子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>
不过,inline-tempalte会让你模块的作用域变得更加难以理解,所以作为最佳实践,请在组件内优先选择template选项或.vue文件里的一个<template>元素来定义模板,
x-templates
另一个定义模板的方式是在一个<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'
})
这些可以用于模板特别大的demo或及小型的应用。但是其他情况下请避免使用,因为这会将模板和该组件的其他定义分离开。
控制更新
感谢 Vue 的响应式系统,它始终知道何时进行更新 (如果你用对了的话)。不过还是有一些边界情况,你想要强制更新,尽管表面上看响应式的数据没有发生改变。也有一些情况是你想阻止不必要的更新。
强制更新
如果你发现你自己需要在 Vue 中做一次强制更新,99.9% 的情况,是你在某个地方做错了事。
你可能还没有留意到数组或对象的变更检测注意事项,或者你可能依赖了一个未被 Vue 的响应式系统追踪的状态。
然而,如果你已经做到了上述的事项仍然发现在极少数的情况下需要手动强制更新,那么你可以通过 $forceUpdate 来做这件事。
通过v-once创建低开销的静态组件
渲染普通的html元素在vue中是非常快速的,但有的时候你可能有一个组件,这个组件包含了大量静态内容,在这种情况下,你可以在根元素伤添加v-once特性来确保这些北荣只计算一次然后缓存起来,就像这样
Vue.component('terms-of-service', {
template: `
<div v-once>
<h1>Terms of Service</h1>
... a lot of static content ...
</div>
再说一次,试着不要过度使用这个模式。当你需要渲染大量静态内容时,极少数的情况下它会给你带来便利,除非你非常留意渲染变慢了,不然它完全是没有必要的——再加上它在后期会带来很多困惑。例如,设想另一个开发者并不熟悉 v-once 或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新。