vuejs組件通信精髓歸納

組件的分類

  1. 常規頁面組件,由 vue-router 產生的每個頁面,它本質上也是一個組件(.vue),主要承載當前頁面的 HTML 結構,會包含數據獲取、數據整理、數據可視化等常規業務。
  2. 功能性抽象組件,不包含業務,獨立、具體功能的基礎組件,比如日期選擇器、模態框等。這類組件作爲項目的基礎控件,會被大量使用,因此組件的 API 進行過高強度的抽象,可以通過不同配置實現不同的功能。
  3. 業務組件,它不像第二類獨立組件只包含某個功能,而是在業務中被多個頁面複用的,它與獨立組件的區別是,業務組件只在當前項目中會用到,不具有通用性,而且會包含一些業務,比如數據請求;而獨立組件不含業務,在任何項目中都可以使用,功能單一,比如一個具有數據校驗功能的輸入框。

組件的構成

一個再複雜的組件,都是由三部分組成的:prop、event、slot,它們構成了 Vue.js 組件的 API。

屬性 prop

prop 定義了這個組件有哪些可配置的屬性,組件的核心功能也都是它來確定的。寫通用組件時,props 最好用對象的寫法,這樣可以針對每個屬性設置類型、默認值或自定義校驗屬性的值,這點在組件開發中很重要,然而很多人卻忽視,直接使用 props 的數組用法,這樣的組件往往是不嚴謹的。

插槽 slot

插槽 slot,它可以分發組件的內容。和 HTML 元素一樣,我們經常需要向一個組件傳遞內容,像這樣:

<alert-box>
  Something bad happened.
</alert-box>

可能會渲染出這樣的東西:

Error!Something bad happended.

幸好,Vue 自定義的 <slot> 元素讓這變得非常簡單:

Vue.component('alert-box', {
  template: `
    <div class="demo-alert-box">
      <strong>Error!</strong>
      <slot></slot>
    </div>
  `
})

如你所見,我們只要在需要的地方加入插槽就行了——就這麼簡單!

自定義事件 event

兩種寫法:

  • 在組件內部自定義事件event
<template>
  <button @click="handleClick">
    <slot></slot>
  </button>
</template>
<script>
  export default {
    methods: {
      handleClick (event) {
        this.$emit('on-click', event);
      }
    }
  }
</script>

通過 $emit,就可以觸發自定義的事件 on-click ,在父級通過 @on-click 來監聽:

<i-button @on-click="handleClick"></i-button>
  • 用事件修飾符 .native直接在父級聲明

所以上面的示例也可以這樣寫:

<i-button @click.native="handleClick"></i-button>

如果不寫 .native 修飾符,那上面的 @click 就是自定義事件 click,而非原生事件 click,但我們在組件內只觸發了 on-click 事件,而不是 click,所以直接寫 @click 會監聽不到。

組件的通信

ref和$parent和$children

Vue.js 內置的通信手段一般有兩種:

  • ref:給元素或組件註冊引用信息;
  • $parent / $children:訪問父 / 子實例。

用 ref 來訪問組件(部分代碼省略):

// component-a
export default {
  data () {
    return {
      title: 'Vue.js'
    }
  },
  methods: {
    sayHello () {
      window.alert('Hello');
    }
  }
}
<template>
  <component-a ref="comA"></component-a>
</template>
<script>
  export default {
    mounted () {
      const comA = this.$refs.comA;
      console.log(comA.title);  // Vue.js
      comA.sayHello();  // 彈窗
    }
  }
</script>

$parent 和 $children 類似,也是基於當前上下文訪問父組件或全部子組件的。
這兩種方法的弊端是,無法在跨級或兄弟間通信,比如下面的結構:

// parent.vue
<component-a></component-a>
<component-b></component-b>
<component-b></component-b>

我們想在 component-a 中,訪問到引用它的頁面中(這裏就是 parent.vue)的兩個 component-b 組件,那這種情況下,是暫時無法實現的,後面會講解到方法。

provide / inject

一種無依賴的組件通信方法:Vue.js 內置的 provide / inject 接口

provide / inject 是 Vue.js 2.2.0 版本後新增的 API,在文檔中這樣介紹 :
這對選項需要一起使用,以允許一個祖先組件向其所有子孫後代注入一個依賴,不論組件層次有多深,並在起上下游關係成立的時間裏始終生效。如果你熟悉 React,這與 React 的上下文特性很相似。
provide 和 inject 主要爲高階插件/組件庫提供用例。並不推薦直接用於應用程序代碼中。
假設有兩個組件: A.vue 和 B.vue,B 是 A 的子組件:

// A.vue
export default {
  provide: {
    name: 'Aresn'
  }
}

// B.vue
export default {
  inject: ['name'],
  mounted () {
    console.log(this.name);  // Aresn
  }
}

需要注意的是:
provide 和 inject 綁定並不是可響應的。這是刻意爲之的。然而,如果你傳入了一個可監聽的對象,那麼其對象的屬性還是可響應的。

只要一個組件使用了 provide 向下提供數據,那其下所有的子組件都可以通過 inject 來注入,不管中間隔了多少代,而且可以注入多個來自不同父級提供的數據。需要注意的是,一旦注入了某個數據,那這個組件中就不能再聲明 這個數據了,因爲它已經被父級佔有。

provide / inject API 主要解決了跨級組件間的通信問題,不過它的使用場景,主要是子組件獲取上級組件的狀態,跨級組件間建立了一種主動提供與依賴注入的關係。然後有兩種場景它不能很好的解決:

  • 父組件向子組件(支持跨級)傳遞數據;
  • 子組件向父組件(支持跨級)傳遞數據。

這種父子(含跨級)傳遞數據的通信方式,Vue.js 並沒有提供原生的 API 來支持,下面介紹一種在父子組件間通信的方法 dispatch 和 broadcast。

派發與廣播——自行實現 dispatch 和 broadcast 方法

要實現的 dispatch 和 broadcast 方法,將具有以下功能:
在子組件調用 dispatch 方法,向上級指定的組件實例(最近的)上觸發自定義事件,並傳遞數據,且該上級組件已預先通過 $on 監聽了這個事件;
相反,在父組件調用 broadcast 方法,向下級指定的組件實例(最近的)上觸發自定義事件,並傳遞數據,且該下級組件已預先通過 $on 監聽了這個事件。

// 部分代碼省略
import Emitter from '../mixins/emitter.js'

export default {
  mixins: [ Emitter ],
  methods: {
    handleDispatch () {
      this.dispatch();  // ①
    },
    handleBroadcast () {
      this.broadcast();  // ②
    }
  }
}
 //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);
    }
  }
};

因爲是用作 mixins 導入,所以在 methods 裏定義的 dispatch 和 broadcast 方法會被混合到組件裏,自然就可以用 this.dispatch 和 this.broadcast 來使用。
這兩個方法都接收了三個參數,第一個是組件的 name 值,用於向上或向下遞歸遍歷來尋找對應的組件,第二個和第三個就是上文分析的自定義事件名稱和要傳遞的數據。
可以看到,在 dispatch 裏,通過 while 語句,不斷向上遍歷更新當前組件(即上下文爲當前調用該方法的組件)的父組件實例(變量 parent 即爲父組件實例),直到匹配到定義的 componentName 與某個上級組件的 name 選項一致時,結束循環,並在找到的組件實例上,調用 $emit 方法來觸發自定義事件 eventName。broadcast 方法與之類似,只不過是向下遍歷尋找。

來看一下具體的使用方法。有 A.vue 和 B.vue 兩個組件,其中 B 是 A 的子組件,中間可能跨多級,在 A 中向 B 通信:

<!-- A.vue -->
<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>
// B.vue
export default {
  name: 'componentB',
  created () {
    this.$on('on-message', this.showMessage);
  },
  methods: {
    showMessage (text) {
      window.alert(text);
    }
  }
}

同理,如果是 B 向 A 通信,在 B 中調用 dispatch 方法,在 A 中使用 $on 監聽事件即可。
以上就是自行實現的 dispatch 和 broadcast 方法。

找到任意組件實例——findComponents 系列方法

它適用於以下場景:

  • 由一個組件,向上找到最近的指定組件;
  • 由一個組件,向上找到所有的指定組件;
  • 由一個組件,向下找到最近的指定組件;
  • 由一個組件,向下找到所有指定的組件;
  • 由一個組件,找到指定組件的兄弟組件。

5 個不同的場景,對應 5 個不同的函數,實現原理也大同小異。

向上找到最近的指定組件——findComponentUpward

// 由一個組件,向上找到最近的指定組件
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 };

比如下面的示例,有組件 A 和組件 B,A 是 B 的父組件,在 B 中獲取和調用 A 中的數據和方法:

<!-- component-a.vue -->
<template>
  <div>
    組件 A
    <component-b></component-b>
  </div>
</template>
<script>
  import componentB from './component-b.vue';

  export default {
    name: 'componentA',
    components: { componentB },
    data () {
      return {
        name: 'Aresn'
      }
    },
    methods: {
      sayHello () {
        console.log('Hello, Vue.js');
      }
    }
  }
</script>
<!-- component-b.vue -->
<template>
  <div>
    組件 B
  </div>
</template>
<script>
  import { findComponentUpward } from '../utils/assist.js';

  export default {
    name: 'componentB',
    mounted () {
      const comA = findComponentUpward(this, 'componentA');
      
      if (comA) {
        console.log(comA.name);  // Aresn
        comA.sayHello();  // Hello, Vue.js
      }
    }
  }
</script>

向上找到所有的指定組件——findComponentsUpward

// 由一個組件,向上找到所有的指定組件
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 };

向下找到最近的指定組件——findComponentDownward

// 由一個組件,向下找到最近的指定組件
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 };

向下找到所有指定的組件——findComponentsDownward

// 由一個組件,向下找到所有指定的組件
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 };

找到指定組件的兄弟組件——findBrothersComponents

// 由一個組件,找到指定組件的兄弟組件
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 };

相比其它 4 個函數,findBrothersComponents 多了一個參數 exceptMe,是否把本身除外,默認是 true。尋找兄弟組件的方法,是先獲取 context.$parent.$children,也就是父組件的全部子組件,這裏面當前包含了本身,所有也會有第三個參數 exceptMe。Vue.js 在渲染組件時,都會給每個組件加一個內置的屬性 _uid,這個 _uid 是不會重複的,藉此我們可以從一系列兄弟組件中把自己排除掉。

參考Vuejs組件精講

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章