Vue組件通訊

Vue的組件是其很是重要的系統,組件之間的通訊也是開發中不可避免的需求vue

通常來講Vue組件是如下幾種關係ajax

Vue組件

A組件和B組件、B組件和C組件、B組件和D組件是父子關係,C組件和D組件是兄弟關係,A組件和C/D組件是隔代關係。vuex

本文闡述了幾種經常使用的通訊方式和使用場景api

props&&emit

父組件經過 props 傳遞數據給子組件,子組件經過 emit 發送事件傳遞數據給父組件數組

這種父子通訊方式也就是典型的單向數據流,父組件經過 props 傳遞數據,子組件不能直接修改 props , 而是必須經過發送事件的方式告知父組件修改數據。app

// component-a
<template>
  <div id="app">
    <com-b title="cc" @changeTitle="handChange"></com-b> 
    <!-- 傳入title  接受自定義事件changeTitle -->
  </div>
</template>

<script>
import comB from './components/comB.vue'

export default {
  name: 'app',
  components: {
    comB
  },
  methods:{
    // 接受子組件傳遞的事件
    handChange(val){
      alert(val)
    }
  }
}
</script>

// component-b
<template>
  <div>
    {{title}}
    <button @click="handChange">Change</button>
  </div>
</template>

<script>
export default {
  name: "com-b",
  props: ["title"],// props 父組件傳來的值
  methods:{
    // 觸發自定義事件,想父組件傳遞發送修改後的數據
    handChange(){
      this.$emit('changeTitle','newTitle')
    }
  }
};
</script>

優勢:易於使用,結構清晰ide

缺點:只能用於父子組件通訊函數

ref&&$parent / $children

這兩種都是直接獲得組件實例,使⽤後能夠直接調⽤組件的⽅法或訪問數據工具

  1. ref:給元素或組件註冊引⽤信息
  2. $parent/$children:訪問父/子組件

ref

// component-a
<template>
  <div id="app">
    <!-- 爲子組件註冊引用信息 -->
    <com-b ref="comB"></com-b>
  </div>
</template>

<script>
import comB from "./components/comB.vue";

export default {
  name: "app",
  components: {
    comB
  },
  mounted() {
    // 經過$refs獲取到對應子組件的引用信息
    console.log(this.$refs.comB);
  }
};
</script>

$parent / $children

$parent$children都是基於當前上下文訪問父組件和子組件ui

// component-a
<template>
  <div id="app">
    <com-b></com-b>
  </div>
</template>

<script>
import comB from "./components/comB.vue";

export default {
  name: "app",
  components: {
    comB
  },
  mounted() {
    // 經過$children獲取所有子組件
    console.log(this.$children);
  }
};
</script>

// component-b
<template>
  <div>

  </div>
</template>

<script>
export default {
  name: "com-b",
  mounted(){
    // 經過$parent獲取父組件
    console.log(this.$parent);
  }
};
</script>

ref$parent/$children的優缺點和props&&emit相同,弊端都是沒法在跨級和兄弟間通訊

provide/inject

ref$parent/$children在跨級通訊中有必定的弊端。
Vue.js 2.2.0 版本後新增 provide / inject API
vue文檔
這對選項須要⼀起使⽤,以容許⼀個祖先組件向其全部⼦孫後代注⼊⼀個依賴,不論組件層次有多深,並在起上下游關係成⽴的時間⾥始終⽣效

provide 選項應該是一個對象或返回一個對象的函數。該對象包含可注入其子孫的屬性。

inject 選項應該是:

  1. 一個字符串數組
  2. 一個對象,對象的 key 是本地的綁定名,value 是:

    • 在可用的注入內容中搜索用的 key (字符串或 Symbol),或
    • 一個對象,該對象的:

      • from 屬性是在可用的注入內容中搜索用的 key (字符串或 Symbol)
      • default 屬性是降級狀況下使用的 value

provide 和 inject 綁定並非可響應的。這是刻意爲之的。然而,若是你傳入了一個可監聽的對象,那麼其對象的屬性仍是可響應的。

// 父級組件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子組件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}

模擬Vuex

在作 Vue ⼤型項⽬時,可使⽤ Vuex 作狀態管理。使用 provide / inject,能夠模擬 達到 Vuex 的效果 。
使⽤ Vuex,最主要的⽬的是跨組件通訊、全局數據維護。⽐如⽤戶的登陸信息維護、通知信息維護等全局的狀態和數據

一般vue應用都有一個根根組件app.vue,能夠⽤來存儲全部須要的全局數據和狀態,methods 等。項目中全部的組件其父組件都是app,經過provideapp實例暴露對外提供

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

接下來任何組件只要經過 inject 注⼊ app.vueapp 的話,均可以直接經過this.app.xxx 來訪問 app.vuedata、computed、methods 等內容

例如經過這個特性保存登陸信息

export default {
  provide() {
    return {
      app: this
    };
  },
  data() {
    return {
      userInfo: null
    };
  },
  methods: {
    getUserInfo() {
      // 這⾥經過 ajax 獲取⽤戶信息後,賦值給this.userInfo;
      $.ajax("/user", data => {
        this.userInfo = data;
      });
    }
  },
  mounted() {
    this.getUserInfo();
  }
};

以後在任何⻚⾯或組件,只要經過 inject 注⼊ app 後,就能夠直接訪問 userInfo 的數據了

<template>
  <div>
    {{ app.userInfo }}
  </div>
</template>
<script>
  export default {
    inject: ['app']
  }
</script>

優勢:

  • 跨級注入
  • 全部子組件均可獲取到注入的信息

缺點:

  • 注入的數據非響應式

Vuex

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。當須要開發開發大型單頁應用(SPA),就應該考慮使用Vuex了,它能把組件的共享狀態抽取出來,當作一個全局單例模式進行管理。這樣無論你在何處改變狀態,都會通知使用該狀態的組件作出相應修改。
Vuex官方文檔已經給出了詳細的使用方式

優勢:

  • 官方集成管理庫,能夠處理各類場景的通訊和狀態管理

缺點:

  • 須要額外引入管理庫

Bus

若是不是大型項目,狀態管理不復雜,數據量不是很大,沒有必要使用Vuex

可使用一個空的vue實例做爲事件總線中間件Bus處理組件間的通訊

首先在全局定義bus

let bus = new Vue();

var eventBus = {
  install(Vue, options) {
    Vue.prototype.$bus = bus;
  }
};
Vue.use(eventBus);

而後就能夠在組件中使用$on,$emit,off來監聽,分發和銷燬組件

分發組件

// component-c
<template>
  <div>
    <button @click="handClick">handClick</button>
  </div>
</template>

<script>
export default {
  name: "com-c",
  methods: {
    handClick: function() {
      this.$bus.$emit("bus", "val"); 
    }
  }
};
</script>

監聽組件

// component-d
<template>
  <div></div>
</template>

<script>
export default {
  name: "com-d",
  // ...
  created() {
    this.$bus.$on("bus", val => {
      //獲取傳遞的參數並進行操做
      console.log(val);
    });
  },
  // 最好在組件銷燬前
  // 清除事件監聽
  beforeDestroy() {
    this.$bus.$off("evetn");
  }
};
</script>

最好在組件銷燬以前清除監聽事件

優勢:

  • 使用簡單,不須要額外支持
  • 能夠實現跨級和兄弟間通訊

缺點:

  • 須要在組件銷燬時,手動清除事件監聽
  • 事件過多時比較混亂

dispatch/broadcast

$dispatch$broadcast 是Vue1.x中提供的API,前者⽤於向上級派發事件,只要是它的⽗級(⼀級或多級以上),均可以在組件內經過 $on 監聽到,後者相反,是由上級向下級⼴播事件

// 子組件
vm.$dispatch(eventName,params)
// 父組件
vm.$on(eventName
, (params) => {
console.log(params); 
});

$broadcast 相似,只不過⽅向相反。這兩種⽅法⼀旦發出事件後,任何組件都是能夠接收到的,就近原則,⽽且會在第⼀次接收到後停⽌冒泡,除⾮返回 true

這2個方法在已經被棄用,Vue官方給出的解釋是:

由於基於組件樹結構的事件流方式實在是讓人難以理解,而且在組件結構擴展的過程當中會變得愈來愈脆弱。這種事件方式確實不太好,咱們也不但願在之後讓開發者們太痛苦。而且$dispatch 和 $broadcast 也沒有解決兄弟組件間的通訊問題。

雖然在開發中,沒有Vuex這樣的專業狀態管理工具方便好用,可是在獨立組件庫和一些特殊場景中,也是很是好用的一種傳遞方式。

模擬dispatch/broadcast

自行模擬dispatch/broadcast沒法達到與原方法如出一轍的效果,可是基本功能都是能夠實現的,解決組件之間的通訊問題

方法有功能有向上/下找到對應的組件,觸發指定事件並傳遞數據,其下/上級組件已經經過$on監聽了該事件。

首先須要正確地向上或向下找到對應的組件實例,並在它上⾯觸發⽅法。

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);
    }
  }
};

這兩個⽅法都接收了三個參數,第⼀個是組件的 name 值,⽤於向上或向下遞歸遍從來尋找對應的組件,第⼆個和第三個就是上⽂分析的⾃定義事件名稱和要傳遞的數據。

dispatch ⾥,經過 while 語句,不斷向上遍歷更新當前組件(即上下⽂爲當前調⽤該⽅法的組件)的⽗組件實例(變量 parent 即爲⽗組件實例),直到匹配到定義的 componentName 與某個上級組件的 name 選項⼀致時,結束循環,並在找到的組件實例上,調⽤ $emit ⽅法來觸發⾃定義事件 eventNamebroadcast ⽅法與之相似,只不過是向下遍歷尋找

優勢:

  • 使用簡單
  • 能夠實現跨級通訊

缺點:

  • 原生支持已經廢除,須要自行實現

findComponents系列

上述介紹的各類通訊方法都有各自的侷限性,咱們能夠實現一個 findComponents 系列的方法,能夠實現

  • 向上找到最近的指定組件
  • 向上找到全部的指定組件
  • 向下找到最近的指定組件
  • 向下找到全部指定的組件
  • 找到指定組件的兄弟組件

5個方法都是經過遞歸和遍歷,經過組件name選項匹配到指定組件返回

向上找到最近的指定組件

function findComponentUpward(context, componentName) {
    let parent = context.$parent; // 獲取父級組件
    let name = parent.$options.name; // 獲取父級組件名稱
    // 若是父級存在 且 父級組件 沒有name 或 name與要尋找的組件名不一致,重置parent和name,再逐級尋找
    while (parent && (!name || [componentName].indexOf(name) < 0)) {
        parent = parent.$parent;
        if (parent) name = parent.$options.name;
    }
    // 逐級查找父級組件名和傳參名是否一致,返回找到的parent
    return parent;
}

findComponentUpward 接收兩個參數,第⼀個是當前上下⽂,即你要基於哪一個組件來向上尋找,⼀般都是基於當前的組件,也就是傳⼊ this;第⼆個參數是要找的組件的 name 。
dispatch是經過觸發和監聽事件來完成事件交互,findComponentUpward 會直接拿到組件的實例

向上找到全部的指定組件

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 [];
    }
}

findComponentsUpward會返回的是⼀個數組,包含了全部找到的組件實例
findComponentsUpward 的使⽤場景較少

向下找到最近的指定組件

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;
}

context.$children 獲得的是當前組件的所有⼦組件,因此須要遍歷⼀遍,找到有沒有匹配到的組件 name,若是沒找到,繼續遞歸找每一個 $children$children,直到找到最近的⼀個爲⽌

向下找到全部指定的組件

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);
    }, []);
}

使⽤ reduce 作累加器,並⽤遞歸將找到的組件合併爲⼀個數組並返回

找到指定組件的兄弟組件

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;
}

findBrothersComponents 多了⼀個參數 exceptMe ,是否把自己除外,默認是 true 。尋找兄弟組件的⽅法,是先獲取 context.$parent.$children,也就是⽗組件的所有⼦組件,這⾥⾯當前包含了自己,全部也會有第三個參數exceptMeVue.js 在渲染組件時,都會給每一個組件加⼀個內置的屬性 _uid,這個 _uid 是不會重複的,藉此咱們能夠從⼀系列兄弟組件中把⾃⼰排除掉。

相關文章
相關標籤/搜索