Vue组件通信的几种方式

153 阅读3分钟

[toc]

父子组件

* props & emit

父组件通过绑定数据,向子组件传递数据
子组件通过自定义事件,向父组件传递数据
  • parent.vue
<template>
  <div>
    <Child1 :user="user" @showMsg="showMsg"/>
    <Child2 :user="user" @showMsg="showMsg"/>
  </div>
</template>
<script>
import Child1 from './Child1'
import Child2 from './Child2'
export default {
  components: {
    Child1,
    Child2,
  },
  data() {
    return {
      user: {
        userName: 'james'
      }
    }
  },
  methods: {
    showMsg(msg) {
      console.log(msg)
    }
  },
}
</script>
  • child1.vue
<template>
  <div>
    <span>Child1 recive: {{ user.userName }}</span>
    <button @click="change">修改</button>
  </div>
</template>
<script>
export default {
  props: {
    user: Object
  },
  // emits: ['showMsg'], // vue3
  methods: {
    change() {
      this.$emit('showMsg', "Child1 要修改username")
    }
  }
}
</script>
  • child2.vue
<template>
  <div>
    <span>Child2 recive: {{ user.userName }}</span>
    <button @click="change">修改</button>
  </div>
</template>
<script>
export default {
  props: {
    user: Object
  },
  // emits: ['showMsg'], // vue3
  methods: {
    change() {
      this.$emit('showMsg', "Child2 要修改username")
    }
  }
}
</script>

parent & refs mounted生命周期:获取

  • Parent.vue
<template>
  <div>
    <ChildLevel1 :user="user" @getUser="getUser"/>
  </div>
</template>
<script>
import ChildLevel1 from './ChildLevel1'
export default {
  components: {
    ChildLevel1,
  },
  data() {
    return {
      user: {
        userName: 'james'
      }
    }
  },
  methods: {
    getUser() {
      console.log(this.user)
    }
  },
}
</script>
  • ChildLevel1.vue
<template>
  <div>
    ChildLevel1
    <ChildLevel2 ref="ChildLevel2" />
  </div>
</template>
<script>
import ChildLevel2 from './ChildLevel2'
export default {
  components: {
    ChildLevel2,
  },
  props: {
    user: Object
  },
  mounted() {
    console.log(this.$parent.user)
    console.log(this.$parent.getUser())
    console.log(this.$refs.ChildLevel2.name)
    console.log(this.$refs.ChildLevel2.getName())
  },
}
</script>
  • ChildLevel2.vue
<template>
  <div>ChildLevel2</div>
</template>
<script>
export default {
  props: {
    user: Object
  },
  data() {
    return {
      name: 'ChildLevel2'
    }
  },
  methods: {
    getName() {
      return this.name
    },
  },
}
</script>

slot

就是把子组件的数据通过插槽的方式传给父组件使用,然后再插回来
// Child.vue
<template>
    <div>
        <slot :user="user"></slot>
    </div>
</template>
export default{
    data(){
        return {
            user:{ name:"xxx" }
        }
    }
}
 
// Parent.vue
<template>
    <div>
        <child v-slot="slotProps">
            {{ slotProps.user.name }}
        </child>
    </div>
</template>

上下级组件(跨多级)通讯

$attr 作为emit和props的候补,created生命:接受&透传

# 接收
  ps: vue3移除 $listeners(emit方法接收),合并至$attrs
props: ['a']
emits: ['getA']
created(){
  console.log(Object.keys(this.$attrs))
}

# 透传
v-bind="$attrs" 跨层级传递

# 细节
dom节点隐藏属性
  inheritAttrs:false
  当根子节点有多个时,默认
    inheritAttrs:true

# ps:
  多层级透传,依赖v-bind,并不方便
  • level1.vue
<template>
    <p>Level1</p>
    <Level2
        :a="a"
        :b="b"
        :c="c"
        @getA="getA" 
        @getB="getB"
        @getC="getC"
    ></Level2>
</template>

<script>
import Level2 from './Level2'

export default {
    name: 'Level1',
    components: { Level2 },
    data() {
        return {
            a: 'aaa',
            b: 'bbb',
            c: 'ccc'
        }
    },
    methods: {
        getA() {
            return this.a
        },
        getB() {
            return this.b
        },
        getC() {
            return this.c
        }
    }
}
</script>
  • level2.vue
<template>
    <p>Level2</p>
    <Level3
        :x="x"
        :y="y"
        :z="z"
        @getX="getX"
        @getY="getY"
        @getZ="getZ"
        v-bind="$attrs"
    ></Level3>
</template>

<script>
import Level3 from './Level3'

export default {
    name: 'Level2',
    components: { Level3 },
    props: ['a'],
    emits: ['getA'],
    data() {
        return {
            x: 'xxx',
            y: 'yyy',
            z: 'zzz'
        }
    },
    methods: {
        getX() {
            return this.x
        },
        getY() {
            return this.y
        },
        getZ() {
            return this.z
        }
    },
    created() {
        // console.log('level2', Object.keys(this.$attrs)) // 是 props 和 emits 后补
    },
}
</script>
  • level3.vue
<template>
    <p>Level3</p>

    <!-- <HelloWorld msg="hello 双越" ref="hello1"/> -->
</template>

<script>
// import HelloWorld from '@/components/HelloWorld.vue'

export default {
    name: 'Level3',
    // components: { HelloWorld },
    props: ['x'],
    emits: ['getX'],
    // inheritAttrs: false,
    data() {
        return {
        }
    },
    created() {
        // console.log('level3', Object.keys(this.$attrs)) // 是 props 和 emits 后补
    },
    mounted() {
        // console.log(this.$parent.getX())
        console.log(this.$refs.hello1.name)
    },
}
</script>

* Provide & Inject

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。 子孙组件通过 inject 注入获取到祖级和父级 provide 的数据。

    注意:provide 和 inject 绑定并不是可响应的。这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。
  • Level1.vue
<template>
    <p>Level1: <input v-model="name"></p>

    <Level2></Level2>
</template>

<script>
import { computed } from 'vue'
import Level2 from './Level2'

export default {
    name: 'Level1',
    components: { Level2 },
    data() {
        return {
            name: '王强'
        }
    },
    // provide: {
    //     info: 'aaa'
    // }
    provide() {
        return {
            info: computed(() => this.name)
        }
    }
}
</script>
  • Level2.vue
<template>
    <p>Level2 {{ info.value }}</p>
    <Level3></Level3>
</template>

<script>
import Level3 from './Level3'

export default {
    name: 'Level2',
    components: { Level3 },
    inject: ['info']
}
</script>
  • Level3.vue
<template>
    <p>Level3 {{ info.value }}</p>
    <!-- <button @click="changeProvide">修改Provide值</button> -->
</template>

<script>
export default {
    name: 'Level3',
    inject: ['info'],
    // methods: {
    //     // ***待验证:改Provide值
    //     changeProvide() {
    //         console.log('this.info.value', this.info.value)
    //         this.info.value = 'aaaa'
    //     }
    // }
}
</script>

全局组件

eventBus 事件总线机制-自定义事件中心,基于发布订阅模式,兄弟组件&跨级组件

同级传参或者隔级传参可以用eventBus(事件车),内部也是发布订阅模式实现的,适合于非常简单的小项目,一般不用

注意:
  组件销毁时,要清除自定义事件防止内存泄漏

vue2

创建一个vue的实例,然后给每个子组件绑定一个方法(触发时候发布eventBus),在 每个子组件做一个订阅的监控,触发绑在created里的方法执行,靠传递参数的不同实现同步数据

vue3 引入第三方的自定义事件

event-emitter

* vuex

Vuex介绍

    Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化.

    Vuex 解决了多个视图依赖于同一状态和来自不同视图的行为需要变更同一状态的问题,将开发者的精力聚焦于数据的更新而不是数据在组件之间的传递上

Vuex各个模块

    state:用于数据的存储,是store中的唯一数据源
    getters:如vue中的计算属性一样,基于state数据的二次包装,常用于数据的筛选和多个数据的相关性计算
    mutations:类似函数,改变state数据的唯一途径,且不能用于处理异步事件
    actions:类似于mutation,用于提交mutation来改变状态,而不直接变更状态,可以包含任意异步操作
    modules:类似于命名空间,用于项目中将各个模块的状态分开定义和操作,便于维护

Vuex实例应用

这里我们先新建 store文件夹, 对Vuex进行一些封装处理
  • 在 store 文件夹下添加 index.js 文件
// index.js
 
// 自动挂载指定目录下的store
import Vue from 'vue'
import Vuex from 'vuex'
 
Vue.use(Vuex)
 
let modules = {}
 
// @/store/module 目录下的文件自动挂载为 store 模块
const subModuleList = require.context('@/store/modules', false, /.js$/)
subModuleList.keys().forEach(subRouter => {
  const moduleName = subRouter.substring(2, subRouter.length - 3)
  modules[moduleName] = subModuleList(subRouter).default
})
 
export default new Vuex.Store({
  state: {},
  mutations: {},
  actions: {},
  modules
})
  • 在 store 文件夹下添加 module 文件夹,在module文件夹再新建 user.js 文件
// user.js
 
import user from '@/utils/user.js'
import userApi from '@/apis/user'
import { OPEN_ACCOUNT_STAGE, STAGE_STATUS } from '@/constant'
 
let getUserPromise = null
 
export default {
  namespaced: true,
  state() {
    return {
      userInfo: null, // 用户信息
      isLogined: !!user.getToken(), // 是否已经登录
    }
  },
  mutations: {
    // 更新用户信息
    updateUser(state, payload) {
      state.isLogined = !!payload
      state.userInfo = payload
    },
  },
  actions: {
    // 获取当前用户信息
    async getUserInfo(context, payload) {
      // forceUpdate 表示是否强制更新
      if (context.state.userInfo && !payload?.forceUpdate) {
        return context.state.userInfo
      }
      if (!getUserPromise || payload?.forceUpdate) {
        getUserPromise = userApi.getUserInfo()
      }
      // 获取用户信息
      try {
        const userInfo = await getUserPromise
        context.commit('updateUser', userInfo)
      } finally {
        getUserPromise = null
      }
      return context.state.userInfo
    },
 
    // 登出
    async logout(context, payload = {}) {
      // 是否手动退出
      const { manual } = payload
      if (manual) {
        await userApi.postLogout()
      }
      user.clearToken()
      context.commit('updateUser', null)
    },
  }
}
  • 然后在项目的 main.js 文件中引入
import Vue from 'vue'
import App from '@/app.vue'
import { router } from '@/router'
import store from '@/store/index'
 
const vue = new Vue({
  el: '#app',
  name: 'root',
  router,
  store,
  render: h => h(App),
})
  • 封装好后了,然后就正常操作即可。
this.$store.state.user.isLogined
this.$store.state.user.userInfo
this.$store.commit('user/updateUser', {})
await this.$store.dispatch('user/logout', { manual: true })

其他

localStorage / sessionStorage

这种通信比较简单,缺点是数据和状态比较混乱,不太容易维护。

    通过window.localStorage.getItem(key)获取数据
    通过window.localStorage.setItem(key,value)存储数据

注意用JSON.parse() / JSON.stringify() 做数据格式转换, localStorage / sessionStorage可以结合vuex, 实现数据的持久保存,同时使用vuex解决数据和状态混乱问题.

通过 $root 访问根实例

通过 $root,任何组件都可以获取当前组件树的根 Vue 实例,通过维护根实例上的 data,就可以实现组件间的数据共享。

注意:通过这种方式,虽然可以实现通信,但在应用的任何部分,任何时间发生的任何数据变化,都不会留下变更的记录,这对于稍复杂的应用来说,调试是致命的,不建议在实际应用中使用。
//main.js 根实例
new Vue({
    el: '#app',
    store,
    router,
    // 根实例的 data 属性,维护通用的数据
    data: function () {
        return {
            author: ''
        }
    },
    components: { App },
    template: '<App/>',
});
 
 
<!--组件A-->
<script>
export default {
    created() {
        this.$root.author = '于是乎'
    }
}
</script>
 
 
<!--组件B-->
<template>
    <div><span>本文作者</span>{{ $root.author }}</div>
</template>

自己实现简单的 Store 模式

对于小型的项目,通信十分简单,这时使用 Vuex 反而会显得冗余和繁琐,这种情况最好不要使用 Vuex,可以自己在项目中实现简单的 Store。

上面代码原理就是,store.js文件暴露出一个对象 store,通过引入 store.js 文件 各个页面来共同维护这个store对象

和 Vuex 一样,store 中 state 的改变都由 store 内部的 action 来触发,并且能够通过 console.log() 打印触发的痕迹。这种方式十分适合在不需要使用 Vuex 的小项目中应用。

与 $root 访问根实例的方法相比,这种集中式状态管理的方式能够在调试过程中,通过 console.log() 记录来确定当前变化是如何触发的,更容易定位问题。
// store.js
const store = {
  debug: true,
  state: {
    author: 'yushihu!'
  },
  setAuthorAction (newValue) {
    if (this.debug) console.log('setAuthorAction triggered with', newValue)
    this.state.author = newValue
  },
  deleteAuthorAction () {
    if (this.debug) console.log('deleteAuthorAction triggered')
    this.state.author = ''
  }
}
export default store
  • 参考博文

vue组件间通信的13种方式 Vue组件之间的通信 Vue组件通信的几种方式 彻底理解Vue组件间通信(6种方式) Vue实现组件间通信的几种方式(多种场景)