父传子
props
父组件
子组件
- props只能是父组件向子组件进行传值,props使得父子组件之间形成了一个单向下行绑定。子组件的数据会随着父组件不断更新。
props 可以显示定义一个或一个以上的数据,对于接收的数据,可以是各种数据类型,同样也可以传递一个函数。
provide / inject
provide / inject是Vue提供的两个钩子,和data、methods是同级的。并且provide的书写形式和data一样。● provide 钩子用来发送数据或方法● inject钩子用来接收数据或方法(接收传递的数据,类似peops)
- 就是 provide 可以在祖先组件中指定我们想要提供给后代组件的数据或方法,而在任何后代组件中,我们都可以使用 inject 来接收 provide 提供的数据或方法。
例子:
// 父级组件提供 'foo'<template><div><div>{{foo}}</div><son></son></div></template><script>import Son from "./Son";export default {name: "parent",components: { Son },provide() {return {foo: this.foo};},data() {return {foo: "测试",};},mounted() {console.log(this.foo)},};</script>//子级组件,不接收<template><grandSon></grandSon></template><script>import grandSon from "./grandSon";export default {name: "son",components: { grandSon },};</script>//孙级组件,接收foo<template><div>{{foo}}</div></template><script>export default {name: "grandSon",inject: ["foo"],mounted() {console.log(this.foo)},};</script>
在这里我们可以发现孙组件越过子组件接收了父组件注入的数据,我们可以理解为爷爷越过爸爸偷偷给孙子买了辣条,这是一组最简单的用法,当层级继续增加时,仍可通过这种方式由父组件直接跨域多个层级向后代组件注入数据。
有一点需要特别注意的是,实际上我们可以将当前组件inject获取的数据直接赋值给它本身的data或props,不过官网提示我们,这是在Vue2.2.1版本才实现的功能,在这之前,必须先进行props和data数据的初始化。
数据的响应更新
在尝试中我们发现,由于Vue的单向流关系,实际上如果在parent中改变了初始传入的foo的值以后,grandSon并不会得到改变后的值,也就是在这个时候,父孙组件的数据出现了不一致的情况,我们肯定是希望拿到的数据是一致的,怎么来解决这个问题呢,官网还有一句提示为我们提供了解释。
提示:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的 property 还是可响应的。 也就是指,我们需要人为的将这组数据关系变成可响应的,哦,我们之前的foo是一个字符串,基本数据类型是不具有响应特性的,那么,我们可能需要传递一个对象。
// 改造一下父级组件提供 'foo'<template><div><div>{{foo}}</div><son></son></div></template><script>import Son from "./Son";export default {name: "parent",components: { Son },provide() {return {foo: this.foo};},data() {return {foo: {foo: "测试"},,};},mounted() {console.log(this.foo)},};</script>
在这个栗子中,改造了一下父组件提供的数据,测试发现,foo变成了一组可响应的数据。经过尝试我发现在这种情况下如果在孙组件改变inject中foo的值,也会响应的更新到父组件中,当然为了保护单向数据流机制,最佳实践还是不要在子组件里更改inject。
默认值设置
在 2.5.0+ 的注入可以通过设置默认值使其变成可选项
<script>export default {name: "grandSon",inject: {foo: {from: 'bar',default: () => [1, 2, 3]}},mounted() {console.log(this.foo)},};</script>:
在2.5.0 版本之后的版本中可以为inject提供默认值了,如果它需要从一个不同名字的 property 注入,则使用 from 来表示其源 property,与 prop 的默认值类似,需要对非原始值使用一个工厂方法。
缺点
provide/inject 的缺点还是非常明显的:
- 当多个后代组件同时依赖同一个父组件提供数据时,只要任一组件对数据进行了修改,所有依赖的组件都会受到影响,实际上是增加了耦合度。
- 任意层级访问使数据追踪变的比较困难,你并不能准确的定位到是哪一个层级对数据进行了改变,当数据出现问题时,尤其是多人协作时,可能会大大增加问题定位的损耗。
优点
在进行组件库或高级插件开发时,provide/inject 仍然是一个不错的选择。ref/$refs
ref: 这个属性用在子组件上,它的引用就指向了子组件的实例。可以通过实例来访问组件的数据和方法。
子传父
自定义事件($emit)
子组件
this.$emit('refresh')
父组件
<template><!-- 打标签 --><add-labels ref="addLabels" @refresh="refresh"></add-labels></template><script>export default {methods:{// 刷新数据refresh() {this.getList()},}}</script>
父子通信
$parent / $children
$parent/$children的常用场景:封装嵌套组件时,直接使用长辈或者子孙组件的方法,该方法并不改变数据,常常结合$broadcast/$dispatch使用。
- 组件本身没有父组件和子组件,而是在运行时,每个组件实例的位置,决定其父组件和子组件
- 每个组件实例有且只有一个父组件(顶级组件实例没有哈)
- 但每个组件实例可能没有子组件,可能有一个,也可能有多个,所以一般children是数组,没有的时候是空数组
- js 运行时,若添加或者删除组件实例,那些发生位置变化的组件实例们,父组件和子组件也会发生变化。
写一个页面组件,里面放些组件,打印下$parent/$children
<template lang="pug">//- 页面组件divlist-itemlist-item</template><script>import ListItem from "@/components/ListItem";export default {name: "List",components: { ListItem },mounted() {console.log("页面的$parent", this.$parent);console.log("页面的$children", this.$children);}};</script>
其实还能看到,渲染的时候先子组件的mounted,然后再是自己的mounted(这与生命周期有关).
要是想在子组件里获取父组件的元素之类的,必须使用nextTick。
$broadcast/$dispatch
在使用element-ui时,如下:
<template lang="pug">el-formel-form-itemel-input</template>
假设el-input想要执行el-form上的方法,就会这样this.$parent.$parent.methodXx(),更多层级可能更复杂,于是$dispatch就诞生了。(vuex中使用的dispatch和这个类似)
// main.js// 向上某个组件,派发事件Vue.prototype.$dispatch = function(eventName, componentName, ...args) {let parent = this.$parent;while (parent) {// 只有是特定组件,才会触发事件。而不会一直往上,一直触发const isSpecialComponent = parent.$options.name === componentName;if (isSpecialComponent) {// 触发了,就终止循环parent.$emit(eventName, ...args);return;}parent = parent.$parent;}};
这样在el-input里想要触发el-form里的方法,this.$dispatch(‘changeSort’,’el-form’,{isAsc:true})
同理,假设el-form想要执行el-input上的方法,就会这样this.$children[0].$children[0].methodXx(),更多层级可能更复杂,于是$broadcast就诞生了,使用的时候this.$broadcast(‘changeValue’,’el-input’,’hello’)。
注意,$children 是数组,所以当只有一个子组件时,使用[0]获取。当有多个子组件时,它并不保证顺序,也不是响应式的。
// 向下通知某个组件,触发事件Vue.prototype.$broadcast = function(eventName, componentName, ...args) {// 这里children是所有子组件,是子组件不是后代组件哈let children = this.$children;broadcast(children);// 这里注意,抽离新的方法递归,而不是递归$broadcastfunction broadcast(children) {for (let i = 0; i < children.length; i++) {let child = children[i];const isSpecialComponent = child.$options.name === componentName;if (isSpecialComponent) {// 触发了,就终止循环child.$emit(eventName, ...args);return;}// 没触发的话,就看下有没有子组件,接着递归child.$children.length &&child.$broadcast(eventName, componentName, ...args);}}};
$attrs / $listeners(属性/方法)
$attrs的使用
官方定义:
包含了父作用域中不作为prop 被识别 (且获取) 的特性绑定 (class和 style除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class和 style除外),并且可以通过 v-bind=”$attrs” 传入内部组件——在创建高级别的组件时非常有用。
(好抽象一脸懵逼。。。)
$attrs只代表的是那些没有被声明为props的属性,如果某个prop被子组件中声明了(就是这个属性已经在子组件的props中了),在子组件中的$attr会把声明的prop剔除。
个人理解: 一个组件在父组件中被引用,$attrs就是组件标签上的静态属性值(attr)和动态属性值(:attr)的对象集合,这个集合不包含class, style和事件属性
// 父组件<child-com class="com" name="attr" :foo="foo" :boo="boo" :coo="coo" :doo="doo" @click="test"></child-com>...data() {return {foo: 'Hello World!',boo: 'Hello Javascript!',coo: 'Hello Vue',doo: 'Last'}},...// child-com.vueexport default {name: 'childCom',components: {childCom2},created() {console.log(this.$attrs) // {name: "attr", foo: "Hello World!", boo: "Hello Javascript!", coo: "Hello Vue", doo: "Last"}}}
如果子组件声明了$prop,$attrs中与$props相同的属性会被移除
// child-com.vueexport default {name: 'childCom',props: ['foo'], // foo被声明components: {childCom2},created() {console.log(this.$attrs) // {name: "attr", boo: "Hello Javascript!", coo: "Hello Vue", doo: "Last"}}}
如果childCom里的子组件还用到foo,可以继续将foo传给子组件
<template><div><p>foo: {{foo}} </p><child-com2 :foo="foo" v-bind="$attrs"></child-com2></div></template><script>const childCom2 = () => import('./childCom2.vue')export default {name: 'childCom1',props: ['foo'], // foo作为props属性绑定inheritAttrs: false,components: {childCom2},created() {console.log(this.$attrs) // {name: "attr", boo: "Hello Javascript!", coo: "Hello Vue", doo: "Last"}}}</script>//childCom2.vuecreated() {console.log(this.$attrs) // {foo: "Hello World!", name: "attr", boo: "Hello Javascript!", coo: "Hello Vue", doo: "Last"}}
inheritAttrs要设置成false还是true(默认)看下图就知道,(inheritAttrs为true时允许动态或静态属性显示,为false时即使你给组件设置了属性也不会显示)
与$props比较
$props必须在组件中注册了props才能用拿到值,所以在嵌套层级比较深的组件中$attrs拿值更加便捷
$listeners的使用
包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过v-on=”$listeners”传入内部组件。
归纳起来也是两点:
$listeners是组件的内置属性,它的值是父组件(不含.native修饰器的) v-on事件监听器。
组件可以通 过在自己的子组件上使用v-on=”$listeners”,进一步把值传给自己的子组件。如果子组件已经绑定$listener中同名的监听器,则两个监听器函数会以冒泡的方式先后执行。
父组件:
<template><div class="test"><child v-bind="{name, sex, age}" v-on="{changeName,changeAge}"></child></div></template><script>import child from './child'export default {data() {return {name: '张三',sex: '男',age: 11,}},components: {child},methods: {changeName(name) {this.name = name},changeAge(age) {this.age = age}}}</script>
子组件child.vue
<template><div class="child">child组件的$attrs {{$attrs}}<child-child v-bind="$attrs" v-on="$listeners" @showAttrs="showAttrs"></child-child></div></template><script>import childChild from './child-child'export default {name: "child",props: ['name'],inheritAttrs: false,created() {console.log('child', this.$listeners)},components: {childChild},methods: {showAttrs() {console.log(this.$attrs)}}}</script>
孙子组件:child-child.vue
<template><div class="child-child">child-child组件的$attrs {{$attrs}}</div></template><script>export default {name: "child-child",inheritAttrs: false,created() {console.log('child-child',this.$listeners)}}</script>
全局事件总线
- 一种组件间通信的方式,适用于任意组件间通信。
安装全局事件总线:
new Vue({......beforeCreate() {Vue.prototype.$bus = this //安装全局事件总线,$bus就是当前应用的vm},......})
使用事件总线:
1.接收数据:A组件想接收数据,则在A组件中给$bus绑定自定义事件,事件的回调留在A组件自身。
methods(){demo(data){......}}......mounted() {this.$bus.$on('xxxx',this.demo)}
2.提供数据:this.$bus.$emit(‘xxxx’,数据)
- 最好在beforeDestroy钩子中,用$off去解绑当前组件所用到的事件。
例子 : ```javascript //引入Vue import Vue from ‘vue’ //引入App import App from ‘./App.vue’ //关闭Vue的生产提示 Vue.config.productionTip = falsebeforeDestroy() {this.$bus.$off('hello')},
// 安装全局事件总线 Vue.prototype.$bus = new Vue()
//创建vm new Vue({ el:’#app’, render: h => h(App), })
两个兄弟组件 :<br />**发送的数据的组件:**```javascript<template><div class="student"><h2>学生姓名:{{name}}</h2><h2>学生性别:{{sex}}</h2><button @click="sendStudentName">把学生名给School组件</button></div></template><script>export default {name:'Student',data() {return {name:'张三',sex:'男',}},mounted() {// console.log('Student',this.x)},methods: { // 点击事件,把学生们传递到兄弟组件SchoolsendStudentName(){this.$bus.$emit('hello',this.name)}},}</script>
接受数据的组件
<template><div class="school"><h2>学校名称:{{name}}</h2><h2>学校地址:{{address}}</h2></div></template><script>export default {name:'School',data() {return {name:'尚硅谷',address:'北京',}},mounted() { //接受数据this.$bus.$on('hello',(data)=>{console.log('我是School组件,收到了数据',data)})},beforeDestroy() { // 销毁调全局事件总线this.$bus.$off('hello')},}</script>
vuex
核心流程及主要功能
● Vue Components 是 vue 组件,组件会触发(dispatch)一些事件或动作,也就是图中的 Actions;
● 在组件中发出的动作,肯定是想获取或者改变数据的,但是在 vuex 中,数据是集中管理的,不能直接去更改数据,所以会把这个动作提交(Commit)到 Mutations 中;
● 然后 Mutations 就去改变(Mutate)State 中的数据;
● 当 State 中的数据被改变之后,就会重新渲染(Render)到 Vue Components 中去,组件展示更新后的数据,完成一个流程。
各模块在核心流程中的主要功能
● Vue Components∶ Vue组件。HTML页面上,负责接收用户操作等交互行为,执行dispatch方法触发对应action进行回应。
● dispatch∶操作行为触发方法,是唯一能执行action的方法。
● actions∶ 操作行为处理模块。负责处理Vue Components接收到的所有交互行为。包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发。向后台API请求的操作就在这个模块中进行,包括触发其他action以及提交mutation的操作。该模块提供了Promise的封装,以支持action的链式触发。
● commit∶状态改变提交操作方法。对mutation进行提交,是唯一能执行mutation的方法。
● mutations∶状态改变操作方法。是Vuex修改state的唯一推荐方法,其他修改方式在严格模式下将会报错。该方法只能进行同步操作,且方法名只能全局唯一。
● state∶ 页面状态管理容器对象。集中存储Vuecomponents中data对象的零散数据,全局唯一,以进行统一的状态管理。页面显示所需的数据从该对象中进行读取,并进行状态更新。
● getters∶ state对象读取方法。
总结
Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 提交修改信息, Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走 Action ,但 Action 也是无法直接修改 State 的,还是需要通过Mutation 来修改State的数据。最后,根据 State 的变化,渲染到视图上。
