Vue3全局組件通信之provide / inject

顧名思義,爺孫組件是比 父子組件通信 要更深層次的引用關系(也有稱之為 “隔代組件”):

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 選項來開始使用這些數據。

  1. Grandfather.vue 通過 provideGrandson.vue 傳值(可包含定義好的函數)
  2. Grandson.vue 通過 injectGrandfather.vue 觸發爺爺組件的事件執行

無論組件層次結構有多深,發起 provide 的組件都可以作為其所有下級組件的依賴提供者。

TIP

這一部分的內容變化都特別大,但使用起來其實也很簡單,不用慌,也有相同的地方:

  1. 父組件不需要知道哪些子組件使用它 provide 的 property
  2. 子組件不需要知道 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, injectprovide 一樣,也是需要先導入然后在 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 本身不可響應,但是并非完全不能夠拿到響應的結果,只需要我們傳入的數據具備響應性,它依然能夠提供響應支持。

我們以 refreactive 為例,來看看應該怎么發起 provide 和接收 inject

對這 2 個 API 還不熟悉的同學,建議先閱讀一下 響應式 API 之 ref響應式 API 之 reactive

先在 Grandfather.vueprovide 數據:

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.vueinject 拿到數據:

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 出去,在子孫組件拿到的也是響應式的,并且可以如同自身定義的響應式變量一樣,直接 returntemplate 使用,一旦數據有變化,視圖也會立即更新。

但上面這句話有效的前提是,不破壞數據的響應性,比如 ref 變量,你需要完整的傳入,而不能只傳入它的 value,對于 reactive 也是同理,不能直接解構去破壞原本的響應性。

切記!切記!!!

#引用類型的傳遞與接收

這里是針對非響應性數據的處理

provide 和 inject 并不是可響應的,這是官方的故意設計,但是由于引用類型的特殊性,在子孫組件拿到了數據之后,他們的屬性還是可以正常的響應變化。

先在 Grandfather.vueprovide 數據:

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.vueinject 拿到數據:

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.vueprovide 數據:

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.vueinject 拿到數據:

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.vueprovide 數據:

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

點贊加關注,永遠不迷路

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 全局組件通信 全局組件通信是指,兩個任意的組件,不管是否有關聯(e.g. 父子、爺孫)的組件,都可以直接進行交流的...
    硅谷干貨閱讀 27,944評論 0 13
  • 前言組件是 vue.js 最強大的功能之一,而組件實例的作用域是相互獨立的,這就意味著不同組件之間的數據無法相互引...
    翔ni閱讀 234評論 0 0
  • 本文主要介紹父->子、子->父、兄弟組件間、跨級組件間的傳值方式。 一、props【父->子】 在父組件頁面使用v...
    YiYaYiYaHei閱讀 407評論 0 1
  • 父子組件通信 絕大部分vue本身提供的通信方式,都是父子組件通信 prop[https://github.com/...
    冉開林閱讀 747評論 0 0
  • Vue3 API 匯總手冊 setup setup 函數是一個新的組件選項。作為在組件內使用 Compositio...
    硅谷干貨閱讀 1,709評論 0 14