在第10天,我們終於學習了一個關鍵的響應式概念:從現有狀態衍生新狀態。在 Vue 3 與 Angular 中,使用 computed
函數來從其他狀態創建只讀的響應式狀態。這些狀態可以是只讀或可寫的狀態。在 Svelte 5 中,衍生狀態透過 $derived
或 $derived.by
rune 創建。
以下是三個衍生狀態的示例:
首先,從 vue 中導入 computed
,reverse_items
是透過複製 items
ref 並反轉來建立的 computed
。
在模板中,使用 v-if
指令檢查 reversed_items
的長度是否大於零。條件為真時,使用 v-for
指令遍歷 reversed_items
,由新至舊顯示項目。
<script setup lang="ts">
import { ref, computed } from 'vue'
const items = ref<Item[]>([])
const reverse_items = computed(() => [...items.value].reverse())
</script>
<template v-if="reversed_items.length > 0">
<ul>
<div class="list-item" v-for="item in reverse_items" :key="item.id">
<li>{{ item.id }} - {{ item.label }}
</li>
</div>
</ul>
</template>
使用 $derived
rune 取得反轉 items
列表並指派給 reversed_items
。模板使用 #if
檢查 reversed_items
非空,然後用 #each
由新至舊遍歷項目。
<script lang="ts">
let items = $state([] as Item[]);
let reversed_items = $derived([...items].reverse());
</script>
{#if reversed_items.length > 0}
<ul>
{#each reversed_items as item (item.id)}
<div class="list-item">
<li>{item.id} - {item.label}</li>
</div>
{/each}
</ul>
{/if}
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-shopping-cart',
imports: [FormsModule, NgIcon],
template: `
@if (reverse_items().length > 0) {
<ul>
@for (item of reverse_items(); track item.id) {
<div class="list-item">
<li>{{ item.id }} - {{ item.label }}</li>
</div>
}
</ul>
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
items = signal<Item[]>([]);
reverse_items = computed(() => [...this.items()].reverse());
}
從 @angular/core
導入 computed
。reverse_items
的定義與 Vue 3 相同,模板透過 @if
判斷 reverse_items
是否有元素,條件為真時,以 @for
逐一由新至舊顯示。
計算已購買商品的數量,可以通過 computed
中使用 Array.reduce
來完成,統計 purchased
屬性為 true 的項目數。
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ref, computed } from 'vue'
const items = ref<Item[]>([])
const num_items_purchased = computed(() =>
items.value.reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0),
)
<script lang="ts">
let items = $state([] as Item[]);
let num_items_purchased = $derived(
items.reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0)
);
</script>
num_items_purchased
rune 是透過在 $derived
rune 中調用 Array.reduce
來衍生出來的。"
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-shopping-cart',
template: `
...
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
items = signal<Item[]>([]);
num_items_purchased = computed(() => this.items().reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0));
}
num_items_purchased
是一個 computed signal,用來表示已購買商品的數量。透過迭代 items
signal,計算出 purchased
屬性為 true 的項目數量。
num_items_purchased_label
是從 num_items_purchased
衍生出來的顯示字串。
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { ref, computed } from 'vue'
const items = ref<Item[]>([])
const num_items_purchased = computed(() =>
items.value.reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0),
)
const num_items_purchased_label = computed(() => {
const unit = num_items_purchased.value === 1 ? 'item' : 'items'
return `${num_items_purchased.value} ${unit} purchased`
})
</script>
當 num_items_purchased
等於 1 時,顯示文字為 "num_items_purchased.value
item purchased"。當 num_items_purchased
大於 1 時,顯示文字為 "num_items_purchased.value
items purchased"。
<div class="header">
<template v-if="num_items_purchased > 0 && num_items_purchased < items.length">
{{ num_items_purchased_label }}</template
>
<template v-else-if="num_items_purchased === 0">
You have not purchased any items yet.
</template>
<template v-else>You have bought everything in the shopping cart.</template>
</div>
當 num_items_purchased
在 1 和小於總商品數之間時,模板會顯示 num_items_purchased_label
的值。
當 num_items_purchased
是 0 時,模板會顯示 "You have not purchased any item yet."。
當 num_items_purchased
等於總商品數時,模板會顯示 "You have bought everything in the shopping cart."。
num_items_purchased_label
無法用 $derived
,因此我們使用 $derived.by
來建立一個衍生的 rune。
<script lang="ts">
let items = $state([] as Item[]);
let num_items_purchased = $derived(
items.reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0)
);
let num_items_purchased_label = $derived.by(() => {
const unit = num_items_purchased > 1 ? 'items' : 'item';
return `${num_items_purchased} ${unit} purchased`;
});
</script>
{#if num_items_purchased > 0 && num_items_purchased < items.length}
{num_items_purchased_label}
{:else if num_items_purchased == 0}
You have not purchased any items yet.
{:else}
You have bought everything in the shopping cart.
{/if}
類似於 Vue 3 應用程式,if-else-if-else 控制流程用於顯示 num_items_purchased_label
rune 以及靜態字串。
import { ChangeDetectionStrategy, Component, computed, signal } from '@angular/core';
@Component({
selector: 'app-shopping-cart',
template: `
<div class="header">
@let num = num_items_purchased();
@if (num > 0 && num < items().length) {
{{ num_items_purchased_label() }}
} @else if (num === 0) {
You have not purchased any items yet.
} @else {
You have bought everything in the shopping cart.
}
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ShoppingCartComponent {
items = signal<Item[]>([]);
num_items_purchased = computed(() => this.items().reduce((acc, item) => acc + (item.purchased ? 1 : 0), 0));
num_items_purchased_label = computed(() => {
const unit = this.num_items_purchased() === 1 ? 'item' : 'items';
return `${this.num_items_purchased()} ${unit} purchased`;
});
}
num_items_purchased
值在控制流程中出現了三次。因此,我們將其重構為一個 num
變數。當 num
在 1 和小於總商品數之間時,會顯示 num_items_purchased_label
的值。
當 num
為 0 時,顯示 "You have not purchased any item yet"。
當 num
等於總項目數時,顯示 "You have bought everything in the shopping cart."。