能工摹形,巧匠竊意岂昭。必三省吾身,萬不可怠惰因循羡榴。
foreword
- 這篇容納了我個人所知道的一些Vue 2.x組件通信的總結(jié),之后3.x官網(wǎng)公布后會增加3.x的部分。(篇幅長,細(xì)節(jié)有那么一些些,熟知部分可以一眼略過).
start
一. props $emit $attrs $listeners $props
- 之所以把$attrs/$listeners/$props 和props $emit 放在一起 是因為個人感覺坑资,更加方便記憶耗帕。老項目使用$attrs $listeners $props這幾個API需要看當(dāng)時的vue版本是不是已經(jīng)支持;
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,// 可以是自定義構(gòu)造函數(shù),用instanceof 檢測
validator(V){ // 自定義驗證函數(shù)
return V.length > 2
}
}
},
created(){
console.log(this.stars) //[{…}, {…}, {…}, {…}, __ob__: Observer]
}
}
</script>
summarize: 父組件通過props傳入到子組件. 子組件可以設(shè)定傳入值的校驗,等屬性.組件中的數(shù)據(jù)方式共有 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的形式改變父組件的值,實際上是調(diào)用傳入?yún)?shù)父組件的方法,來改變父組件的值. 有部分程序員喜歡將 .sync 和v-model這兩個語法糖也歸為組件通信方式,這里不做歸納.詳細(xì)請看官方文檔.sync,v-model.
3. $attrs/$listeners/$props
官方解釋
- $props:當(dāng)前組件接收到的 props 對象。Vue 實例代理了對其 props 對象屬性的訪問袱贮。類型(Object)
- $attrs:包含了父作用域中不作為 prop 被識別 (且獲取) 的特性綁定 (class 和 style 除外)仿便。類型:{ [key: string]: string }(只讀)
- $listeners: 包含了父作用域中的 (不含 .native 修飾器的) v-on 事件監(jiān)聽器。它可以通過 v-on="$listeners" 傳入內(nèi)部組件——在創(chuàng)建更高層次的組件時非常有用攒巍。類型: { [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
- 注意這里使用v-bind="$props"就會使得子組件中的input標(biāo)簽綁定上父組件中定義的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, // 將默認(rèn)綁定根元素屬性去掉
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
默認(rèn)情況下父作用域的不被認(rèn)作 props 的特性綁定 (attribute bindings) 將會“回退”且作為普通的 HTML 特性應(yīng)用在子組件的根元素上嗽仪。當(dāng)撰寫包裹一個目標(biāo)元素或另一個組件的組件時,這可能不會總是符合預(yù)期行為柒莉。通過設(shè)置 inheritAttrs 到 false绒疗,這些默認(rèn)行為將會被去掉城榛。而通過 (同樣是 2.4 新增的) 實例屬性 $attrs 可以讓這些特性生效,且可以通過 v-bind 顯性的綁定到非根元素上顽耳。
$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:父實例容握,如果當(dāng)前實例有的話侧戴。(類型:Vue instance)
$children:當(dāng)前實例的直接子組件。需要注意 $children 并不保證順序喷市,也不是響應(yīng)式的。如果你發(fā)現(xiàn)自己正在嘗試使用 $children 來進(jìn)行數(shù)據(jù)綁定威恼,考慮使用一個數(shù)組配合 v-for 來生成子組件品姓,并且使用 Array 作為真正的來源。(類型:Array)
$root:當(dāng)前組件樹的根 Vue 實例箫措。如果當(dāng)前實例沒有父實例腹备,此實例將會是其自己。
$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類似,是當(dāng)前組件樹的根實例.
附加:使用$parent或者$root配合$on和$emit可以 進(jìn)行兄弟組件之間通信
// 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 格式為數(shù)組,如果沒有就是空數(shù)組,但是這里的數(shù)組順序與頁面順序是不對應(yīng)的,這里涉及到了虛擬dom掛載.
- 上面的部分情況其實是拿到對應(yīng)的組件的實例,相當(dāng)于在對應(yīng)vue組件中調(diào)用this.xx = 'xxxx';
- 實際開發(fā)中,非自定義組件,或者真實需要,不建議使用$parent和$children $root進(jìn)行組件之間的通信.
三. provide/inject
- provide 和 inject 主要為高階插件/組件庫提供用例斤蔓。并不推薦直接用于應(yīng)用程序代碼中植酥。provide/inject能夠?qū)崿F(xiàn)祖先和后代之間傳值.
// 祖先組件
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 綁定并不是可響應(yīng)的。這是刻意為之的。然而友驮,如果你傳入了一個可監(jiān)聽的對象漂羊,那么其對象的屬性還是可響應(yīng)的。這里也可以傳入this到后代組件中,但實際開發(fā)中不推薦使用,可以用于開發(fā)高階組件或者組件庫.
四.事件總線eventBus方式(自定義Bus類,或者使用Vue代替);
// Bus 類
class Bus {
constructor() {
this.CB = {};
}
// 監(jiān)聽
$on(name, fn) {
this.CB[name] = this.CB[name] || [];
this.CB[name].push(fn)
}
// 派發(fā)
$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內(nèi)部已經(jīng)做了具體處理.并且提供$once只監(jiān)聽一次這個事件,$off(name)移除name事件監(jiān)聽,$off() 移除所有事件監(jiān)聽.
- 這里主要說Vue通信方式,所以關(guān)于上部分需要在destroy生命周期需要注銷監(jiān)聽等操作都未列出,實際開發(fā)實際需求.
五.Vuex
- Vuex 是一個專為 Vue.js 應(yīng)用程序開發(fā)的狀態(tài)管理模式,實際上是把一些需要多處用到的狀態(tài)放在同一個對象中.
- 小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簡單一些,詳細(xì)可以參考中文官網(wǎng)Vuex中文官網(wǎng)
六. 自定義broadcast/dispatch
- vue 1.x 版本中有兩個API $dipatch,$broadcast,$broadcast和$dispatch 這兩個API在2.x版本中去除. 實際上我們經(jīng)常寫一些自定義組件庫,或者高階組件的時候可能會用到.
vue 1.x解釋
$dispatch:向上級派發(fā)事件,祖輩組件中$on監(jiān)聽到
$broadcast:與$dispatch相反,向下級廣播事件.
- 自定義代碼實現(xiàn)功能.
/**
* @param {*} componentName // 組件名
* @param {*} eName // 自定義事件名稱
* @param {*} params // 傳遞參數(shù)數(shù)據(jù)
*/
export function broadcast(componentName, eName, params) {
this.$children.forEach(child => {
const name = child.$options.name;
if (name === componentName) {
// 調(diào)用子組件emit
child.$emit.bind(child)(eName, params)
} else {
// 遞歸調(diào)用
broadcast.bind(child)(componentName, eName, params)
}
})
};
/**
* @param {*} componentName // 組件名
* @param {*} eName // 自定義事件名稱
* @param {*} params // 傳遞參數(shù)數(shù)據(jù)
*/
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)
}
解析
- this.$options.xx 可以取到vue組件中export default暴露的對象的對應(yīng)xx屬性值.我們一幫用來取一些靜態(tài)屬性.例如 組件的name值,判斷是哪個組件.
- 我們找到對應(yīng)的子組件或者父組件,然后用$emit調(diào)用,實際上就相當(dāng)于我們在對應(yīng)的組件A中用this.$emit(xxx)調(diào)用其在當(dāng)前組件A中created生命周期中$on監(jiān)聽的事件.
- 實際的邏輯就是找到對應(yīng)組件實例, 組件實例$emit 自己本身$on監(jiān)聽的事件.
- 引入 main.js
import { broadcast, dispatch } from './dispatch-broadcast';
Vue.prototype.$dispatch = dispatch;
Vue.prototype.$broadcast = broadcast;
- 實例引用.
- $dispatch 派發(fā)
// 后代
<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>
- $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多個方法
- 就像上面說的,其實我們尋找到了對應(yīng)組件的實例,就可以用這個實例進(jìn)行操作,就可以說進(jìn)行了組件的通信.那么這里就存在幾個問題. (注意這里的前提是組件中name的屬性設(shè)置嚴(yán)格按照規(guī)范),這些方法一般在我們自定義組件庫,或者定義一些高階組件用來使用.
提出問題.
- 如何由一個組件向上找到第一個最近的指定組件?
- 如何由一個組件向上找到所有的指定組件?
- 如何由一個組件向下找到最近的指定組件?
- 如何由一個組件向下找到所有的指定組件?
- 如何由一個組件找到指定的兄弟組件?
分析:
- 利用$options.name $children $parent , 參數(shù)包含當(dāng)前組件的this,要找到的組件名name. 通過$options.name確定尋找的組件.
1. 由一個組件向上找到第一個最近的指定組件.
/**
* @param {*} context 執(zhí)行上下文卸留,這里一般傳 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 執(zhí)行上下文走越,這里一般傳 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 執(zhí)行上下文,這里一般傳 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 執(zhí)行上下文耻瑟,這里一般傳 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 執(zhí)行上下文旨指,這里一般傳 this
* @componentName {*} componentName 要找到的組件名 name
* @exceptMe {Boolean} 是否包含本身
* @description2 Vue.js 在渲染組件時,都會給每個組件加一個內(nèi)置的屬性 _uid喳整,這個 * * *_uid 是不會重復(fù)的谆构,
*/
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,之后通信的方式就可以很隨意了,當(dāng)然這里使用方式一般是存在特殊情況下,正常我們組件之間的通信使用Vuex 或者 props $emit 就可以了.
代碼參考 iview源碼 具體位置在 iview assets.js,有興趣的朋友可以查看源碼.
總結(jié)
- Vue組件之間的通信,當(dāng)然可能還有更多,這里容納了大部分,當(dāng)然可能還有其他一些.
好學(xué)而不勤問非真好學(xué)者. 如果有幫助請點上一個贊,如果由疑問,請評論留言.