Rodrick

vuePress-theme-reco Rodrick    2022
Rodrick Rodrick

Choose mode

  • dark
  • auto
  • light
Home
Category
  • CS基础
  • 数据库
  • 前端
  • 其他
Tag
About
Timeline
D&T
  • 官方文档

    • Vue
    • Vue3
    • Webpack
    • MDN
    • Node中文网
    • React
    • 小程序
    • FineReport
  • 学习面试

    • 现代JavaScript教程
    • ES6
    • 阿西河
    • LeetCode
    • 牛客网
  • 工具

    • bejson
Contact
  • Github
  • Gitee
author-avatar

Rodrick

62

Article

18

Tag

Home
Category
  • CS基础
  • 数据库
  • 前端
  • 其他
Tag
About
Timeline
D&T
  • 官方文档

    • Vue
    • Vue3
    • Webpack
    • MDN
    • Node中文网
    • React
    • 小程序
    • FineReport
  • 学习面试

    • 现代JavaScript教程
    • ES6
    • 阿西河
    • LeetCode
    • 牛客网
  • 工具

    • bejson
Contact
  • Github
  • Gitee
  • vue全家桶

    • vue基础
    • vue-cli使用说明
    • vue-router
    • vuex
    • 浅谈组件通讯
    • mapGetters & mapActions
    • mixin(混入)基本使用
    • vuecli 配合 svg-sprite-loader 使用svg
    • 实现一个mini版的Vue
    • 初探vue3

浅谈组件通讯

vuePress-theme-reco Rodrick    2022

浅谈组件通讯

Rodrick 2020-10-31 vue

# 父子组件之间🧑👶

# 1. props[父->子传值] & $emit[子->父发射事件]


父组件通过 :propName='xxxx' 的方式传递值,子组件用 props:{propName:{...}} 进行接收
子组件通过 this.$emit('sonEventName'[,param1[,param2]]) 的形式传出事件名 sonEventName 和参数 param1,param2... ,父组件在自己的子组件标签上用 v-on 绑定: @sonEventName = 'parEventName[(param1[,param2])]' 的方式接收执行。

父组件

<template>
    <div>
        父组件:{{money}}
        <son1 :money='money' @addMoney = 'addMoney'></son1>
    </div>
</template>
<script>
import son1 from './son1'
export default {
    components:{
        son1
    },
    data(){
        return {
            money:100
        }
    },
    methods:{
        addMoney(val){
            this.money = val
        }
    }
}
</script>

子组件

<template>
    <div>
        儿子1获得{{money}}
        <button @click="raiseMoney">多给点嘛</button>
    </div>
</template>
<script>
export default {
    props:{
        money:{
            type:Number,
            default:1
        }  
    },
    methods:{
        raiseMoney(){
            this.$emit('addMoney',200)
        }
    }
}
</script>

# 2. $parent 和 $children/$refs 获取父子元素

  • $parent

在子组件中使用 this.$parent 可以获取到父实例,也可以使用 this.$parent.xxdata = 'xxx' 的方式修改父元素的值

  • $root

另外提一下, $root 是会指向当前组件树的根 Vue 实例。如果当前实例没有父实例,此实例将会是其自己。

  • $children

父元素获取子元素的方式,由于 $children 返回的是一个数组 ,所以根据官方文档所说:

需要注意 $children 并不保证顺序,也不是响应式的。

使用时需要谨慎

$parent 和 $children 是不利于组件复用的,不是非常推荐。
**

  • $refs

$refs 可以给每一个父元素中使用的子组件起别名,就可以精准使用自己需要的子组件 
用法:

// 父组件
<son ref="aSon"/>

mounted(){
  console.log(this.$refs.aSon) //即可拿到子组件的实例,就可以直接操作 data 和 methods
}

# 3. .sync 和 v-model


.sync

  • 需求需要对一个 prop 进行“双向绑定”。不幸的是,真正的双向绑定会带来维护上的问题,因为子组件可以修改父组件,且在父组件和子组件都没有明显的改动来源。
// 父组件
<son1 :money.sync='money'></son1>
//编译时会被扩展为
<son1 :money="money" @update:money="(val)=>this.money=val"></son1>

// 子组件
// 所以子组件可以通过$emit 触发 update 方法改变
  this.$emit('update:money',200)


自定义组件的 v-model

根据官网的介绍:一个组件上的 v-model 默认会利用名为 value 的 prop 和名为 input 的事件,即:

<son1 :value="money" @input="(val)=>this.money=val></son1>

也可以写成

<son1 v-model="money"></son1>

但是 v-model 这种方法,子组件只能接收 value 这一个默认的 prop 具有局限性,而 .sync 可以随意起名字,官方也提供了一个扩展的办法,详见这里

# 祖孙组件之间👴👶

# 1. $attrs 和 $listeners

  1. $attrs

包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)。当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件——在创建高级别的组件时非常有用。

先来看一段代码:

<template>
  <div>
    <p>我是父组件</p>
    <test name="tom" age="12" id="12345" class="child" style="color: red" />
  </div>
</template>

<script>
export default {
  components: {
    test: {
      template: `
      <div>
        <p>我是子组件</p>
        <test2 v-bind="$attrs" s1="sss" s2="sss" />
      </div>`,
      props: ["name"],
      created() {
        console.log(this.$attrs); // {age: 12, id: 12345}
      },
      components: {
        test2: {
          template: `<p>我是孙子组件</p>`,
          props: ["age", "s1"],
          created() {
            console.log(this.$attrs); // {s2: "sss", id: 12345}
          }
        }
      }
    }
  }
};
</script>

我们一层一层来分析:

  • 父级发送给下级 name age id class style 五个参数,由于 class 和 style 不属于范畴

包含了父作用域中不作为 prop 被识别 (且获取) 的 attribute 绑定 (class 和 style 除外)

所以只传下去 name age id

  • 子组件的 props 内已经声明了 name ,所以 this.$attrs 只剩下 {age: 12, id: 12345}
  • 子组件又给孙组件传了 s1 和 s2 ,(可以假定认为此时 this.$attr  )但是孙组件的 props 又声明了 age 和 s1,那么 this.$attrs 只剩下  {s2: "sss", id: 12345} ,而 age 和 id 是父组件传过来的,此时就已经传到孙组件了

也就是说所接受到的属性是不包含class、style、和已经被prop获取的到的,接下来看一下子组件中的配置项inheritAttrs :

默认情况下父作用域的不被认作 props 的 attribute 绑定 (attribute bindings) 将会“回退”且作为普通的 HTML attribute 应用在子组件的根元素上。当撰写包裹一个目标元素或另一个组件的组件时,这可能不会总是符合预期行为。通过设置 inheritAttrs 到 false,这些默认行为将会被去掉。而通过 (同样是 2.4 新增的) 实例 property $attrs 可以让这些 attribute 生效,且可以通过 v-bind 显性的绑定到非根元素上。

翻译成人话就是把 $attr 上的没有在 props 里的值扔到当前组件的根元素上,具体是啥样呢:

<template>
  <div>
    <p>我是父组件</p>
    <test name="tom" age="12" class="child" style="color: red" />
  </div>
</template>

<script>
export default {
  components: {
    test: {
      template: `<p>我是子组件</p>`,
      props: ["name"],
      inheritAttrs: true,  // 默认为 true
      created() {
        console.log(this.$attrs); // {age: "12"}
      }
    }
  }
};
</script>

浏览器解析出来就是这样的:age在根上

<div>
  <p data-v-469af010>我是父组件</p>
  <p data-v-469af010 age="12" class="child" style="color: red;">我是子组件</p>
</div>

如果设置 inheritAttrs: false 或者不设置, age就没了:

<div>
  <p data-v-469af010>我是父组件</p>
  <p data-v-469af010 class="child" style="color: red;">我是子组件</p>
</div>

  1. $listeners

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件——在创建更高层次的组件时非常有用。


让我们看看下面的代码做了什么:

  • 类似于我们使用 $emit ,我们这里在孙组件使用了 this.$listeners.methodName([param1[,param2]]) 的形式,向外发射了一个事件 todo
  • 在子组件 sonButton 里用 v-on="$listeners" 传递这个事件,这个写法类似于 v-bind="$attrs"
  • 在父组件中,用 @methodName="[父组件 method 名]" 来使用,这个和正常父子通信一样的
  • 注意孙组件和子组件的打印内容,都是会完整的拿到父组件绑定的所有方法
<div id="app">
    {{msg}}
    <son-button @todo="handleClick"></son-button>
  </div>

  <script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.7/vue.common.dev.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data: {
        msg: ''
      },
      methods: {
        handleClick(grandsonMsg) {
          this.msg = grandsonMsg
        }
      },
      components: {
        'sonButton': {
          template: `<grand-son-button v-on="$listeners"></grand-son-button>`,
          created() {
            console.log('son listeners',this.$listeners) // 包含父级所有绑定的方法
          },
          components: {
            'grandSonButton': {
              template: `<button @click="grandSonClick">grandSon</button>`,
              created() {
                console.log('grandSon listeners',this.$listeners) // 包含父级所有绑定的方法
              },
              methods: {
                grandSonClick() {
                  this.$listeners.todo('a msg from grandson')
                }
              }
            },
          }
        },
      }
    })
  </script>

运行效果:
WWWW.gif

# 2. provide 和 inject


先看下官网的介绍:

  • provide:Object | () => Object
  • inject:Array<string> | { [key: string]: string | Symbol | Object }
  • 这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

provide 选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的 property。

inject 选项应该是:

  • 一个字符串数组,或
  • 一个对象,对象的 key 是本地的绑定名,value 是:
  • 在可用的注入内容中搜索用的 key (字符串或 Symbol),或
  • 一个对象,该对象的:
  - `from` property 是在可用的注入内容中搜索用的 key (字符串或 Symbol)
  - `default` property 是降级情况下使用的 value


非响应式的数据传值代码演示

// parent 父组件
<template>
  <div>
    <h1 ref="h1">父组件</h1>
    <p @click="changeData">{{ msg }}</p>
    <children ref="children"></children>
  </div>
</template>
<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
      msg: '来自父亲的值'
    };
  },
  methods: {
    changeData() {
      this.msg = '父组件改变了向下传递的msg值';
    }
  },
  provide() {
    return {
      msg: this.msg
    };
  }
};
</script>

// children 子组件
<template>
  <div class="children">
    <h2 ref="h2">子节点</h2>
    <p>{{ msg }}</p>
    <grandson></grandson>
  </div>
</template>
<script>
import Grandson from './grandson';
export default {
  name: 'children',
  components: {
    Grandson
  },
  inject: ['msg']
};
</script>

启动页面,点击父组件p标签,父组件的值已经修改了,向下传递的值并没有修改

如果传入的值为响应式的

<template>
  <div>
    <h1 ref="h1">父组件</h1>
    <p @click="changeData">{{ msg.value }}</p>
    <children ref="children"></children>
  </div>
</template>
<script>
import Children from './children';
export default {
  name: 'praent',
  components: {
    Children
  },
  data() {
    return {
        // 稍微把msg的值改为一个对象
      msg: { value: '来自父亲的值' }
    };
  },
  methods: {
    changeData() {
      this.msg.value = '父组件改变了向下传递的msg值';
    }
  },
  provide() {
    return {
      msg: this.msg
    };
  }
};
</script>
// children 子组件
<template>
  <div class="children">
    <h2 ref="h2">子节点</h2>
    <p>{{ msg.value }}</p>
    <grandson></grandson>
  </div>
</template>
<script>
import Grandson from './grandson';
export default {
  name: 'children',
  components: {
    Grandson
  },
  inject: ['msg']
};
</script>

// grandson 孙组件
<template>
  <div class="grandson">
    <h3>孙组件</h3>
    <p>{{ msg.value }}</p>
  </div>
</template>
<script>
export default {
  name: 'grandson'
  inject: ['msg']
};
</script>

启动页面,此时再去点击父组件的p标签,现在不单单父组件的值更新了,连同子组件和孙组件的值都发生变化了

将孙组件稍微修改一下,定义一个值去存储inject接受到的响应式的值,再通过点击事件去修改值,

<template>
  <div class="grandson">
    <h3>孙组件</h3>
    <p @click="changeMsg">{{ grandsonMsg.value }}</p>
  </div>
</template>
<script>
export default {
  name: 'grandson',
  data() {
    return {
      // 用来保存inject接收到的整个【msg对象】
      grandsonMsg: ''
    }
  },
  mounted(){
		this.grandsonMsg = this.msg
  },
  inject: ['msg'],
  methods:{
    changeMsg(){
      // 改变inject接收到的值,可以改变整条链路上的msg对象的value
      this.grandsonMsg.value = '孙组件修改了msg值'
    }
  }
};
</script>

可以看到,孙组件接受到这个响应式的值,然后通过这种的方式去修改这个值,同样可以修改到父组件provide下来的值

# 其他传递(兄弟节点或长距离)🧒👦

# 1. 事件总线$bus

1.1 场景

这里以项目中实际场景举例:



我想要在GoodsListItem 组件中每一个item 加载完后调用一次Scroll 组件发送给Home 的refresh 事件,但是 GoodsListItem 是GoogsItem 组件的子组件,不能够直接去Home 拿事件,由于组件离得太远,一层层传事件过于麻烦,这时候就用上了事件总线$bus 来管理这个事件

1.2 使用
**

  1. main.js中在Vue原型链上把他加上
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'

Vue.prototype.$bus = new Vue() // event Bus 用于无关系组件间的通信。

Vue.config.productionTip = false

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app')
  1. 下级组件中发射事件


使用this.$bus.$emit('事件名',params)

GoodsListItem.vue

img 标签中使用@load 监听图片加载完成时间

相当于原生js的<img src="xxxxx" onload="imgLoad()">

  <img :src="goodsItem.show.img" alt="" @load="imgLoad"/>
  
  methods:{
    imgLoad(){
      // 这里使用事件总线$bus $bus和vuex区别在于$bus是用于管理事件的监听 vuex主要管理状态
      this.$bus.$emit('itemImgLoad')
    }
  }
  1. 上级组件拿到事件


使用this.$bus.$on('事件名',(params)=>{do sth...})

Home.vue

created() {
    // 3.监听事件总线的图片加载事件
    this.$bus.$on("itemImgLoad", () => {
      // 作用是可以在图片加载后重新计算现在可滚动区域的高度,避免图片还没加载结束就已经算好高度,或者切换类别的时候用上一个类别的高度
      this.$refs.scroll.scroll.refresh();
    });
  },

# 2. vuex

参考 vuex 专门的文章

欢迎来到 Rodrick
看板娘