欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Vue组件通讯黑科技

程序员文章站 2022-10-05 10:28:15
Vue组件通讯 组件可谓是 Vue框架的最有特色之一, 可以将一大块拆分为小零件最后组装起来。这样的好处易于维护、扩展和复用等。 提到 Vue的组件, 相必大家对Vue组件之间的数据流并不陌生。最常规的是父组件的数据通过 prop传递给子组件。子组件要进行数据更新,那么需要通过自定义事件通知父组件。 ......

vue组件通讯

组件可谓是 vue框架的最有特色之一, 可以将一大块拆分为小零件最后组装起来。这样的好处易于维护、扩展和复用等。

提到 vue的组件, 相必大家对vue组件之间的数据流并不陌生。最常规的是父组件的数据通过 prop传递给子组件。子组件要进行数据更新,那么需要通过自定义事件通知父组件。

那么还有其他方法, 方便父子组件乃至跨组件之间的通讯吗?

props $emit

可以通过 prop属性从父组件向子组件传递数据

// child.vue
vue.component('child', {
    props: ['msg'],
    template: `<div>{{ msg }}</div>`
})


// parent.vue
<div>
    <child :msg="message"></child>
<div>
// 部分省略...
{
  data(){
    return {
      message: 'hello.child'
    }
  }
}

以上代码父子组件通讯是通过 prop的传递, vue是单向数据流, 子组件不能直接修改父组件的数据。可以通过自定义事件通知父组件修改,和要修改的内容

provide / inject

provide 和 inject 主要为高阶插件/组件库提供用例。并不推荐直接用于应用程序代码中。但是我们如果合理的运用, 那还是非常方便的

provide 应该是一个对象, 或者是一个函数返回一个对象。它主要的作用就是给应用provide的组件的子孙组件提供注入数据。就相当于 vuex的store,用来存放数据。

inject: 给当前的组件注入 provide提供的数据。切记 provideinject 绑定并不是可响应的。

看一下这两个怎么使用

a组件给子组件b提供数据

  // a.vue
  export default {
    provide: {
      name: 'qiqingfu'
    }
  }
  
  // b.vue
  export default {
    inject: ['name'],
    mounted(){
      console.log(this.name)  // qiqingfu
    }
  }

以上代码父组件a提供了 name: qiqingfu的数据, 子组件和子孙组件如果需要就通过 inject注入进来就可以使用了

provide / inject 替代 vuex存储用户的登录数据实例
这里并不是一定要替代 vuex, 介绍另一种可行性

我们在使用 webpack进行构建 vue项目时, 都会有一个入口文件 main.js, 里面通常都导入一些第三方库如 vuexelementvue-router等。但是我们也引入了一个 vue的根组件 app.vue。简单的app.vue是这样子的

  <template>
     <div>
       <router-view></router-view>
     </div>
  </template>
  <script>
    export default {
      
    }
    </script>

这里的 app.vue 是一个最外层的组件, 可以用来存储一些全局的数据和状态。因为组件的解析都是以app.vue作为入口。引入你项目中所有的组件,它们的共同根组件就是app.vue。所以我们可以在 app.vue中将自己暴露出去

  <template>
  <div>
    <router-view></router-view>
  </div>
</template>
<script>
  export default {
    provide () {
      return {
        app: this
      }
    }
  }
</script>

以上代码把整个 app.vue实例的 this对外提供, 并且命令为 app。那么子组件使用时只需要访问或者调用 this.app.xxx或者访问 app.vue的 datacomputed等。

因为 app.vue只会渲染一次, 很适合做一些全局的状态数据管理, 比如用户的登录信息保存在 app.vuedata中。

app.vue

<template>
  <div>
    <router-view></router-view>
  </div>
</template>
<script>
  export default {
    provide () {
      return {
        app: this
      }
    },
    data: {
      userinfo: null
    },
    mounted() {
      this.getuserinfo()
    },
    methods: {
      async getuserinfo() {
        const result = await axios.get('xxxxxx')
        if (result.code === 200) {
          this.userinfo = result.data.userinfo
        }
      }
    }
  }
</script>

以上代码,通过接口获取用户对数据信息, 保存在 app.vue的data中。而且provide将实例提供给任何子组件使用。所以任何页面和子组件都可以通过 inject注入 app。并且通过 this.app.userinfo来取得用户信息。

那么如果用户要修改当前的信息怎么办? app.vue只初始化一次呀?

  // header.vue
  <template>
    <div>
      <p>用户名: {{ app.userinfo.username }}</p>
    </div>
  <template>

  <script>
    export default {
      name: 'userheader',
      inject: ['app'],
      methods: {
        async updateusername() {
          const result = await axios.post(xxxxx, data)
          if (result === 200) {
            this.app.getuserinfo()
          }
        }
      }
    }
  </script>

以上代码, 在header.vue组件中用户修改了个人信息, 只需要修改完成之后再调用一次 app.vue根组件的 getuserinfo方法, 就又会同步最新的修改数据。

dispatch / broadcast

父子组件通讯的方法。可以支持:

  • 父 - 子 传递数据 (广播)
  • 子 - 父 传递数据 (派发)

先聊一下 vue实例的方法 $emit()$on()

$emit: 触发当前实例上的事件。附加参数都会传给监听器回调。

$on: 监听当前实例上的事件

也就是一个组件向父组件通过 $emit自定义事件发送数据的时候, 它会被自己的 $on方法监听到

   
   // child.vue
   export default {
     name: 'child',
     methods: {
       handleevent() {
         this.$emit('test', 'hello, 这是child.vue组件')
       }
     },
     mounted() {
       // 监听自定义事件
       this.$on('test', data => {
         console.log(data)  // hello, 这是child.vue组件
       })
     }
   }

   // parent
   <template>
    <div>
      <child v-on:test="handletest"></child>
    </div>
   <template>
   
   <script>
    export default {
      methods: {
        handletest(event) {
          console.log(event) // hello, 这是child.vue组件
        }
      }
    }
   </script>

以上代码, $on 监听自己触发的$emit事件, 因为不知道何时会触发, 所以会在组件的 createdmounted钩子中监听。

看起来多此一举, 没有必要在自己组件中监听自己调用的 $emit。 但是如果当前组件的 $emit在别的组件被调用, 并且传递数据的话那就不一样了。

举个例子

  // child.vue 
   export default {
     name: 'ichild',
     methods: {
       sayhello() {
         // 如果我在子组件中调用父组件实例的 $emit
         this.$parent.$emit('test', '这是子组件的数据')
       }
     }
   }

   // parent.vue
   export default = {
     name: 'iparent',
     mounted() {
       this.$on('test', data => {
         console.log(data) // 这是子组件的数据
       })
     }
   }

以上代码, 在子组件调用父组件实例的 $emit方法, 并且传递响应的数据。那么在父组件的 mounted钩子中可以用 $on监听事件。

如果这样写肯定不靠谱, 所以我们要封装起来。哪个子组件需要给父组件传递数据就将这个方法混入(mixins)到子组件

dispatch 封装

    // emitter.js
   export default {
     methods: {
       dispatch(componentname, eventname, params) {
         let parent = context.$parent || context.$root
         let name = parent.$options.name

         while(parent && (!name || name !== componentnamee)) {
           parent = parent.$parent
           if (parent) {
             name = parent.$options.name
           }
         }
         if (parent) {
           parent.call.$emit(parent, eventname, params)
         }
       }
     }
   }

以上代码对 dispatch进行封装, 三个参数分别是 接受数据的父组件name自定义事件名称传递的数据

对上面的例子重新修改

  import emitter from './emitter'
  // child.vue 
   export default {
     name: 'ichild',
     mixins: [ emitter ],  // 将方法混入到当前组件
     methods: {
       sayhello() {
         // 如果我在子组件中调用父组件实例的 $emit
         this.dispatch('iparent', 'test', 'hello,我是child组件数据')
       }
     }
   }

   // parent.vue
   export default = {
     name: 'iparent',
     mounted() {
       this.$on('test', data => {
         console.log(data) // hello,我是child组件数据
       })
     }
   }

以上代码, 子组件要向父组件传递数据。可以将先 emitter混入。然后再调用 dispatch方法。第一个参数是接受数据的父组件, 也可以是爷爷组件, 只要 name值给的正确就可以。然后接受数据的组件需要通过 $on来获取数据。

broadcast

广播是父组件向所有子孙组件传递数据, 需要在父组件中注入这个方法。实现如下:

  // emitter.js
  export default {
    methods: {
      broadcast(componentname, eventname, params) {
       this.$children.foreach(child => {
         let name = child.$options.name
         if (name === componentname) {
           child.$emit.apply(child, [eventname].concat(params))
         } else {
           // 递归
           broadcast.apply(child, [componentname, eventname].concat([params]))
         }
       })
      }
    }
  }

以上代码是通过递归匹配子组件的 name, 如果没有找到那么就递归寻找。找到之后调用子组件实例的 $emit()方法并且传递数据。

使用:

   // 儿子组件 child.vue
   export default {
     name: 'ichild',
     mounted() {
       this.$on('test', data => {
         console.log(data)
       })
     }
   }

   // 父亲组件 parent.vue 
   <template>
      <div>
        <child></child>  
      </div>
   <template>
   export default {
     name: 'iparent',
     components: {
       child
     }
   }

   // 爷爷组件 root.vue
   <template>
    <div>
      <parent></parent>
    </div>
   <template>
   import emitter from './emitter'
   export default {
     name: 'iroot',
     mixin: [ emitter ],
     components: {
       parent
     },
     methods: {
       this.broadcast('ichild', 'test', '爷爷组件给孙子传数据')
     }
   }

以上代码, root根组件给孙组件(child.vue)通过调用 this.broadcast找到对应name的孙组件实例。child.vue只需要监听 test事件就可以获取数据。

找到任意组件实例的方法

这些方法并非 vue组件内置, 而是通过自行封装, 最终返回你要找的组件的实例,进而可以调用组件的方法和函数等。

使用场景:

  • 由一个组件,向上找到最近的指定组件
  • 由一个组件,向上找到所有的指定组件
  • 由一个组件,向下找到最近的指定组件
  • 由一个组件,向下找到所有的指定组件
  • 由一个组件,找到指定组件的兄弟组件

找到最近的指定组件

findcomponentupwarp(context, componentname)

  export function findcomponentupwarp(context, componentname) {
    let parent = context.$parent
    let name = parent.$options.name

    while(parent && (!name || name !== componentname)) {
      parent = parent.$parent
      if (parent) {
        name = parent.$options.name
      }
    }
    return parent
  }

这个函数接受两个参数,分别是当前组件的上下文(this),第二个参数是向上查找的组件 name。最后返回找到组件的实例。

向上找到所有的指定组件

findcomponentsupwarp(context, componentname)
return array

  export function findcomponentsupwarp(context, componentname) {
    let parent = context.$parent
    let result = []
    if (parent) {
      if (parent.$options.name === componentname) result.push(parent)
      return result.concat(findcomponentsupwarp(parent, componentname))
    } else {
      return []
    }
  }

这个函数接受两个参数,分别是当前组件的上下文(this),第二个参数是向上查找的组件 name。最后返回一个所有组件实例的数组

向下找到最近的指定组件

findcomponentdownwarp(context, componentname)

  export function findcomponentdownwarp(context, componentname) {
    let resultchild = null
    context.$children.foreach(child => {
      if (child.name === componentname) {
         resultchild = child
         break
      } else {
        resultchild = findcomponentdownwarp(child, componentname)
        if (resultchild) break
      }
    })
    
    return resultchild
  }

以上代码接受两个参数, 当前组件的上下文(this)和向下查找的组件name。返回第一个name和componentname相同的组件实例

向下找到所有的指定组件

findcomponentsdownwarp(context, componentname)

  export function findcomponentsdownwarp(context, componentname) {
    return context.$children.reduce((resultchilds, child) => {
      if (child.$options.name === componentname) resultchilds.push(child)
      // 递归迭代
      let foundchilds = findcomponentsdownwarp(child, componentname)
      return resultchilds.concat(foundchilds)
    }, [])
  }

以上代码接受两个参数, 当前组件的上下文(this)和向下查找的组件name。返回一个所有组件实例的数组

找到当前组件的其他兄弟组件

findbrotherscomponents(context, componentname, exceptme?)

exceptme默认为true, 排除它自己, 如果设置为false则包括当前组件

  export function findbrotherscomponents(context, componentname, exceptme = true) {
    let res = context.$parent.$children.filter(child => child.$options.name === componentname)

    // 根据唯一表示_uid找到当前组件的下标
    let index = res.findindex(item => item._uid === context._uid)
    if (exceptme) res.splice(index, 1)
    return res
  }

以上代码通过第一个参数的上下文, 拿到它父组件中的所有子元素,最后根据 exceptme决定是否排除自己。