前言:

1>在介绍组件传值之前先明确三种组件关系:父子组件、兄弟组件、无关系组件。

如上图所示:

  1. 父子关系:A 和 B、B 和 C、B 和 D 都是父子关系;
  2. 兄弟关系:C 和 D 是兄弟关系,
  3. 无关系:A 和 C、A 和 D 是隔代关系(可能隔多代)。

2.常见使用场景可以分为三类:

  • 父子组件通信: props/$emit;ref ; $parent/$children;provide/inject ;$attrs/$listeners
  • **兄弟组件通信: **eventBus ; vuex
  • **跨级通信: **eventBus;Vuex;本地传值;provide/inject ;$attrs/$listeners;

一、props / $emit

父组件通过 props 的方式向子组件传递数据,而通过$emit  子组件可以向父组件通信。

1.父传子(属性传值)必须掌握

即父组件通过属性的方式向子组件传值,子组件通过 props 来接收。

父组件 parent:

<template>
  <div id="app">
    <childeren :message="父组件给子组件传值了"></childeren>
  </div>
</template>
<script>
  import 'children' from './children.vue' //引入子组件
  export default {
     components:{childeren} //使用组件
  }
</script>

子组件 children.vue

<script>
export default {
		//props:[message], //简写
    props:{
        'message':{
        	type:'String',
          default:''
        }
    }
}
</script>

总结: prop 只可以从上一级组件传递到下一级组件(父子组件),即所谓的单向数据流。而且 prop 只读,不可被修改,所有修改都会失效并警告。

2. 子传父(通过事件形式)必须掌握

子组件通过事件向父组件传值,子组件绑定一个事件,通过 this.$emit() 来触发

子组件 children.vue

<template>
 	<div @click="childEvent">子组件</div>
</template>
<script>
export default {
	methods:{
  	childEvent(){
    	this.$emit('childEvent',val)
    }
  }
}
</script>

父组件 parent:

<template>
  <div id="app">
    <childeren @childEvent="childEvent"></childeren>
  </div>
</template>
<script>
  import 'children' from './children.vue' //引入子组件
  export default {
     components:{childeren} ,//使用组件
     methods:{
     	 childEvent(val){
       	 console.log(val)
       }
     }
  }
</script>

3.父子数据同步(修饰符.sync)

1.通过$emit()去同步子元素和父元素

通过以上三种方式, 我想你应该能解决绝大多数父子组件通信的场景了,但让我们再仔细考虑一下上面的通信场景,就会发现它们还可能存在的问题:
从子组件向父组件传递数据时,父子组件中的数据仍不是每时每刻都同步的,

在某些特殊的需求场景下,我们可能会希望父子组件中的数据时刻保持同步, 这时候你可能会像下面这样做:

//这是父组件中的template:
<son :foo="bar" v-on:update="val => bar = val"></son>

同时每当子组件中数据改变的时候,通过

this.$emit("update", newValue);

把参数 newValue 传递给父组件 template 中监听函数中的"val"。然后通过

(val) => (bar = val);

表达式 val => bar = val 意味着强制让父组件的数据等于子组件传递过来的数据, 这个时候,我们发现父子组件的地位是平等的。 父可以改变子(数据), 子也可以改变父(数据)

2.通过 sync 实现数据双向绑定, 从而同步父子组件数据

parent.vue
父组件 :msg.sync="fatherValue"

<template>
  <div>
    <son :msg.sync="fatherValue"></son>
  </div>
</template>

<script>
import son from './son.vue'
export default {
  data: function () {
    return {
      fatherValue: ''
    }
  },
  components: {
    son: son
  }
}
</script>

child.vue
子组件:this.$emit('update:msg', this.msg))

<template>
  <div>
    <p>智力: {{ wisdom }}</p>
    <button @click="changeChild('msg')">子组件</button>
  </div>
</template>

<script>
export default {
  props: {
    msg: { // 父组件传递的值
    	type:Number
    }
  },
  methods: {
    changeChild (dataName) {
      this.$emit(`update:${dataName}`, this.msg+1)
    }
  }
}
</script>

二、兄弟组件传值(eventBus,vuex,本地传值)

不同组件之间传值,通过 eventBus(小项目少页面用 eventBus,大项目多页面使用 vuex,简单的用本地传值)

1.eventBus(事件总线)必须掌握

vue 中可以使用它来作为沟通桥梁的概念, 就像是所有组件共用相同的事件中心,可以向该中心注册发送事件或接收事件,包括父子、兄弟、跨级。
eventBus 也有不方便之处, 当项目较大,就容易造成难以维护的灾难。

1>.安装并在 main.js 中引用

npm install vue-bus //main.js import VueBus from 'vue-bus' Vue.use(VueBus);

2>.使用

a.vue 分发事件的组件

//分发事件的组件(传递方法和值)
this.$bus.emit("changeEvent", "测试传值");

b.vue 监听 a.vue 传递过来的事件和值

//监听的组件
// ...
created() {
  this.$bus.$on('changeEvent', (params) => {  //获取传递的参数并进行操作
      //todo something
  })
},

// 清除事件监听
beforeDestroy () {// 最好在组件销毁前
  this.$bus.$off('changeEvent');
},

3>.方法使用总结:

//...
created() {
  this.$bus.$on('changeEvent',this.change)
},
methods:{
	change(val){
   	 console.log(val) //测试传值
  }
},
// 移除事件监听者
beforeDestroy () {// 最好在组件销毁前
  this.$bus.$off('changeEvent',this.change);
},

2.vuex(必须掌握)

1>vuex 介绍:

Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化.
Vuex 解决了多个视图依赖于同一状态和来自不同视图的行为需要变更同一状态的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上

2>vuex 的原理

Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件要更改 State 中的数据时,必须通过 Mutation 进行,Mutation 同时提供了订阅者模式供外部插件调用获取 State 数据的更新。而当所有异步操作(常见于调用后端接口异步获取更新数据)或批量的同步操作需要走 Action,但 Action 也是无法直接修改 State 的,还是需要通过 Mutation 来修改 State 的数据。最后,根据 State 的变化,渲染到视图上。

3> Vuex 各个模块

  • **state:**用于数据的存储,是 store 中的唯一数据源;
  • **getters:**state 对象读取方法,如 vue 中的计算属性一样,常用于数据的筛选和多个数据的相关性计算;
  • **mutations:**状态改变操作方法,改变 state 数据的唯一途径,只能进行同步操作,且方法名只能全局唯一;
    • 由 actions 中的 commit('mutation 名称')来触发。
    • commit**:**状态改变提交操作方法。对 mutation 进行提交,是唯一能执行 mutation 的方法。
    • Vue Components:Vue 组件。HTML 页面上,负责接收用户操作等交互行为,执行 dispatch 方法触发对应 action 进行回应。
  • **actions:**操作行为处理模块,,用于触发 mutation 调用,间接更新 state,包含同步/异步操作,支持多个同名方法,按照注册的顺序依次触发;
    • 由组件中的$store.dispatch('action 名称', data1)来触发。然后由 commit()来触发 mutation 的调用 , 间接更新 state。该模块提供了 Promise 的封装,以支持 action 的链式触发。
    • dispatch**:**操作行为触发方法,是唯一能执行 action 的方法。
  • **modules:**类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护。

3>应用

3.1 安装 vuex

 cnpm install vuex --save

3.2 在 src 新建一个 store 文件夹
3.3 store 文件夹里新建一个 index.js 并写入

import Vue from "vue";
import vuex from "vuex";
Vue.use(vuex);

export default new vuex.Store({
  state: {
    show: false,
  },
});

3.4 在 main.js 入引入

//vuex
import store from "./store";

//实例化Vue对象时加入store 对象
new Vue({
  el: "#app",
  router,
  store, //使用store
  template: "<App/>",
  components: { App },
});

$store.state.show 无论哪个组件都可以使用 , 那组件多了之后 , 状态也多了 , 这么多状态都堆在 store 文件夹下的 index.js 不好维护怎么办 ?
我们可以使用 vuex 的 modules, 把 store 文件夹下的 index.js 改成

store/index.js

import Vue from "vue";
import vuex from "vuex";
Vue.use(vuex);

import defaultState from "./state/state"; //state状态模块
import getters from "./getters/getters"; //getter模块
import mutations from "./mutations/mutations"; //mutations模块
import actions from "./actions/actions"; //actions模块

//modules模块
import app from "./modules/app";
import user from "./modules/user";
import shop from "./modules/shop";

const store = new Vuex.Store({
  state: defaultState,
  getters,
  mutations,
  actions,
  modules: {
    app,
    user,
    shop,
  },
  // 模块, 相当于里面又进行了一个vuex
  // modules: {
  //   a: moduleA,
  //   b: moduleB
  // }
});

举例:modules/app.js

const app = {
  state: {
    count:0,
    products:[{name:'大米',price:20},{name:'小米',price:25},{name:'黑米'}]
  },
  getters: { // 将方法写在store的getters中,供其他组件调用(减少冗余)
    saleProducts: (state) => {
      var saleProducts = state.products.map(product => { // 使用map遍历生成新的数组
        return {name: '***' + product.name + '**',}
      })
      return saleProducts
     }
  },
  mutations: {//只能做同步操作,参数为state和传入的参数Payload
   	updateCount(state,count){
       state.count = count
    }
  },
  actions: { //只能做异步操作,且actions提交到mutations来改变state.
     updateCountSync({commit,state},count){
       setTimeout(function() {
         increment (context) {
           commit('updateCount',count)
         }, 1000);
      }
  }
}

export default app

在组件 home.vue 中使用

export default {
  name: 'ProductListOne',
  computed:{//注意获取state中值的要写在computed计算属性中,否则不能及时更新的。
    /***1.获取state中的值****/
    count(){
       return this.$store.state.count;
    },
    /***获取state中的值****/

    /****2.获取getters中的值,2种方案等同***/
    saleProducts(){
       return this.$store.getters.saleProducts;
    },
    //简写
    ...mapGetters([
      'saleProducts'
    ])
    /****获取getters中的值,2种方案等同***/

  },
  data(){
  	return {
    	number:1
    }
  },
  methods: {
    reducePrice (amount) { // 使用this.$store.commit('')来store传递一个事件
       this.$store.state.products.forEach(product => {
         product.price -= 1
       })
       this.$store.commit('reducePrice')// commit是mutations的同步方法
       this.$store.dispatch('reducePriceSync', amount)// dispatch分发是actions中异步的方法
     },

     /**在组件中同步触发muations和异步触发actions**/
     plus(){
       let count = Number(this.number)+1

       /**3.使用commit进行同步执行muations**/
     	 this.$store.commit('updateCount',count)
       /**使用commit进行同步执行muations**/

       /**4.使用dispatch进行异步执行actions**/
       this.$store.dispatch('updateCountSync',count)
       /**使用dispatch进行异步执行actions**/
     }
	   /**在组件中同步触发muations和异步触发actions**/
  }
</script>
  • 通信比较简单,缺点是数据和状态比较混乱,不太容易维护
  • 注意用 JSON.parse() / JSON.stringify()  做数据格式转换
  • localStorage / sessionStorage 可以结合 vuex, 实现数据的持久保存,同时使用 vuex 解决数据和状态混乱问题.

存:

localStorage.setItem("tolist", JSON.stringify(this.tolist));

取:

var tolist = JSON.parse(localStorage.getItem("tolist"));

三者的异同

特性sessionStoragelocalStorageCookie
数据的生命期仅在当前会话下有效,关闭页面或浏览器后被清除除非被清除,否则永久保存一般由服务器生成,可设置失效时间。如果在浏览器端生成 Cookie,默认是关闭浏览器后失效
存放数据大小4K 左右一般为 5MB
与服务器端通信每次都会携带在 HTTP 头中,如果使用 cookie 保存过多数据会带来性能问题仅在客户端(即浏览器)中保存,不参与和服务器的通信
易用性需要自己封装,源生的 Cookie 接口不友好源生接口可以接受,亦可再次封装来对 Object 和 Array 有更好的支持

三、路由传值 (必须掌握)

1.父组件 push 使用 this.$router.push
2.在子组件中获取参数的时候是this.$route.params

1>Query 传参(参数在 URL 路径拼接显示)

1.传值

//第一种写法 :
	 <router-link :to="{name:'Log',query:{id:6}}">显示登录页面</router-link>

//第二种写法 :
  goToUser() {
    this.$router.push({name:'Log',query:{id:6}});
  }

2.取值

//在对应页面取值
 this.$route.query;  // 结果:{id:6} 刷新页面参数丢失

//网页地址显示为
 http://localhost:8080/#/log?id=6

2>动态路由传值(刷新不丢失参数)

1>配置动态路由

routes: [
  //动态路由参数  以冒号开头
  { path: "/user/:id", conponent: User },
];

2.传值

//第一种写法 :
	<router-link :to="'/user/'+item.id">传值</router-link>

//第二种写法 :
  goToUser(id) {
    this.$router.push( {path:'/user/'+id});
  }

3.取值

//在对应页面取值
 this.$route.params;  // 结果:{id:123}

//网页地址显示为
 http://localhost:8080/#/log/123

3>params 传参(参数不在 URL 路径拼接显示)

注意:上述这种利用 params 不显示 url 传参的方式会导致在刷新页面的时候,传递的值会丢失

1.传值

//第一种写法 :
	 <router-link :to="{name:'Log',params:{id:6}}">显示登录页面</router-link>

//第二种写法 :
  goToUser() {
    this.$router.push({name:'Log',params:{id:6}});
  }

2.取值

//在对应页面取值
 this.$route.params;  // 结果:{id:6} 刷新页面参数丢失

//网页地址显示为
 http://localhost:8080/#/log

四、ref / refs , $children / $parent

通过$refs,$parent,$children 就可以访问组件的实例,拿到实例代表什么?代表可以访问此组件的所有方法和 data。

1>ref , refs

ref:如果在普通的 DOM 元素上使用,引用指向的就是 DOM 元素;如果用在子组件上,引用就指向组件实例,可以通过实例直接调用组件的方法或访问数据, 我们看一个 ref  来访问组件的例子:

//  在我们需要获取实例的地方定义ref
<my-component ref="myRef"></my-component>;
// 然后我们在js中通过$refs方式获取该实例
this.$refs.myRef;

2>$parent,$children

要注意边界情况,如在#app 上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent 得到的是 undefined,而在最底层的子组件拿$children是个空数组。也要注意得到$parent 和$children的值不一样,$children  的值是数组,而$parent 是个对象。

父组件:

<template>
    <my-component></my-component>
</template>
<script>
    export default{
        data(){
            return{
                msg: 'this is old',
              	num:0
            }
        },
        methods:{
        	changeChild(){
          	this.$children[0].messageChild='this is new value'
          }
        }

    }
</script>

子组件:

<template>
    <div>我是子组件</div>
		<p>获取父组件的值为:{{parentVal}}</p>
</template>
<script>
    export default{
				data(){
        	return {
          	messageChild:'this is value'
          }
        },
        computed:{
        	 parentVal(){
               // 通过$parent我们可以获取父组件实例,且可以更改父组件的值
             	 this.$parent.num=1;
               return this.$parent.msg;
            }
        }
    }
</script>

如果子组件是公共组件,会被多个父组件调用,那么$parent 会怎么获取?改变他们的属性将会怎么变化?父组件中没有这个属性怎么办?

  1. 针对不同父组件调用,子组件会每次都会生成一个实例,这也是 Vue 的重要机制。$parent 会获取每个调用它的父组件实例。
  2. 子组件中通过$parent 会改变每个调用它的父组件中的对应属性。

总结:上面两种方式用于父子组件之间的通信, 而使用 props 进行父子组件通信更加普遍; 二者皆不能用于非父子组件之间的通信

五、provide/ inject

概念:provide/ inject 是 vue2.2.0 新增的 api, 简单来说就是父组件中通过 provide 来提供变量, 然后再子组件中通过 reject 来注入变量。

A.vue

<templete>
  <div>
  	<comB></comB>
  </div>
<templete>
<script>
 import comB from './B.vue'
 export default({
 	name:'A',
  provide:{ forData:'demo' },
  components:{comB}
 })
<script>

B.vue

<templete>
  <div>
  	{{demo}}
  	<comC></comC>
  </div>
<templete>
<script>
 import comC from './C.vue'
 export default({
 	name:'B',
  inject:['forData'],
  components:{comC},
  data(){
  	return {
    	demo:this.forData
    }
  }
 })
<script>

C.vue

<templete>
  <div>
  	{{demo}}
  </div>
<templete>
<script>
 export default({
 	name:'C',
  inject:['forData'],
  components:{comC},
  data(){
  	return {
    	demo: this.forData
    }
  }
 })
<script>

六、$attrs与 $listeners

多级组件嵌套需要传递数据时,通常使用的方法是通过 vuex。但如果仅仅是传递数据,而不做中间处理,使用 vuex 处理,未免有点大材小用。为此 Vue2.4 版本提供了另一种方法—-$attrs/$listeners, 新增了 inheritAttrs  选项。

  • $attrs:包含了父作用域中不被 prop 所识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind=”$attrs” 传入内部组件。通常配合 interitAttrs 选项一起使用。
  • $listeners:包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on=”$listeners” 传入内部组件

index.vue

<templet>
  <div>
  	<childA :name="name" :age="age" :sex="sex" :weight="weight" title="这是一个属性"></childA>
  </div>
<templete>
<script>
 const childA=()=>import('./childA.vue')
 export default({
 	name:'index',
  components:{childA},
  data(){
  	return {
    	name:'Jacke',
      age:'18',
      sex:'女',
      weight:'93'
    }
  }
 })
<script>

childA.vue

<templet>
  <div>
  	<P>name:{{name}}</P>
		<p>chilA的$attrs:{{$attrs}}</p>
  	<childB v-bind="$attrs"></childB>
  </div>
<templete>
<script>
 const childA=()=>import('./childB.vue')
 export default({
 	name:'childB',
  components:{childB},
  inheritAttrs :false,//可以关闭自动挂载到组件根元素上的没有在props上申明的属性
  props:{name:String} // name作为props的属性绑定
  data(){
  	return {
    	nmae:'Jacke'
    }
  },
  created(){
  	console.log(this.$attrs)
    //{ "age":"18","sex":'女', "weight":"93",title:'这是一个属性'}
  }
 })
<script>

childB.vue

<templet>
  <div>
  	<P>age:{{age}}</P>
		<p>chilB的$attrs:{{$attrs}}</p>
  </div>
<templete>
<script>
 export default({
 	name:'childB',
  inheritAttrs :false,//可以关闭,自动挂载到组件根元素上的没有在props上申明的属性
  props:{name:String} // name作为props的属性绑定
  data(){
  	return {
    	age:'Jacke'
    }
  },
  created(){
  	console.log(this.$attrs)
    //{"name":"Jacke", "sex":'女', "weight":"93",title:'这是一个属性'}
  }
 })
<script>

文章引用:

https://www.bilibili.com/read/cv2898813/
https://www.jianshu.com/p/0fb563e04f61
https://blog.fundebug.com/2019/05/18/6-ways-for-vue-communication/

Q.E.D.