# Vue2.x 的通信

前言

介绍 Vue2.x 中 17 种组件之间的通信方式,在开发时灵活运用不同的通信方式,找到最佳应用场景

# 1.父子组件通信

# 1.1 props

可以实现向子组件传递数据,也可以向父组件传递数据

# 1.1.1 基本数据类型

子组件 props 为响应式数据,data 接收后失去响应

  • 案例 1:子组件不修改 props 值

    刷新
    全屏/自适应

    可以得出结论:props 为响应式数据,父组件传递的数据修改,子组件会跟着修改

  • 案例 2:子组件修改 props 值

    刷新
    全屏/自适应

    可以得出结论:props 可以修改,但是 Vue 会报错,直接修改 props 是不规范的不建议

  • 案例 3:子组件通过 data 接收 props 值

    刷新
    全屏/自适应

    可以得出结论:props 被 data 接收后,父组件修改,data 数据不会响应变化

  • 案例 4:子组件通过 data 接收 props 值,然后通过 watch 监听 props 变化,重新赋值 data

    刷新
    全屏/自适应

# 1.1.2 复杂数据类型

1.子组件 props 为响应式数据,data 接收后由于指向同一个地址空间,仍然有响应式

2.数据类型如果是函数,可以修改父组件相关逻辑,也可以修改其状态,向父组件传递数据

  • 案例 1.props 传递方法,子组件向父组件传递数据

    刷新
    全屏/自适应

# 1.1.3 原理

# 1.2 $emit

用于子组件向父组件传递数据

# 1.2.1 案例

刷新
全屏/自适应

# 1.2.2 原理

# 1.3 ref

用于实现父组件向子组件传递数据,父组件获取子组件数据(dom)

  • 1.获取组件的 dom

    this.$refs.contain
    
    1
  • 2.执行子组件的方法

    this.$refs.contain.show(val) //此处传递参数,实现向子组件传递数据
    
    1

# 1.3.1 用法

  • parent.vue

    <template>
      <child ref="contain"></child>
    </template>
    <script>
    import child from "./child"
    export default {
      components: {
        child,
      },
      mounted() {
        console.log(this.$refs.contain) // 父组件获取子组件的dom
        console.log(this.$refs.contain.show()) // 父组件调用子组件的方法
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
  • child.vue

    <template>
      <div>my-component</div>
    </template>
    <script>
    export default {
      methods: {
        show() {
          alert(1)
        },
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

主要是为了解决子组件给父组件传递数据或方法的通信问题

parent.vue

<template>
  <child @childfn="childfn" />
</template>
<script>
export default {
  methods: {
    childfn(val) {
      console.log("子组件传递给父组件的数据", val)
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12

child.vue

<template>
  <button @click="handleClick">点我给父组件传递数据:123</button>
</template>
<script>
export default {
  methods: {
    handleClick() {
      this.$emit("childfn", "123") // 第一个参数为方法名,和父组件上事件名称对应,第二个参数为需要传递的值
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12

# 1.3.2 原理

# 1.4 $parent

  • 1.执行父组件的方法

    this.$parent.close()
    
    1
  • 2.执行子组件的方法

    this.$children[0].close()
    
    1

智能组件和木偶组件 collapse-item.vue

<template>
  <div>
    <div class="title" @click="change">{{ title }}</div>
    <div v-show="show">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: ["title"],
  data() {
    return { show: false }
  },
  methods: {
    change() {
      this.$parent.cut(this._uid)
      this.show = !this.show
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

collapse.vue

<template>
  <div class="wrap">
    <slot></slot>
  </div>
</template>
<script>
export default {
  methods: {
    cut(childId) {
      // 只要儿子点击了 我就知道当前点击的是谁
      this.$children.forEach((child) => {
        if (child._uid !== childId) {
          child.show = false
        }
      })
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

parent.vue

<template>
  <collapse>
    <collapse-item title="react">内容1</collapse-item>
    <collapse-item title="vue">内容2</collapse-item>
    <collapse-item title="node">内容3</collapse-item>
  </collapse>
</template>
<script>
import collapse from "./collapse"
import collapseItem from "./collapse-item"
export default {
  components: {
    collapse,
    collapseItem,
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

平级通信 $parent 获取父组件的实列 $children 获取所有的儿子

# 1.5 $children

  • 1.执行父组件的方法

    this.$parent.close()
    
    1
  • 2.执行子组件的方法

    this.$children[0].close()
    
    1

智能组件和木偶组件 collapse-item.vue

<template>
  <div>
    <div class="title" @click="change">{{ title }}</div>
    <div v-show="show">
      <slot></slot>
    </div>
  </div>
</template>
<script>
export default {
  props: ["title"],
  data() {
    return { show: false }
  },
  methods: {
    change() {
      this.$parent.cut(this._uid)
      this.show = !this.show
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

collapse.vue

<template>
  <div class="wrap">
    <slot></slot>
  </div>
</template>
<script>
export default {
  methods: {
    cut(childId) {
      // 只要儿子点击了 我就知道当前点击的是谁
      this.$children.forEach((child) => {
        if (child._uid !== childId) {
          child.show = false
        }
      })
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

parent.vue

<template>
  <collapse>
    <collapse-item title="react">内容1</collapse-item>
    <collapse-item title="vue">内容2</collapse-item>
    <collapse-item title="node">内容3</collapse-item>
  </collapse>
</template>
<script>
import collapse from "./collapse"
import collapseItem from "./collapse-item"
export default {
  components: {
    collapse,
    collapseItem,
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

平级通信 $parent 获取父组件的实列 $children 获取所有的儿子

# 1.6 v-slot

# 1.6.1 匿名插槽

也叫默认插槽,没有命名,有且只有一个

  • 方式一:
刷新
全屏/自适应
  • 方式二:
刷新
全屏/自适应

# 1.6.2 具名插槽

相对匿名插槽组件 slot 标签带 name 命名的

刷新
全屏/自适应

# 1.6.2 作用域插槽

子组件内数据可以被父页面拿到(解决了数据只能从父页面传递给子组件)

刷新
全屏/自适应
// 父组件
<todo-list>
<template v-slot:todo="slotProps" >
 {{slotProps.user.firstName}}
</template>
</todo-list>
//slotProps 可以随意命名
//slotProps 接取的是子组件标签slot上属性数据的集合所有v-bind:user="user"

// 子组件
<slot name="todo" :user="user" :test="test">
  {{ user.lastName }}
</slot>
data() {
  return {
    user:{
      lastName:"Zhang",
      firstName:"yue"
    },
    test:[1,2,3,4]
  }
},
// {{ user.lastName }}是默认数据  v-slot:todo 当父页面没有(="slotProps")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 1.7 sync

 // 父组件
<home :title.sync="title" />
//编译时会被扩展为
<home :title="title"  @update:title="val => title = val"/>

// 子组件
// 所以子组件可以通过$emit 触发 update 方法改变
mounted(){
 this.$emit("update:title", '这是新的title')
}
1
2
3
4
5
6
7
8
9
10

# 1.8 $attrs

主要是为了解决父组件向子组件批量传递属性($attrs)或方法($listeners)的问题

  • $listeners 首先来一个父组件 parent.vue
<template>
  <child @click="change"></child>
</template>
<script>
import child from "./child"
export default {
  components: {
    child,
  },
  methods: {
    change() {
      alert(1)
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

然后来一个子组件 child.vue

<template>
  <button>点我啊</button>
</template>
1
2
3

点击子组件没有触发父组件的方法

原因:如果想在父级中 绑定原生事件给组件 需要加.native 不加就认为是一个普通属性

  • 1.native

    将父组件添加.native:

    <template>
      <child @click.native="change"></child>
    </template>
    <script>
    import child from "./child"
    export default {
      components: {
        child,
      },
      methods: {
        change() {
          alert(1)
        },
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    现在子组件可以正常调用父组件的方法了

    如果上述子组件外面嵌套有其他元素用.native 就失效了

  • 2.@click='$listeners.click()

    父组件不用加.native,子组件外层嵌套其他元素可以用这种方式在 children.vue 中调用父组件的方法

    <template>
      <div>
        <button @click="$listeners.click()">点我啊</button>
      </div>
    </template>
    
    1
    2
    3
    4
    5

    现在子组件可以正常调用父组件的方法了

    如果上述父组件中有多个方法,这种方式又不行了

  • 3.v-on='$listeners'

    父组件中存在多个方法

    <template>
      <child @click="change" @mouseup="change"></child>
    </template>
    <script>
    import child from "./child"
    export default {
      components: {
        child,
      },
      methods: {
        change() {
          alert(1)
        },
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    子组件可以这样写:

    <template>
      <div>
        <button v-on="$listeners">点我啊</button>
      </div>
    </template>
    
    1
    2
    3
    4
    5

    可以正常调用父组件中的方法

# 1.9 $listeners

# 1.10 v-model

主要是为了解决父组件向子组件批量传递属性($attrs)或方法($listeners)的问题

  • $listeners 首先来一个父组件 parent.vue
<template>
  <child @click="change"></child>
</template>
<script>
import child from "./child"
export default {
  components: {
    child,
  },
  methods: {
    change() {
      alert(1)
    },
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

然后来一个子组件 child.vue

<template>
  <button>点我啊</button>
</template>
1
2
3

点击子组件没有触发父组件的方法

原因:如果想在父级中 绑定原生事件给组件 需要加.native 不加就认为是一个普通属性

  • 1.native

    将父组件添加.native:

    <template>
      <child @click.native="change"></child>
    </template>
    <script>
    import child from "./child"
    export default {
      components: {
        child,
      },
      methods: {
        change() {
          alert(1)
        },
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    现在子组件可以正常调用父组件的方法了

    如果上述子组件外面嵌套有其他元素用.native 就失效了

  • 2.@click='$listeners.click()

    父组件不用加.native,子组件外层嵌套其他元素可以用这种方式在 children.vue 中调用父组件的方法

    <template>
      <div>
        <button @click="$listeners.click()">点我啊</button>
      </div>
    </template>
    
    1
    2
    3
    4
    5

    现在子组件可以正常调用父组件的方法了

    如果上述父组件中有多个方法,这种方式又不行了

  • 3.v-on='$listeners'

    父组件中存在多个方法

    <template>
      <child @click="change" @mouseup="change"></child>
    </template>
    <script>
    import child from "./child"
    export default {
      components: {
        child,
      },
      methods: {
        change() {
          alert(1)
        },
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16

    子组件可以这样写:

    <template>
      <div>
        <button v-on="$listeners">点我啊</button>
      </div>
    </template>
    
    1
    2
    3
    4
    5

    可以正常调用父组件中的方法

# 2.兄弟组件通信

# 2.1 任意组件查找

组件为树状结构,可以通过组件名称,向上向下查找到具体组件

  • 1.向上找到最近的指定组件
// assist.js
// 由一个组件,向上找到最近的指定组件
function findComponentUpward(context, componentName) {
  let parent = context.$parent
  let name = parent.$options.name

  while (parent && (!name || [componentName].indexOf(name) < 0)) {
    parent = parent.$parent
    if (parent) name = parent.$options.name
  }
  return parent
}
export { findComponentUpward }
1
2
3
4
5
6
7
8
9
10
11
12
13
  • 2.向上找到所有的指定组件
// assist.js
// 由一个组件,向上找到所有的指定组件
function findComponentsUpward(context, componentName) {
  let parents = []
  const parent = context.$parent

  if (parent) {
    if (parent.$options.name === componentName) parents.push(parent)
    return parents.concat(findComponentsUpward(parent, componentName))
  } else {
    return []
  }
}
export { findComponentsUpward }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  • 3.向下找到最近的指定组件
// assist.js
// 由一个组件,向下找到最近的指定组件
function findComponentDownward(context, componentName) {
  const childrens = context.$children
  let children = null
  if (childrens.length) {
    for (const child of childrens) {
      const name = child.$options.name
      if (name === componentName) {
        children = child
        break
      } else {
        children = findComponentDownward(child, componentName)
        if (children) break
      }
    }
  }
  return children
}
export { findComponentDownward }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  • 4.向下找到所有指定的组建
// assist.js
// 由一个组件,向下找到所有指定的组件
function findComponentsDownward(context, componentName) {
  return context.$children.reduce((components, child) => {
    if (child.$options.name === componentName) components.push(child)
    const foundChilds = findComponentsDownward(child, componentName)
    return components.concat(foundChilds)
  }, [])
}
export { findComponentsDownward }
1
2
3
4
5
6
7
8
9
10
  • 5.找到指定组件中的兄弟组件
// assist.js
// 由一个组件,找到指定组件的兄弟组件
function findBrothersComponents(context, componentName, exceptMe = true) {
  let res = context.$parent.$children.filter((item) => {
    return item.$options.name === componentName
  })
  let index = res.findIndex((item) => item._uid === context._uid)
  if (exceptMe) res.splice(index, 1)
  return res
}
export { findBrothersComponents }
1
2
3
4
5
6
7
8
9
10
11

# 3.跨级组件通信

# 3.1 Vuex

state:定义存贮数据的仓库 ,可通过this.$store.state 或mapState访问
getter:获取 store 值,可认为是 store 的计算属性,可通过this.$store.getter 或
       mapGetters访问
mutation:同步改变 store 值,为什么会设计成同步,因为mutation是直接改变 store 值,
         vue 对操作进行了记录,如果是异步无法追踪改变.可通过mapMutations调用
action:异步调用函数执行mutation,进而改变 store 值,可通过 this.$dispatch或mapActions
       访问
modules:模块,如果状态过多,可以拆分成模块,最后在入口通过...解构引入
1
2
3
4
5
6
7
8

# 3.2 $root

// 父组件
mounted(){
  console.log(this.$root) //获取根实例,最后所有组件都是挂载到根实例上
  console.log(this.$root.$children[0]) //获取根实例的一级子组件
  console.log(this.$root.$children[0].$children[0]) //获取根实例的二级子组件
}
1
2
3
4
5
6

# 3.3 provide & inject

主要是为了解决跨级组件间的通信问题

父组件中通过 provide 来提供变量,然后在子组件或后代组件中通过 inject 来注入变量

# 1.1 用法

  • 父组件:提供数据

    export default {
      provide: {
        name: "Aresn",
      },
    }
    
    1
    2
    3
    4
    5
  • 子组件:使用数据

    export default {
      inject: ["name"],
      mounted() {
        console.log(this.name) //Aresn
      },
    }
    
    1
    2
    3
    4
    5
    6

    注意

    provideinject 绑定的数据不是可响应的,这是刻意为之的。然而,如果你传入了一个可监听的对象,那么其对象的属性还是可响应的。

# 1.2 应用

  • 1.替代 Vuex:将用户的登录信息保存起来

用户的登录信息维护、通知信息维护等全局的状态和数据,通过inject获取,我们把整个 app.vue 实例通过porvide对外提供

任何组件路只要通过inject注入 app.vue 的 app,都可以直接通过this.app.xxx来访问 app.vue 的datacomputedmethods

  • app.vue:提供数据,由于只会渲染一次,利用这个特性,很适合做一次性全局的状态数据管理

    <script>
    export default {
      provide() {
        return {
          app: this,
        }
      },
      data() {
        return {
          userInfo: null,
        }
      },
      methods: {
        getUserInfo() {
          $.ajax("/user/info", (data) => {
            this.userInfo = data // 这里通过 ajax 获取用户信息后,赋值给 this.userInfo
          })
        },
      },
      mounted() {
        this.getUserInfo()
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
  • 访问数据:只要通过inject注入app后,就可以直接访问userInfo的数据了

    <template>
      <div>
        {{ app.userInfo }}
      </div>
    </template>
    <script>
    export default {
      inject: ["app"],
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
  • 修改数据:调用方法,页面修改个人资料,需要重新获取userInfo

    <template>
      <div>
        {{ app.userInfo }}
      </div>
    </template>
    <script>
    export default {
      inject: ["app"],
      methods: {
        changeUserInfo() {
          $.ajax("/user/update", () => {
            this.app.getUserInfo() // 直接通过 this.app 就可以调用 app.vue 里的方法
          })
        },
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17

# 3.4 $dispatch & $broadcast

主要解决:1.父组件向子组件(支持跨级)传递数据、2.子组件向父组件(支持跨级)传递数据的问题

  • emitter.js

    function broadcast(componentName, eventName, params) {
      this.$children.forEach((child) => {
        const name = child.$options.name
        if (name === componentName) {
          child.$emit.apply(child, [eventName].concat(params))
        } else {
          broadcast.apply(child, [componentName, eventName].concat([params]))
        }
      })
    }
    export default {
      methods: {
        dispatch(componentName, eventName, params) {
          let parent = this.$parent || this.$root
          let name = parent.$options.name
          while (parent && (!name || name !== componentName)) {
            parent = parent.$parent
            if (parent) {
              name = parent.$options.name
            }
          }
          if (parent) {
            parent.$emit.apply(parent, [eventName].concat(params))
          }
        },
        broadcast(componentName, eventName, params) {
          broadcast.call(this, componentName, eventName, params)
        },
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30

    注意

    其底层本质就是一个发布订阅的应用场景

# 2.1 用法

  • 子组件调用 dispatch 方法,向上级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该上级组件已预先通过 $on 监听了这个事件

  • 父组件调用 broadcast 方法,向下级指定的组件实例(最近的)上触发自定义事件,并传递数据,且该下级组件已预先通过 $on 监听了这个事件

    import Emitter from "../mixins/emitter.js"
    export default {
      mixins: [Emitter],
      methods: {
        handleDispatch() {
          this.dispatch("组件名称", "函数名称", "函数参数") // ①
        },
        handleBroadcast() {
          this.broadcast("组件名称", "函数名称", "函数参数") // ②
        },
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 2.2 应用

  • broadcast

    <template>
      <button @click="handleClick">触发事件</button>
    </template>
    <script>
    import Emitter from "../mixins/emitter.js"
    
    export default {
      name: "componentA",
      mixins: [Emitter],
      methods: {
        handleClick() {
          this.broadcast("componentB", "on-message", "Hello Vue.js")
        },
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // B.vue
    export default {
      name: "componentB",
      created() {
        this.$on("on-message", this.showMessage)
      },
      methods: {
        showMessage(text) {
          window.alert(text)
        },
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
  • dispatch

    <!-- A.vue -->
    <template>
      <button @click="handleClick">触发事件</button>
    </template>
    <script>
    import Emitter from "../mixins/emitter.js"
    
    export default {
      name: "componentA",
      mixins: [Emitter],
      methods: {
        handleClick() {
          this.dispatch("componentB", "on-message", "Hello Vue.js")
        },
      },
    }
    </script>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // B.vue
    export default {
      name: "componentB",
      created() {
        this.$on("on-message", this.showMessage)
      },
      methods: {
        showMessage(text) {
          window.alert(text)
        },
      },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12

# 4.非组件间通信

# 4.1 bus

  • vue 版的发布订阅模式,平级组件数据传递,这种情况下可以使用中央事件总线的方式
// 在 main.js
Vue.prototype.$eventBus = new Vue()

// 传值组件
this.$eventBus.$emit("eventTarget", "这是eventTarget传过来的值")

// 接收组件
this.$eventBus.$on("eventTarget", (v) => {
  console.log("eventTarget", v) //这是eventTarget传过来的值
})
1
2
3
4
5
6
7
8
9
10

# 4.2 observable

// 文件路径 - /store/store.js
import Vue from "vue"
export const store = Vue.observable({ count: 0 })
export const mutations = {
  setCount(count) {
    store.count = count
  },
}
1
2
3
4
5
6
7
8
<template>
  <div>
    <label for="bookNum">数 量</label>             
    <button @click="setCount(count + 1)">+</button>
    <span>{{ count }}</span>
    <button @click="setCount(count - 1)">-</button>
  </div>
</template>
<script>
import { store, mutations } from "../store/store" // Vue2.6新增API Observable
export default {
  name: "Add",
  computed: {
    count() {
      return store.count
    },
  },
  methods: {
    setCount: mutations.setCount,
  },
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22