阅读 2381

Vue整理——组件间的通信

通过阅读了官方文档和大量网友的博文,融入自己的想法,整理出对组件通信的看法,第一次发文,如有不周敬请谅解哦~

进行组件间通信的原因

组件实例的作用域是孤立的,但组件间的联系和交互不可避免。当组件间进行数据交互时,组件通信必不可少。

组件通信的类型

于是组件的通信可以分为两种情况:

父子组件间通信(父传子,子传父),非父子组件通信。



父传子类型


prop传值


step1:父组件用prop自定义属性传递属性,prop有字面量语法和动态语法。字面量只能将数据按字符串的形式传递;动态语法类似于v-bind,可以将父组件数据的实时变化传递给子组件。




step2:子组件需要用props来显式地声明prop。子组件props的写法有两种:

  • 数组形式:


  • 对象形式:可以进行条件约束


子传父

1.回调传参

  1. 父将函数用prop传递给子,子在调用这个函数时,将数据作为参数传递给父组件

    父组件中:

    <child2 :changeParent="changeMyself"></child2>复制代码

    methods: {
        // 父组件改变自身数据的方法
        changeMyself (chidData) {
            this.parentChangeData = chidData
        }
    }复制代码

    子组件中:  

    export default {
      // 子组件显示声明props父组件的函数
      props: {
        changeParent: {
          requried: true, // 代表是否为必传
          type: Function // 代表数据类型
        }
      },
      data () {
        return {
          childData: '我要把数据传给父'
        }
      }
    }复制代码

    <button @click="changeParent(childData)">点我传递数据给父组件</button>复制代码

2.自定义事件+事件监听

子$emit自定义一个事件,第一个参数为事件名,第二个参数为数据。父v-on绑定一个事件监听器,在绑定的method中以参数形式获得子组件的数据。

子组件中:

methods: {
    // 自定义事件$emit暴露数据,数据作为参数
    changeParent () {
      this.$emit('change', this.childData)
    }
}复制代码

父组件中:

<!-- 父组件绑定监听,得到子组件数据-->
<child3 @change="changeMyself"></child3>复制代码

methods: {
    // 父组件改变自身数据的方法
    changeMyself (chidData) {
        this.parentChangeData = chidData
    }
}复制代码

3.直接访问

用ref对子组件进行标记,父组件直接通过this.$refs[子组件ref].[子组件属性/方法]来获得子组件的数据。

注意:"$refs 只在组件渲染完成后才填充,并且它是非响应式的。它仅仅作为一个直接访问子组件的应急方案——应当避免在模版或计算属性中使用 $refs 。" 官网这么说的,只是应急用,一般不推荐这样使用

思考:单项数据流

Prop 是单向绑定的:当父组件的属性变化时,将传导给子组件,但是反过来不会。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。

另外,每次父组件更新时,子组件的所有 prop 都会更新为最新值。这意味着你不应该在子组件内部改变 prop。如果你这么做了,Vue 会在控制台给出警告。

在两种情况下,我们很容易忍不住想去修改 prop 中数据:

  1. Prop 作为初始值传入后,子组件想把它当作局部数据来用;

  2. Prop 作为原始数据传入,由子组件处理成其它数据输出。

对这两种情况,正确的应对方式是:

  1. 定义一个局部变量,并用 prop 的值初始化它:


  2. 定义一个计算属性,处理 prop 的值并返回:


思考:子与父的双向绑定

单项数据流导致子组件不能直接去改动父组件的数据。但真实场景中,有很多时候需要在子组件中更改父组件的数据。

方法:

  1. 父有一个改变其自身数据的方法,用prop传给子,子在需要改变父组件数据的时候调用该方法。(与回调参数方法一致)

  2. 子组件通过事件发送,$emit事件将数据传给父组件,父组件监听后自己执行改变自身数据的方法。(与子传父的第二种方法一致)

  3. 自己想到的方法:父组件有一个改变自身数据的方法,子通过this.$parent.[父组件方法]直接调用父组件方法,将子组件的数据作为参数传递给父组件从而改变父组件数据

    1. 父组件中:

      methods: {
          // 父组件改变自身数据的方法
          changeMyself (chidData) {
              this.parentChangeData = chidData
          }
      }复制代码

      子组件中:

      export default {
        data () {
          return {
            childData: '我是子要传给父的数据'
          }
        },
        methods: {
          change () {
            // 通过直接访问的方法调用
            this.$parent.changeMyself(this.childData)
          }
        }
      }复制代码

    但以上的方法都是基于事件触发的,不能保证子父组件的数据时刻同步

    4.使用.sync绑定修饰符在父组件prop中显示强调双向绑定。子组件在watch中使用$emit(‘update:data’,newVal)监听,更新父组件prop传过来的数据,父组件不需要再监听该update方法。


    父组件中:

    <!-- 显式绑定.sync修饰符 -->
    <child6 :parentData.sync= "parentData"></child6>复制代码

    子组件中:

    props: ['parentData']复制代码

    // 子组件进行数据监听
    watch: {
        childData (newVal, oldVal) {
          this.$emit('update:parentData', newVal)
        }
    }复制代码

这里的自定义事件与子传父的自定义事件有一些不同。

<child6 :parentData.sync="parentData"></child6>

会被扩展为:

<child6 :parentData"parentData" @update:parentData="val => parentData = val"></child6>

思考:

自定义事件发生时候运行的响应表达式是<child6 :parentData="parentData" @update:parentData="val => parentData = val"></child6>中的 "val => bar = val"

在子传父的“通过$emit事件从子组件向父组件中传递数据” 里,自定义事件发生时候运行的响应表达式是:<child @chang="changeMyself"></child>中的changeMyself

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

对后者, changeMyself是在父组件中定义的, 在这个函数里, 可以对从子组件接受来的数据做任意的操作或处理, 决定权完全落在父组件中, 也就是: 父可以改变子(数据), 但子不能直接改变父(数据)!, 父中数据的变动只能由它自己决定。

有一个投机取巧的办法:在父用prop传数据给子时,若子组件中props的类型为对象或数组时,可以直接在子组件中修改这个prop来的数据,且不会被vue检测报错,但这样会使得数据流变得更加难以分析,同时,当props的类型为引用数据类型时,要注意在子组件中对对象进行深拷贝,防止隐性修改父组件的对象。


兄弟传兄弟(非父子)

1.两个兄弟组件有共同的父组件

父组件将数据prop给一个兄弟A,将改变数据的方法prop给另一个兄弟B,B调用方法改变A的数据,将数据和改变数据提升到了父组件内部,然后再分发下去。

父组件中:

<!-- 父组件把改变数据的方法传给child2,把数据传给child,将数据通信提升到父组件层次 -->
<child2 :changeParent="changeMyself"></child2>
<child :parent="parentChangeData"></child>复制代码

child2中:

<!-- 子组件调用方法,将数据作为参数 -->
<button @click="changeParent(childData)">点我传递数据给父组件</button>复制代码

export default {
  // 子组件显示声明props父组件的函数
  props: {
    changeParent: {
      requried: true, // 代表是否为必传
      type: Function // 代表数据类型
    }
  },
  data () {
    return {
      childData: '我要把数据传给父'
    }
  }
}复制代码

child中:

// 显示声明props父组件的数据
props: {
    parent: {
      requried: true, // 代表是否为必传
      type: String, // 代表数据类型
      default: '我是默认值'
    }
}复制代码

2.路由传参

把需要跨页面传递的数据放到url后面,跳转到另外页面时直接获取url字符串获取想要的参数即可。

{
    path: '/params/:id'
    name: '',
    component: Sample
}复制代码

<router-link :to="params/12">跳转路由</router-link>复制代码

在跳转后的组件中用$route.params.id去获取到这个id参数为12,但这种只适合传递比较小的数据,数字之类的。

3.EventBus

首先创建一个中央时间总线,在需要使用的组件里引入。组件A用this.Bus.$emit('eventName', value)触发事件,组件B用this.Bus.$on('eventName', value => { this.print(value) })接收事件。在$emit之前,必须已经$on, 因此普遍采用在created钩子中进行$on.


// 新建Bus.js文件
import Vue from 'vue'
export default new Vue()复制代码

组件A传送数据:

<script>
// 在需要使用的组件中引用
import Bus from '@/components/Bus'
export default {
  data () {
    return {
      childData: '我是兄弟A,把我的数据放进eventBus'
    }
  },
  methods: {
    submit () {
      // 触发事件
      Bus.$emit('change', this.childData)
    }
  }
}
</script>复制代码

组件B接收数据:

get () {
  // 监听接收事件
  Bus.$on('change', value => {
    this.myData = value
  })
}复制代码

// 组件销毁时,解除绑定
destroyed () {
    Bus.$off('change')
}复制代码


组件间的通信都可以用EventBus实现, 无论有多少个组件,只要保证eventName不一样就可以了,专门设置一个空的Bus实例来作为中央事件总线,而不是直接访问root,更清晰也更利于管理。

特殊的eventBus

传统的eventBus只负责$emit和$on,与数据没有任何的交集。这使得数据不是“长效”的,只在$emit后生效,并且同一组件多次生成会多次$on,路由切换时,还要考虑新组件的绑定和旧组件的解除绑定。

方法:考虑将$on放在Bus中完成,修改Bus为:

// 新建Bus.js文件
// Bus进行监听,将数据直接放在Bus中
import Vue from 'vue'
const Bus = new Vue({
  data () {
    return {
      child7Val: ''
    }
  },
  created () {
    this.$on('change', value => {
      this.child7Val = value
    })
  }
})
export default Bus复制代码


发出数据的组件不变

<script>
// 在需要使用的组件中引用
import Bus from '@/components/Bus'
export default {
  data () {
    return {
      childData: '我是兄弟A,把我的数据放进eventBus'
    }
  },
  methods: {
    submit () {
      // 触发事件
      Bus.$emit('change', this.childData)
    }
  }
}
</script>复制代码


接收数据的组件修改为用计算属性直接从Bus中存取数据,使用计算属性是为了保证数据的动态性。

computed: {
    child7Val () {
      return Bus.child7Val
    }
 }复制代码


Vuex

我的理解是,Vuex相当于一个大型的专门用来储存共享变量的仓库。

在安装Vuex之后,创建store.js文件

import Vue from 'vue'
import Vuex from 'vuex'

import app from './modules/app'

Vue.use(Vuex)

const store = new Vuex.Store({
  state: {
    // 存放共享变量
    msg: '我是原始数据'
  },
  getter: {
    // 相当于store中的计算属性
    mymsg: state => {
      return state.msg
    }
  },
  mutations: {
    // 修改state,vue推荐使用大写
    MUTATIONSMSG (state, payload) {
      state.msg = payload.msg
    }
  },
  actions: {
    // 与mutations类似,支持异步
    mutationsMsg (context, payload) {
      context.commit('MUTATIONSMSG', payload)
    }
  },
  modules: {
    app
  },
  strict: process.env.NODE_ENV !== 'production'
})

export default store复制代码

当使用mutations来修改state时:

组件A使用store中的数据:

<template>
  <div class="child">
    <h3>组件A</h3>
    {{$store.state.msg}}
  </div>
</template>复制代码

组件B修改store中的数据:

<script>
export default {
  data () {
    return {
      myData: '组件B的数据'
    }
  },
  methods: {
    get () {
      this.$store.commit('MUTATIONSMSG', this.myData)
    }
  }
}
</script>复制代码

当使用actions修改state时:

<script>
export default {
  data () {
    return {
      myData: '组件B的数据'
    }
  },
  methods: {
    get () {
      this.$store.dispatch('mutationsMsg', this.myData)
    }
  }
}
</script>复制代码

state用来存放共享变量,通过this.$store.state.[变量]来获取共享变量的值。

getter,可以增加一个getter派生状态,(相当于store中的计算属性),store.getters.方法名()用来获得共享变量的值。

mutations用来存放修改state的方法(相当于set)。

actions也是用来存放修改state的方法,不过action是在mutations的基础上进行。在actions先commit mutations里的方法,再由使用actions的组件进行dispatch


思考

  • 组件化的思想就是希望组件的独立性,数据能互不干扰

  • 通过组件A直接去修改组件B的值,比如双向绑定,虽然方便,但增加了组件间的耦合性。最好就是如果组件A要修改组件B的值,那么就将修改的数据暴露出去,由B得到数据后,自行修改。




文章分类
前端
文章标签