能工摹形,巧匠竊意。必三省吾身,萬不可怠惰因循。
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)
}
解析
- this.$options.xx 可以取到vue組件中export default暴露的對象的對應xx屬性值.我們一幫用來取一些靜態屬性.例如 組件的name值,判斷是哪個組件.
- 我們找到對應的子組件或者父組件,然后用$emit調用,實際上就相當于我們在對應的組件A中用this.$emit(xxx)調用其在當前組件A中created生命周期中$on監聽的事件.
- 實際的邏輯就是找到對應組件實例, 組件實例$emit 自己本身$on監聽的事件.
- 引入 main.js
import { broadcast, dispatch } from './dispatch-broadcast';
Vue.prototype.$dispatch = dispatch;
Vue.prototype.$broadcast = broadcast;
- 實例引用.
- $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>
- $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的屬性設置嚴格按照規范),這些方法一般在我們自定義組件庫,或者定義一些高階組件用來使用.
提出問題.
- 如何由一個組件向上找到第一個最近的指定組件?
- 如何由一個組件向上找到所有的指定組件?
- 如何由一個組件向下找到最近的指定組件?
- 如何由一個組件向下找到所有的指定組件?
- 如何由一個組件找到指定的兄弟組件?
分析:
- 利用$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組件之間的通信,當然可能還有更多,這里容納了大部分,當然可能還有其他一些.
好學而不勤問非真好學者. 如果有幫助請點上一個贊,如果由疑問,請評論留言.