Vue組件通信總結

能工摹形,巧匠竊意。必三省吾身,萬不可怠惰因循。

foreword

  • 這篇容納了我個人所知道的一些Vue 2.x組件通信的總結,之后3.x官網公布后會增加3.x的部分。(篇幅長,細節有那么一些些,熟知部分可以一眼略過).

start

一. props $emit $attrs $listeners $props

  • 之所以把$attrs/$listeners/$props 和props $emit 放在一起 是因為個人感覺,更加方便記憶。老項目使用$attrs $listeners $props這幾個API需要看當時的vue版本是不是已經支持;
1.props 父組件向子組件傳值

_ parent.vue

<template>
  <div>
    PARENT
    <children :stars="stars"></children>
  </div>
</template>

<script>
import children from './children/children';
export default {
  components:{children},
  data(){
    return {
      stars:[
        {name:"周杰倫",id:1},
        {name:"劉亦菲",id:2},
        {name:"胡歌",id:3},
        {name:"古天樂",id:4},
      ]
    }
  },
}
</script>

_ children.vue

<template>
   <div>
     CHILDREN
     <ul>
       <li v-for="star in stars" :key="star.id">{{star.name}}</li>
     </ul>
   </div>
</template>

<script>
export default {
  name:"children", 
  props:{
    stars:{
      type:Array,
      default(){
        return []
      },
      // required:true // 是否必須屬性
      // type:Symbol, // 傳入類型 type String Number Boolean Function Object Array Symbol 
      // type:CustormFn,// 可以是自定義構造函數,用instanceof 檢測
      validator(V){  // 自定義驗證函數
        return V.length > 2
      }
    }
  },
  created(){
    console.log(this.stars) //[{…}, {…}, {…}, {…}, __ob__: Observer]
  }
}
</script>
summarize: 父組件通過props傳入到子組件. 子組件可以設定傳入值的校驗,等屬性.組件中的數據方式共有 data,computed,props以及provide和inject(這個待商榷).
2. 子組件通過事件的形式向父組件傳值

_ parent

<template>
  <div>
    <p>{{bestHandsome}}</p>
    <children @handleBs='handleBs'></children>
  </div>
</template>
<script>
import children from './children';
export default {
  name:'parent2',
  components:{children},
  data(){
    return {
      bestHandsome:'劉德華'
    }
  },
  methods:{
    handleBs(name){
      this.bestHandsome = name;
    }
  }
}
</script>

_ children

<template>
  <button @click="setBestHandsome('吳彥祖')">BUTTON</button>
</template>

<script>
export default {
  name:'children2',
  methods:{
    setBestHandsome(name){
      this.$emit('handleBs',name);
    }
  }
}
</script>
summarize:子組件通過events的形式改變父組件的值,實際上是調用傳入參數父組件的方法,來改變父組件的值. 有部分程序員喜歡將 .sync 和v-model這兩個語法糖也歸為組件通信方式,這里不做歸納.詳細請看官方文檔.sync,v-model.
3. $attrs/$listeners/$props

官方解釋

  • $props:當前組件接收到的 props 對象。Vue 實例代理了對其 props 對象屬性的訪問。類型(Object)
  • $attrs:包含了父作用域中不作為 prop 被識別 (且獲取) 的特性綁定 (class 和 style 除外)。類型:{ [key: string]: string }(只讀)
  • $listeners: 包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監聽器。它可以通過 v-on="$listeners" 傳入內部組件——在創建更高層次的組件時非常有用。類型: { [key: string]: Function | Array<Function> }(只讀)
$props

_ code

// parent.vue
<template>
  <div>
    <children
      name='input'
      type='nmber'
      disabled
      autofocus
      placeholder='這是一個輸入框'
    ></children>
  </div>
</template>
// children.vue
<template>
   <div>
     <input v-bind="$props">
   </div>
</template>

<script>
export default {
  name:"children", 
  props:['name','type','disabled','autofocus','placeholder'],
  mounted(){
    console.log(this.$props.name)// input
  }
}
</script>

_ view


view

html.png
  • 注意這里使用v-bind="$props"就會使得子組件中的input標簽綁定上父組件中定義的props屬性.
$attrs

_ code

// parent.vue
<template>
  <div>
    <children
      name='input'
      type='nmber'
      disabled
      autofocus
      placeholder='這是一個輸入框'
    ></children>
  </div>
</template>
// children.vue
<template>
   <div>
     <input v-bind="$attrs">
   </div>
</template>

<script>
export default {
  inheritAttrs:false, // 將默認綁定根元素屬性去掉
  name:"children", 
  props:['handsome'],
  mounted(){
    console.log(this.$attrs.name)// input
    console.log(this.$attrs.handsome)// undefined
    console.log(this.$props.handsome)// 1
  }
}
</script>
  • inheritAttrs

默認情況下父作用域的不被認作 props 的特性綁定 (attribute bindings) 將會“回退”且作為普通的 HTML 特性應用在子組件的根元素上。當撰寫包裹一個目標元素或另一個組件的組件時,這可能不會總是符合預期行為。通過設置 inheritAttrs 到 false,這些默認行為將會被去掉。而通過 (同樣是 2.4 新增的) 實例屬性 $attrs 可以讓這些特性生效,且可以通過 v-bind 顯性的綁定到非根元素上。

不設置inheritAttrs:false效果

設置inheritAttrs效果
$listeners

_ code

// parent.vue
<template>
  <div>
    <p>{{ handsome }}</p>
    <children
      @changeHandsome="changeHandsome"
      @clearHandsome="clearHandsome"
      @resetHandsome="resetHandsome"
    ></children>
  </div>
</template>

<script>
import children from './children/children';
export default {
  components:{children},
  data(){
    return {
      handsome:'lin'
    }
  },
  methods:{
    changeHandsome(name){
      this.handsome = name;
    },
    clearHandsome(){
      this.handsome = '';
    },
    resetHandsome(){
      this.handsome = 'lin';
    },
  }
}
</script>
// children.vue
<template>
   <div>
     <g-children v-on="$listeners"></g-children>
   </div>
</template>
<script>
import gChildren from './grandchildren'
export default {
  name:"children", 
  components:{gChildren},
  mounted(){
    console.log(this.$listeners)
  }
}
</script>
// grandchildren.vue
<template>
  <div>
    <button @click="$emit('changeHandsome','zhou')">set Zhou</button>
    <button @click="$emit('clearHandsome')">clear</button>
    <button @click="$emit('resetHandsome')">reset</button>
  </div>
</template>
以上這些實際上是父子組件直接直接或者間接通過vue提供的通信方式通信.

二. $refs $parent $children $root

  • 官方解釋

$refs:一個對象,持有注冊過 [ref 特性] 的所有 DOM 元素和組件實例。
$parent:父實例,如果當前實例有的話。(類型:Vue instance)
$children:當前實例的直接子組件。需要注意 $children 并不保證順序,也不是響應式的。如果你發現自己正在嘗試使用 $children 來進行數據綁定,考慮使用一個數組配合 v-for 來生成子組件,并且使用 Array 作為真正的來源。(類型:Array)
$root:當前組件樹的根 Vue 實例。如果當前實例沒有父實例,此實例將會是其自己。

$refs

_ code

//children.vue
<script>
export default {
  name: "children",
  data() {
    return {
      name: "xiaoerlang",
      age: 18
    };
  }
};
</script>
// parent.vue
<template>
  <div>
    <children ref="children"></children>
    <button @click="setChildrenData">button</button>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  components: { children },
  methods: {
    setChildrenData() {
      console.log(this.$refs.children.name); //第一次點擊按鈕的時候打印 xiaolang
      this.$refs.children.name = "xiaoming";
      console.log(this.$refs.children.name); //第一次點擊按鈕的時候打印 xiaoming
    }
  }
};
</script>
$parent
// parent.vue
<template>
  <div>
    {{name}}
    <children></children>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  components: { children },
  data(){
    return {
      name:'liu'
    }
  },
};
</script>
// children.vue
<template>
  <div>
    <button @click="setParentName('fei')">button</button>
  </div>
</template>
<script>
export default {
  name: "children",
  methods:{
    setParentName(name){
      this.$parent.name = name;
    }
  }
};
</script>
$children
// parent.vue
<template>
  <div>
    <children></children>
    <button @click="setChildrenName('yi')">button</button>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  components: { children },
  methods:{
    setChildrenName(name){
      this.$children[0].name = name;
    }
  }
};
</script>
// children.vue
<template>
  <div>
    {{name}}
  </div>
</template>
<script>
export default {
  name: "children",
  data() {
    return {
      name: "xiaoerlang",
    };
  },
};
</script>
$root:這里與$parent類似,是當前組件樹的根實例.

附加:使用$parent或者$root配合$on和$emit可以 進行兄弟組件之間通信

// parent.vue
<template>
  <div>
    <bother1></bother1>
    <bother2></bother2>
  </div>
</template>
<script>
import bother1 from './children/brother1';
import bother2 from './children/brother2';
export default {
  components: { bother1,bother2 },
};
</script>
// bother2.vue
<template>
  <div>{{name}}</div>
</template>
<script>
export default {
  name:'brother2',
  data(){
    return{
      name:'zhouxiaolun'
    }
  },
  created(){
    this.$parent.$on('setB2',this.setName)
  },
  methods:{
    setName(name){
      this.name = name;
    }
  }
}
</script>

// bother1.vue
<template>
  <button @click="setB2Name('zhoujielun')">button</button>
</template>

<script>
export default {
  name:'brother1',
  methods:{
    setB2Name(name){
      this.$parent.$emit('setB2',name)
    }
  }
}
</script>
summarize
  • 注意這里$children 格式為數組,如果沒有就是空數組,但是這里的數組順序與頁面順序是不對應的,這里涉及到了虛擬dom掛載.
  • 上面的部分情況其實是拿到對應的組件的實例,相當于在對應vue組件中調用this.xx = 'xxxx';
  • 實際開發中,非自定義組件,或者真實需要,不建議使用$parent和$children $root進行組件之間的通信.

三. provide/inject

  • provide 和 inject 主要為高階插件/組件庫提供用例。并不推薦直接用于應用程序代碼中。provide/inject能夠實現祖先和后代之間傳值.
// 祖先組件
export default {
  provide() {
    const that = this;
    return {
      foo: "foo",
      forefathersThis: that
    };
  },
  name: "parent",
  components: { children }
};
// 后代組件
export default {
  name: "children",
  inject: ["foo", "forefathersThis"],
  created() {
    console.log(this.foo);
    console.log(this.forefathersThis); // 祖先組件的實例
  }
};
  • 提示:provide 和 inject 綁定并不是可響應的。這是刻意為之的。然而,如果你傳入了一個可監聽的對象,那么其對象的屬性還是可響應的。這里也可以傳入this到后代組件中,但實際開發中不推薦使用,可以用于開發高階組件或者組件庫.

四.事件總線eventBus方式(自定義Bus類,或者使用Vue代替);

// Bus 類
class Bus {
  constructor() {
    this.CB = {};
  }
  // 監聽
  $on(name, fn) {
    this.CB[name] = this.CB[name] || [];
    this.CB[name].push(fn)
  }
  // 派發
  $emit(name, args) {
    this.CB[name] && this.CB[name].forEach(cb => cb(args))
  }
}
export default Bus;
// main.js
import Bus from './eventBus';
Vue.prototype.$bus = new Bus();
// 組件1
 methods: {
    setBH2Name() {
      this.$bus.$emit("setB2", "zhoujielun");
    }
  }
// 組件2 
  created() {
    this.$bus.$on("setB2", this.setName);
  },
  methods: {
    setName(name) {
      this.name = name;
    }
  }
summarize:
  • 如果不使用自定義方式,也可以Vue.prototype.$bus = new Vue(); vue內部已經做了具體處理.并且提供$once只監聽一次這個事件,$off(name)移除name事件監聽,$off() 移除所有事件監聽.
  • 這里主要說Vue通信方式,所以關于上部分需要在destroy生命周期需要注銷監聽等操作都未列出,實際開發實際需求.

五.Vuex

  • Vuex 是一個專為 Vue.js 應用程序開發的狀態管理模式,實際上是把一些需要多處用到的狀態放在同一個對象中.
  • 小demo
// store
  state: {
    infoName: "handsome"
  },
  mutations: {
    setInfoName(state, payload) {
      state.infoName = payload;
    }
  }
// 組件1
<script>
import { mapMutations } from "vuex";
import bother2 from "./children/brother2";
export default {
  name: "parent",
  components: { bother2 },
  methods: {
    ...mapMutations(["setInfoName"]),
    setInfo() {
      const name = "ugly";
      this.setInfoName(name);
    }
  }
};
</script>
// 組件2
<script>
import { mapState } from "vuex";
export default {
  name: "brother2",
  computed: {
    ...mapState({
      infoName: s => s.infoName
    })
  }
};
</script>

summarize: Vuex相對來說比redux簡單一些,詳細可以參考中文官網Vuex中文官網

六. 自定義broadcast/dispatch

  • vue 1.x 版本中有兩個API $dipatch,$broadcast,$broadcast和$dispatch 這兩個API在2.x版本中去除. 實際上我們經常寫一些自定義組件庫,或者高階組件的時候可能會用到.
    vue 1.x解釋

$dispatch:向上級派發事件,祖輩組件中$on監聽到
$broadcast:與$dispatch相反,向下級廣播事件.

  • 自定義代碼實現功能.
/**
 * @param {*} componentName  // 組件名
 * @param {*} eName  // 自定義事件名稱
 * @param {*} params // 傳遞參數數據
 */
export function broadcast(componentName, eName, params) {
  this.$children.forEach(child => {
    const name = child.$options.name;
    if (name === componentName) {
      // 調用子組件emit
      child.$emit.bind(child)(eName, params)
    } else {
      // 遞歸調用
      broadcast.bind(child)(componentName, eName, params)
    }
  })
};
/**
 * @param {*} componentName  // 組件名
 * @param {*} eName  // 自定義事件名稱
 * @param {*} params // 傳遞參數數據
 */
export function dispatch(componentName, eName, 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.bind(parent)(eName, params)
}
解析
  1. this.$options.xx 可以取到vue組件中export default暴露的對象的對應xx屬性值.我們一幫用來取一些靜態屬性.例如 組件的name值,判斷是哪個組件.
  2. 我們找到對應的子組件或者父組件,然后用$emit調用,實際上就相當于我們在對應的組件A中用this.$emit(xxx)調用其在當前組件A中created生命周期中$on監聽的事件.
  3. 實際的邏輯就是找到對應組件實例, 組件實例$emit 自己本身$on監聽的事件.
  • 引入 main.js
import { broadcast, dispatch } from './dispatch-broadcast';
Vue.prototype.$dispatch = dispatch;
Vue.prototype.$broadcast = broadcast;
  • 實例引用.
  1. $dispatch 派發
//  后代
<template>
  <div><button @click="setParentDay('Sat')">button</button></div>
</template>
<script>
export default {
  name: "children",
  methods: {
    setParentDay(day) {
      this.$dispatch("parent", "setDay", day);
    }
  }
};
</script>
//  祖先
<div>
    <children></children>
    <p>{{ day }}</p>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  name: "parent",
  components: { children },
  data() {
    return { day: "Fir" };
  },
  created() {
    this.$on("setDay", this.setDay);
  },
  methods: {
    setDay(day) {
      this.day = day;
    }
  }
};
</script>
  1. $broadcast 廣播
// 祖先
<template>
  <div>
    <children></children>
    <button @click="setChildrenDay('Fir')">button</button>
  </div>
</template>
<script>
import children from "./children/children";
export default {
  name: "parent",
  components: { children },
  methods: {
    setChildrenDay(day) {
      this.$broadcast("children", "setDay", day);
    }
  },
};
</script>
// --------- 后代 --------------
<template>
  <div>{{ day }}</div>
</template>
<script>
export default {
  name: "children",
  data() {
    return {
      day: "Sat"
    };
  },
  created() {
    this.$on("setDay", this.setDay);
  },
  methods: {
    setDay(day) {
      this.day = day;
    }
  }
};
</script>

七. 自定義findComponents多個方法

  • 就像上面說的,其實我們尋找到了對應組件的實例,就可以用這個實例進行操作,就可以說進行了組件的通信.那么這里就存在幾個問題. (注意這里的前提是組件中name的屬性設置嚴格按照規范),這些方法一般在我們自定義組件庫,或者定義一些高階組件用來使用.
提出問題.
  1. 如何由一個組件向上找到第一個最近的指定組件?
  2. 如何由一個組件向上找到所有的指定組件?
  3. 如何由一個組件向下找到最近的指定組件?
  4. 如何由一個組件向下找到所有的指定組件?
  5. 如何由一個組件找到指定的兄弟組件?
分析:
  • 利用$options.name $children $parent , 參數包含當前組件的this,要找到的組件名name. 通過$options.name確定尋找的組件.
1. 由一個組件向上找到第一個最近的指定組件.
/**
 * @param {*} context  執行上下文,這里一般傳 this
 * @param {*} componentName 要找到的組件名 name
 * @returns
 */
function findComponentUpwrad(context, componentName) {
  let parent = context.$parent;
  let { name } = parent.$options;

  while (parent && (!name || [componentName].indexOf(name) < 0)) {
    parent = parent.$parent;
    if (parent) name = parent.$options.name;
  }
  return parent;
}
2. 由一個組件向上找到所有的指定組件
/**
 * @param {*} context  執行上下文,這里一般傳 this
 * @param {*} componentName 要找到的組件名 name
 */
function findComponentsUpward(context, componentName) {
  const parents = [];
  const parent = context.$parent;
  if (parent) {
    if (parent.$options.name === componentName) parents.push(parent);
    return parents.concat(findComponentUpwrad(parent, componentName));
  }
  return [];
}
3. 由一個組件向下找到最近的指定組件
/**
 *@description 向下找到最近的指定組件
 *
 * @context {*} context 執行上下文,這里一般傳 this
 * @componentName {*} componentName 要找到的組件名 name
 */
function findComponentDownward(context, componentName) {
  const childrens = context.$children;
  let children = null;
  if (childrens.length) {
    for (const child of childrens) {
      const { name } = child.$options;
      if (name === componentName) {
        children = child;
        break;
      } else {
        children = findComponentDownward(child, componentName);
        if (children) break;
      }
    }
  }
  return children;
}
4. 由一個組件向下找到所有的指定組件
/**
 * @context {*} context 執行上下文,這里一般傳 this
 * @componentName {*} componentName 要找到的組件名 name
 */
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);
  }, []);
}
5. 由一個組件找到指定的兄弟組件
/**
 * @context {*} context 執行上下文,這里一般傳 this
 * @componentName {*} componentName 要找到的組件名 name
 * @exceptMe  {Boolean}  是否包含本身
 * @description2 Vue.js 在渲染組件時,都會給每個組件加一個內置的屬性 _uid,這個 * * *_uid 是不會重復的,
 */
function findBrothersComponents(context, componentName, exceptMe) {
  const res = context.$parent.$children.filter(item => item.$options.name === componentName);
  const index = res.findIndex(item => item._uid === context._uid);
  if (exceptMe) res.splice(index, 1);
  return res;
}
找到組件后就等于找到組件中的this,之后通信的方式就可以很隨意了,當然這里使用方式一般是存在特殊情況下,正常我們組件之間的通信使用Vuex 或者 props $emit 就可以了.
代碼參考 iview源碼 具體位置在 iview assets.js,有興趣的朋友可以查看源碼.

總結

  • Vue組件之間的通信,當然可能還有更多,這里容納了大部分,當然可能還有其他一些.

好學而不勤問非真好學者. 如果有幫助請點上一個贊,如果由疑問,請評論留言.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,837評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,196評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,688評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,654評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,456評論 6 406
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,955評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,044評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,195評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,725評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,608評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,802評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,318評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,048評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,422評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,673評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,424評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,762評論 2 372

推薦閱讀更多精彩內容