访问元素 & 组件
在绝大多数情况下,我们最好不要触达另一个组件实例内部或手动操作
DOM
元素。不过也确实在一些情况下做这些事情是合适的。
$root
- 所有的子组件都可以将这个实例作为一个全局
store
来访问或使用 - 对于
demo
或非常小型的有少量组件的应用来说这是很方便的。 - 不过这个模式扩展到中大型应用来说就不然了。
- 因此在绝大多数情况下,我们强烈推荐使用
Vuex
来管理应用的状态。
$parent
-
和
$root
类似,$parent
属性可以用来从一个子组件访问父组件的实例。 -
它提供了一种机会,可以在后期随时触达父级组件,以替代将数据以
prop
的方式传入子组件的方式。 -
触达父级组件会使得你的应用更难调试和理解
-
parent与children的关系
<!-- parent -->
<template>
<div>
<h3>我是爹爹,哈哈哈</h3>
<slot></slot>
</div>
</template>
<script>
export default {
data(){
return {
parentText:'text999999999'
}
},
mounted(){
console.log(this.$children, "children")
}
}
</script>
<!-- child -->
<template>
<div>
我是娃...
</div>
</template>
<script>
export default {
data(){
return {
childText:'childtttttttt'
}
},
mounted(){
console.log(this.$parent, "parent")
}
}
</script>
child
组件被parent
组件包裹
打印结果:
child
组件被div
包裹(div
包裹几层都一样:应该原生的都不算吧?)
- prop写法
- $parent写法
- 官网栗子
在一些可能适当的时候,你需要特别地共享一些组件库。举个例子,在和 JavaScript API
进行交互而不渲染 HTML
的抽象组件内,诸如这些假设性的 Google
地图组件一样:
<google-map>
<google-map-markers v-bind:places="iceCreamShops">
</google-map-markers>
</google-map>
这个 <google-map>
组件可以定义一个 map
属性,所有的子组件都需要访问它。在这种情况下 <google-map-markers>
可能想要通过类似 this.$parent.getMap
的方式访问那个地图,以便为其添加一组标记。
哈哈哈,然后可能有小伙伴就会用到this.$parent.$parent.map
(孙子访问爷爷),然后就失控了。
$refs
我们自己的项目中,这个貌似用的比较多
$refs
只会在组件渲染完成之后生效(后面$nextTick
单独一篇介绍下),并且它们不是响应式的。这仅作为一个用于直接操作子组件的【逃生舱】—— 你应该避免在模板或计算属性中访问$refs
。
provide&inject
上面$parent
一节讲到过this.$parent.$parent.map
(孙子访问爷爷),失控了。
那如果真有这种需求呢?这就是依赖注入的用武之地,它用到了两个新的实例选项:provide
和 inject
。
parent
包裹child
的情况
div
包裹child/gchild
:即当前调用div
的组件中定义provide
选项,child
及gchild
组件中定义inject
- provide 选项允许我们指定我们想要提供给后代组件的数据/方法。该选项应该是一个对象或返回一个对象的函数
provide: function () {
return {
getMap: this.getMap
}
}
// 或者
provide: {
foo: 'bar'
}
- provide/inject应用
- 然后在任何后代组件里,我们都可以使用
inject
选项来接收指定的我们想要添加在这个实例上的属性。
inject
选项应该是:
一个字符串数组,或
一个对象,对象的 key 是本地的绑定名,value 是:
在可用的注入内容中搜索用的 key (字符串或 Symbol),或
一个对象,该对象的:
from 属性是在可用的注入内容中搜索用的 key (字符串或 Symbol)
default 属性是降级情况下使用的 value
inject: ['getMap']
//或
inject: {
simpleIndex:{from:'simpleIndex', default:1000 }
}
Vue-cli 3.0 + Typescript
环境下
//父组件 provide
@Provide()
public componentActivity = this.getProvide()
private getProvide() {
return 'aaaaaa'
}
// 或者
@Provide()
public componentActivity = {name:'aaaaaa'}
//后代组件 Inject
@Inject()
private componentActivity: string
private created() {
console.info(this.componentActivity) // 'aaaaaa'
}
程序化的事件侦听器
你通常不会用到这些,但是当你需要在一个组件实例上手动侦听事件时,它们是派得上用场的
官方案例
- 你可能经常看到这种集成一个第三方库的模式:
// 一次性将这个日期选择器附加到一个输入框上
// 它会被挂载到 DOM 上。
mounted: function () {
// Pikaday 是一个第三方日期选择器的库
this.picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
},
// 在组件被销毁之前,
// 也销毁这个日期选择器。
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()
})
}
}
$on & $off &once
【vue事件总线EventBus了解下】
-
该章节学习自: vue篇之事件总线–简书:程序汪
-
EventBus
(事件总线)的简介: 在Vue
中可以来作为事件的沟通桥梁,就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,所以组件都可以上下平行地通知其他组件;但也就是太方便所以若使用不慎,就会造成难以维护的灾难,因此才需要更完善的Vuex作为状态管理中心,将通知的概念上升到共享状态层次。 -
如何使用
EventBus
- 初始化
/**单独一个js文件event-bus.js,
定义一个变量EventBus
局部定义
*/
/**实质上它是一个不具备 DOM 的组件,
它具有的仅仅只是它实例方法而已,
因此它非常的轻便
*/
import Vue from 'vue'
export const EventBus = new Vue()
/**或者直接在在main.js中初始化也可以
(这是个全局的定义,后面的具体案例先不管这个)
*/
Vue.prototype.$EventBus = new Vue()
- 发送事件:假设你有两个子组件:
DecreaseCount
和IncrementCount
,分别在按钮中绑定了decrease()
和increment()
方法。这两个方法做的事情很简单,就是数值递减(增) 1 ,以及角度值递减(增) 180 。
<!--在这两个方法中,通过
EventBus.$emit(channel: string, callback(payload1,…))
监听 decreased (和 incremented )频道。
-->
<!-- DecreaseCount.vue -->
<template>
<button @click="decrease()">-</button>
</template>
<script> import { EventBus } from "../event-bus.js";
export default {
name: "DecreaseCount",
data() {
return {
num: 1,
deg:180
};
},
methods: {
decrease() {
EventBus.$emit("decreased", {
num:this.num,
deg:this.deg
});
}
}
};
</script>
<!-- IncrementCount.vue -->
<template>
<button @click="increment()">+</button>
</template>
<script> import { EventBus } from "../event-bus.js";
export default {
name: "IncrementCount",
data() {
return {
num: 1,
deg:180
};
},
methods: {
increment() {
EventBus.$emit("incremented", {
num:this.num,
deg:this.deg
});
}
}
};
</script>
<!--
上面的示例,在 DecreaseCount 和 IncrementCount
分别发送出了 decreased 和 incremented频道。
接下来,我们需要在另一个组件中接收这两个事件,
保持数据在各组件之间的通讯。
-->
- 接收事件:现在我们可以在组件
App.vue
中使用EventBus.$on(channel: string, callback(payload1,…))
监听DecreaseCount
和IncrementCount
分别发送出了decreased
和incremented
频道。
<!-- App.vue -->
<template>
<div id="app">
<div class="container" :style="{transform: 'rotateY(' + degValue + 'deg)'}">
<div class="front">
<div class="increment">
<IncrementCount />
</div>
<div class="show-front"> {{fontCount}} </div>
<div class="decrement">
<DecreaseCount />
</div>
</div>
<div class="back">
<div class="increment">
<IncrementCount />
</div>
<div class="show-back"> {{backCount}} </div>
<div class="decrement">
<DecreaseCount />
</div>
</div>
</div>
</div>
</template>
<script>
import IncrementCount from "./components/IncrementCount";
import DecreaseCount from "./components/DecreaseCount";
import { EventBus } from "./event-bus.js";
export default {
name: "App",
components: {
IncrementCount,
DecreaseCount
},
data() {
return {
degValue:0,
fontCount:0,
backCount:0
};
},
mounted() {
EventBus.$on("incremented", ({num,deg}) => {
this.fontCount += num
this.$nextTick(()=>{
this.backCount += num
this.degValue += deg;
})
});
EventBus.$on("decreased", ({num,deg}) => {
this.fontCount -= num
this.$nextTick(()=>{
this.backCount -= num
this.degValue -= deg;
})
});
}
};
</script>
-
用一张图来描述示例中用到的 EventBus 之间的关系:
-
如果你只想监听一次事件的发生,可以使用
EventBus.$once(channel: string, callback(payload1,…))
-
移除事件监听者:
import { eventBus } from './event-bus.js'
EventBus.$off('decreased', {})
- 你也可以使用
EventBus.$off('decreased')
来移除应用内所有对此事件的监听。或者直接调用EventBus.$off()
来移除所有事件频道, 注意不需要添加任何参数 。 - 上面就是
EventBus
的使用方式,是不是很简单。PS:每次使用EventBus
时都需要在各组件中引入event-bus.js
- 事实上,我们还可以通过别的方式,让事情变得简单一些。那就是创建一个全局的
EventBus
。
- 全局的
EventBus
全局
EventBus
,虽然在某些示例中不提倡使用,但它是一种非常漂亮且简单的方法,可以跨组件之间共享数据。它的工作原理是发布/订阅方法,通常称为Pub/Sub
。
我们从上图中可以得出以下几点:
a.有一个全局EventBus
b.所有事件都订阅它
c.所有组件也发布到它,订阅组件获得更新
总结一下:
a.所有组件都能够将事件发布到总线,
b.然后总线由另一个组件订阅,
c.然后订阅它的组件将得到更新
var EventBus = new Vue();
Object.defineProperties(Vue.prototype, {
$bus: {
get: function () {
return EventBus
}
}
})
/**
现在,这个特定的总线使用两个方法 $on 和 $emit 一个用于创建发出的事件,它就是$emit
另一个用于订阅 $on
*/
this.$bus.$emit('nameOfEvent',{ ... pass some event data ...});
this.$bus.$on('nameOfEvent',($event) => {
// ...
})
现在我们创建两个简单的组件:一个 ShowMessage
的组件用来显示信息,另外创建一个 UpdateMessage
的组件,用来更新信息。
<!-- UpdateMessage.vue -->
<template>
<div class="form">
<div class="form-control">
<input v-model="message" >
<button @click="updateMessage()">更新消息</button>
</div>
</div>
</template>
<script>
export default {
name: "UpdateMessage",
data() {
return {
message: "这是一条消息"
};
},
methods: {
updateMessage() {
this.$bus.$emit("updateMessage", this.message);
}
},
beforeDestroy () {
$this.$bus.$off('updateMessage')
}
};
</script>
<!-- ShowMessage.vue -->
<template>
<div class="message">
<h1>{{ message }}</h1>
</div>
</template>
<script>
export default {
name: "ShowMessage",
data() {
return {
message: "我是一条消息"
};
},
created() {
var self = this
this.$bus.$on('updateMessage', function(value) {
self.updateMessage(value);
})
},
methods: {
updateMessage(value) {
this.message = value
}
}
};
</script>
EventBus
注册在全局上时,路由切换时会重复触发事件
-
看了篇文章:vue中eventbus被多次触发
-
全局定义的事件是不会跟随组件的生命周期函数进行状态改变的。
-
切换路由时,如果不手动清除事件的话,它会注册多次。
-
手动清除事件
created() {
this.bus.$off('clickBus');
//在每次创建事件之前,手动清除定义的事件
//根据实际的业务需求也可以在beforeDestroy()和destroyed()
},
mounted(){
this.bus.$on('clickBus', (e) => {})
}
$once
【定时器销毁案例看下】
- 这个首先看下官方案例,即【程序化的事件侦听器】刚开始讲到的那个
- 方案1:
该方案有两点不好的地方,引用尤大的话来说就是:
- (1)它需要在这个组件实例中保存这个数据timer,这是多余的。
- (2)我们的建立定时器代码独立于我们的清理代码(定时器需要卸载页面的时候卸载),这使得我们比较难于程序化的清理我们建立的所有东西(意思是执行代码和清除代码不在一起)。
- 方案2(同官网案例)
循环引用
递归组件(自己调自己)
组件是可以在它们自己的模板中调用自身的。不过它们只能通过 name
选项来做这件事(动态组件和异步组件一章中我们提起过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></stack-overflow></div>'
A组件与B组件相互调用
这个我们也在动态组件和异步组件的“异步组件”中讲到了
组件之间的循环引用
模板定义的替代品
A. 内联模板inline-template
定义一个私有子组件时,如果子组件的template过长会使得代码非常难以阅读
这时可以使用内联模版
不过,
inline-template
会让模板的作用域变得更加难以理解。所以作为最佳实践,请在组件内优先选择template
选项或.vue
文件里的一个<template>
元素来定义模板。
B. <script type="text/x-template">
这个的意义应该和inline-template
差不多,用法如下图:
控制更新
Vue
响应式系统,它始终知道何时进行更新 (如果你用对了的话)。不过还是有一些边界情况,你想要强制更新($forceUpdate
),尽管表面上看响应式的数据没有发生改变。也有一些情况是你想阻止不必要的更新(v-once
)。
强制更新$forceUpdate
如果你发现你自己需要在
Vue
中做一次强制更新,99.9%
的情况,是你在某个地方做错了事。
- 你可能还没有留意到数组或对象的变更检测注意事项(后面另行学习),或者你可能依赖了一个未被
Vue
的响应式系统追踪的状态。 - 一个对象数组,我们尝试直接给某个item增加一个属性,发现页面上没有效果;直接将
length
变成0
来清空数组,也没有效果,关键代码如下:
- 上面是我们按照
vue
的规范去写的,是可以实现变化的,关键代码如下:
- 可是如果我们不想利用
$set
去设置,非要按照我们第一种方式去写,可以实现么?答案是可以的,就是利用$forceUpdate
了,因为你修改了数据,但是页面层没有变动,说明数据本身是被修改了,但是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
或漏看了它在模板中,他们可能会花很多个小时去找出模板为什么无法正确更新
总结一句
边界情况这块内容挺多的,但是基本上实际开发应用中都不咋用