顧名思義,爺孫組件是比 父子組件通信 要更深層次的引用關系(也有稱之為 “隔代組件”):
C組件引入到B組件里,B組件引入到A組件里渲染,此時A是C的爺爺級別(可能還有更多層級關系),如果你用 props
,只能一級一級傳遞下去,那就太繁瑣了,因此我們需要更直接的通信方式。
他們之間的關系如下,Grandson.vue
并非直接掛載在 Grandfather.vue
下面,他們之間還隔著至少一個 Son.vue
(可能有多個):
Grandfather.vue
└─Son.vue
└─Grandson.vue
這一 Part 就是講一講 C 和 A 之間的數據傳遞,常用的方法有:
方案 | 爺組件向孫組件 | 孫組件向爺組件 | 對應章節傳送門 |
---|---|---|---|
provide / inject | provide | inject | 點擊查看 |
EventBus | emit / on | emit / on | 點擊查看 |
Vuex | - | - | 點擊查看 |
為了方便閱讀,下面的父組件統一叫 Grandfather.vue
,子組件統一叫 Grandson.vue
,但實際上他們之間可以隔無數代…
TIP
因為上下級的關系的一致性,爺孫組件通信的方案也適用于 父子組件通信 ,只需要把爺孫關系換成父子關系即可。
#provide / inject
這個特性有兩個部分:Grandfather.vue
有一個 provide
選項來提供數據,Grandson.vue
有一個 inject
選項來開始使用這些數據。
-
Grandfather.vue
通過provide
向Grandson.vue
傳值(可包含定義好的函數) -
Grandson.vue
通過inject
向Grandfather.vue
觸發爺爺組件的事件執行
無論組件層次結構有多深,發起 provide
的組件都可以作為其所有下級組件的依賴提供者。
TIP
這一部分的內容變化都特別大,但使用起來其實也很簡單,不用慌,也有相同的地方:
- 父組件不需要知道哪些子組件使用它 provide 的 property
- 子組件不需要知道 inject property 來自哪里
另外,要切記一點就是:provide 和 inject 綁定并不是可響應的。這是刻意為之的,但如果傳入了一個可監聽的對象,那么其對象的 property 還是可響應的。
#發起 provide
我們先來回顧一下 2.x 的用法:
export default {
// 定義好數據
data () {
return {
tags: [ '中餐', '粵菜', '燒臘' ]
}
},
// provide出去
provide () {
return {
tags: this.tags
}
}
}
舊版的 provide
用法和 data
類似,都是配置為一個返回對象的函數。
3.x 的新版 provide
, 和 2.x 的用法區別比較大。
TIP
在 3.x , provide
需要導入并在 setup
里啟用,并且現在是一個全新的方法。
每次要 provide
一個數據的時候,就要單獨調用一次。
每次調用的時候,都需要傳入 2 個參數:
參數 | 類型 | 說明 |
---|---|---|
key | string | 數據的名稱 |
value | any | 數據的值 |
來看一下如何創建一個 provide
:
// 記得導入provide
import { defineComponent, provide } from 'vue'
export default defineComponent({
// ...
setup () {
// 定義好數據
const msg: string = 'Hello World!';
// provide出去
provide('msg', msg);
}
})
操作非常簡單對吧哈哈哈,但需要注意的是,provide
不是響應式的,如果你要使其具備響應性,你需要傳入響應式數據,詳見:響應性數據的傳遞與接收
#接收 inject
也是先來回顧一下 2.x 的用法:
export default {
inject: [
'tags'
],
mounted () {
console.log(this.tags);
}
}
舊版的 inject
用法和 props
類似,3.x 的新版 inject
, 和 2.x 的用法區別也是比較大。
TIP
在 3.x, inject
和 provide
一樣,也是需要先導入然后在 setup
里啟用,也是一個全新的方法。
每次要 inject
一個數據的時候,就要單獨調用一次。
每次調用的時候,只需要傳入 1 個參數:
參數 | 類型 | 說明 |
---|---|---|
key | string | 與 provide 相對應的數據名稱 |
來看一下如何創建一個 inject
:
// 記得導入inject
import { defineComponent, inject } from 'vue'
export default defineComponent({
// ...
setup () {
const msg: string = inject('msg') || '';
}
})
也是很簡單(寫 TS 的話,由于 inject
到的值可能是 undefined
,所以要么加個 undefined
類型,要么給變量設置一個空的默認值)。
#響應性數據的傳遞與接收
之所以要單獨拿出來說, 是因為變化真的很大 - -
在前面我們已經知道,provide 和 inject 本身不可響應,但是并非完全不能夠拿到響應的結果,只需要我們傳入的數據具備響應性,它依然能夠提供響應支持。
我們以 ref
和 reactive
為例,來看看應該怎么發起 provide
和接收 inject
。
對這 2 個 API 還不熟悉的同學,建議先閱讀一下 響應式 API 之 ref 和 響應式 API 之 reactive 。
先在 Grandfather.vue
里 provide
數據:
export default defineComponent({
// ...
setup () {
// provide一個ref
const msg = ref<string>('Hello World!');
provide('msg', msg);
// provide一個reactive
const userInfo: Member = reactive({
id: 1,
name: 'Petter'
});
provide('userInfo', userInfo);
// 2s 后更新數據
setTimeout(() => {
// 修改消息內容
msg.value = 'Hi World!';
// 修改用戶名
userInfo.name = 'Tom';
}, 2000);
}
})
在 Grandsun.vue
里 inject
拿到數據:
export default defineComponent({
setup () {
// 獲取數據
const msg = inject('msg');
const userInfo = inject('userInfo');
// 打印剛剛拿到的數據
console.log(msg);
console.log(userInfo);
// 因為 2s 后數據會變,我們 3s 后再看下,可以爭取拿到新的數據
setTimeout(() => {
console.log(msg);
console.log(userInfo);
}, 3000);
// 響應式數據還可以直接給 template 使用,會實時更新
return {
msg,
userInfo
}
}
})
非常簡單,非常方便!!!
TIP
響應式的數據 provide
出去,在子孫組件拿到的也是響應式的,并且可以如同自身定義的響應式變量一樣,直接 return
給 template
使用,一旦數據有變化,視圖也會立即更新。
但上面這句話有效的前提是,不破壞數據的響應性,比如 ref 變量,你需要完整的傳入,而不能只傳入它的 value
,對于 reactive
也是同理,不能直接解構去破壞原本的響應性。
切記!切記!!!
#引用類型的傳遞與接收
這里是針對非響應性數據的處理
provide 和 inject 并不是可響應的,這是官方的故意設計,但是由于引用類型的特殊性,在子孫組件拿到了數據之后,他們的屬性還是可以正常的響應變化。
先在 Grandfather.vue
里 provide
數據:
export default defineComponent({
// ...
setup () {
// provide 一個數組
const tags: string[] = [ '中餐', '粵菜', '燒臘' ];
provide('tags', tags);
// provide 一個對象
const userInfo: Member = {
id: 1,
name: 'Petter'
};
provide('userInfo', userInfo);
// 2s 后更新數據
setTimeout(() => {
// 增加tags的長度
tags.push('叉燒');
// 修改userInfo的屬性值
userInfo.name = 'Tom';
}, 2000);
}
})
在 Grandsun.vue
里 inject
拿到數據:
export default defineComponent({
setup () {
// 獲取數據
const tags: string[] = inject('tags') || [];
const userInfo: Member = inject('userInfo') || {
id: 0,
name: ''
};
// 打印剛剛拿到的數據
console.log(tags);
console.log(tags.length);
console.log(userInfo);
// 因為 2s 后數據會變,我們 3s 后再看下,能夠看到已經是更新后的數據了
setTimeout(() => {
console.log(tags);
console.log(tags.length);
console.log(userInfo);
}, 3000);
}
})
引用類型的數據,拿到后可以直接用,屬性的值更新后,子孫組件也會被更新。
WARNING
由于不具備真正的響應性,return
給模板使用依然不會更新視圖,如果涉及到視圖的數據,請依然使用 響應式 API 。
#基本類型的傳遞與接收
這里是針對非響應性數據的處理
基本數據類型被直接 provide
出去后,再怎么修改,都無法更新下去,子孫組件拿到的永遠是第一次的那個值。
先在 Grandfather.vue
里 provide
數據:
export default defineComponent({
// ...
setup () {
// provide 一個數組的長度
const tags: string[] = [ '中餐', '粵菜', '燒臘' ];
provide('tagsCount', tags.length);
// provide 一個字符串
let name: string = 'Petter';
provide('name', name);
// 2s 后更新數據
setTimeout(() => {
// tagsCount 在 Grandson 那邊依然是 3
tags.push('叉燒');
// name 在 Grandson 那邊依然是 Petter
name = 'Tom';
}, 2000);
}
})
在 Grandsun.vue
里 inject
拿到數據:
export default defineComponent({
setup () {
// 獲取數據
const name: string = inject('name') || '';
const tagsCount: number = inject('tagsCount') || 0;
// 打印剛剛拿到的數據
console.log(name);
console.log(tagsCount);
// 因為 2s 后數據會變,我們 3s 后再看下
setTimeout(() => {
// 依然是 Petter
console.log(name);
// 依然是 3
console.log(tagsCount);
}, 3000);
}
})
很失望,并沒有變化。
TIP
那么是否一定要定義成響應式數據或者引用類型數據呢?
當然不是,我們在 provide
的時候,也可以稍作修改,讓它能夠同步更新下去。
我們再來一次,依然是先在 Grandfather.vue
里 provide
數據:
export default defineComponent({
// ...
setup () {
// provide 一個數組的長度
const tags: string[] = [ '中餐', '粵菜', '燒臘' ];
provide('tagsCount', (): number => {
return tags.length;
});
// provide 字符串
let name: string = 'Petter';
provide('name', (): string => {
return name;
});
// 2s 后更新數據
setTimeout(() => {
// tagsCount 現在可以正常拿到 4 了
tags.push('叉燒');
// name 現在可以正常拿到 Tom 了
name = 'Tom';
}, 2000);
}
})
再來 Grandsun.vue
里修改一下 inject
的方式,看看這次拿到的數據:
export default defineComponent({
setup () {
// 獲取數據
const tagsCount: any = inject('tagsCount');
const name: any = inject('name');
// 打印剛剛拿到的數據
console.log(tagsCount());
console.log(name());
// 因為 2s 后數據會變,我們 3s 后再看下
setTimeout(() => {
// 現在可以正確得到 4
console.log(tagsCount());
// 現在可以正確得到 Tom
console.log(name());
}, 3000);
}
})
這次可以正確拿到數據了,看出這2次的寫法有什么區別了嗎?
TIP
基本數據類型,需要 provide
一個函數,將其 return
出去給子孫組件用,這樣子孫組件每次拿到的數據才會是新的。
但由于不具備響應性,所以子孫組件每次都需要重新通過執行 inject
得到的函數才能拿到最新的數據。
按我個人習慣來說,使用起來挺別扭的,能不用就不用……
WARNING
由于不具備真正的響應性,return
給模板使用依然不會更新視圖,如果涉及到視圖的數據,請依然使用 響應式 API 。
點贊加關注,永遠不迷路