Vue 3 中的 Provide 與 Inject

Eason Lin
8 min readOct 3, 2021

在 Vue 中,父子組件可以透過 propsemit 溝通,祖父跟祖孫的溝通則需要透過父來當中間人傳遞。除了 Vuex 外,Provide/Inject 也是一種選擇。

Provide/Inject 在 Vue 2.2.0 中加入,是一種讓上層組件和下層組件可以跨過中間組件來傳遞資料的方式,由上層組件 Provide,即可 Inject 至下層組件,例如:

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<father></father>
</div>
<script>
const app = Vue.createApp({
provide: {
foo: "bar",
},
});
app.component("father", {
template: `<child></child>`,
});
app.component("child", {
inject: ["foo"],
template: `<h1>{{ foo }}</h1>`,
});
app.mount("#app");
</script>
</body>
</html>

傳遞方式會類似這樣:

需注意的是,Provide 是由父層傳遞資料至子層以下,子層組件 Provide 無法被父層組件 Inject,例如:

    <script>
const app = Vue.createApp({
inject: ["foo"],
});
app.component("father", {
provide: {
foo: "bar",
},
template: `<child></child>`,
});
app.component("child", {
inject: ["foo"],
template: `<h1>{{ foo }}</h1>`,
});
app.mount("#app");
</script>

此時開啟瀏覽器應該會看到:

此外,若祖父層和父層同時 Provide 相同的屬性,子層的 Inject 會以父層為主,例如:

    <script>
const app = Vue.createApp({
data() {
return {
foo: "foo provided by grandpa",
};
},
provide() {
return {
foo: this.foo,
};
},
mounted() {
setTimeout(() => {
this.foo = "modified";
}, 2000);
},
});
app.component("father", {
template: `<child></child>`,
provide() {
return {
foo: "foo provided by father",
};
},
});
app.component("child", {
inject: ["foo"],
template: `<span>Injected foo from child: {{ foo }}</span>`,
});
app.mount("#app");
</script>
實際開啟瀏覽器看到的結果

Provide 提供的值只為其子層以下組件所使用,父層以上、兄弟等其他組件是無法 Inject 的,若在不同組件中有同樣屬性名、但值為不同的情境,感覺會是個不錯的使用契機。

若要傳遞的資料需要透過 this 取得,則要將 provide 改為回傳物件的函式,例如:

    <script>
const app = Vue.createApp({
data() {
return {
foo: "bar",
};
},
provide() {
return {
foo: this.foo,
};
},
});
app.component("father", {
template: `<child></child>`,
});
app.component("child", {
inject: ["foo"],
template: `<h1>{{ foo }}</h1>`,
});
app.mount("#app");
</script>

在默認情況下,Provide/Inject 並不是響應式的,在 Vue2 文件有這樣一段敘述(在 Vue3 中則無提及此為故意設計):

Note: the provide and inject bindings are NOT reactive. This is intentional. However, if you pass down an observed object, properties on that object do remain reactive.

大意就是說 Provide/Inject 不是響應式的,我們是刻意這樣設計,但如果傳入的是響應式物件,依然可以是響應式。可參考以下範例(改寫自官方範例)

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="https://unpkg.com/vue@next"></script>
</head>
<body>
<div id="app">
<todo-list :todos="todos"></todo-list>
<button @click="todos.push('another')">Click to add todo</button>
</div>
<script>
const app = Vue.createApp({
data() {
return {
todos: ["Feed a cat", "Buy tickets"],
};
},
provide() {
return {
todoLength: Vue.computed(() => this.todos.length),
};
},
});
app.component("todo-list", {
props: {
todos: {
type: Array,
required: true,
},
},
template: `
<div>
<ol>
<li v-for="(item, index) in todos" :key="index">{{ item }}</li>
</ol>
<todo-list-statistics />
</div>
`,
});
app.component("todo-list-statistics", {
inject: ["todoLength"],
created() {
console.log(`Injected property: ${this.todoLength}`);
},
template: `
<p>
todo length: {{ todoLength }}
</p>
`,
});
app.mount("#app");
</script>
</body>
</html>

使用上就可以是響應式:

關於 Provide/Inject 就記錄到這,閱讀文件下來給我的感覺有點像是 ReactcreateContext 。這個主題其實掛在我的草稿區一陣子了,之前就一直在想等到實際使用過再來記錄,可惜目前大多數跨組件溝通都使用 Vuex 來解決,實際上尚無碰到我自己認為適合使用的情境。

希望這篇記錄能幫助到你,若有任何錯誤,也煩請不吝指出,謝謝。

References:

--

--