vuejs元件通訊精髓歸納
元件的分類
- 常規頁面元件,由 vue-router 產生的每個頁面,它本質上也是一個元件(.vue),主要承載當前頁面的 HTML 結構,會包含資料獲取、資料整理、資料視覺化等常規業務。
- 功能性抽象元件,不包含業務,獨立、具體功能的基礎元件,比如日期選擇器、模態框等。這類元件作為專案的基礎控制元件,會被大量使用,因此元件的 API 進行過高強度的抽象,可以通過不同配置實現不同的功能。
- 業務元件,它不像第二類獨立元件只包含某個功能,而是在業務中被多個頁面複用的,它與獨立元件的區別是,業務元件只在當前專案中會用到,不具有通用性,而且會包含一些業務,比如資料請求;而獨立元件不含業務,在任何專案中都可以使用,功能單一,比如一個具有資料校驗功能的輸入框。
元件的構成
一個再複雜的元件,都是由三部分組成的: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 是不會重複的,藉此我們可以從一系列兄弟元件中把自己排除掉。