iT邦幫忙

2025 iThome 鐵人賽

DAY 21
0

快速回顧

在前面幾個章節中,我們完成了對 React 環境的整合應用,接者我們來嘗試導入 Vue 環境使用。

本篇目標

把我們的 signal / computed 安全地接到 Vue 3 Composition API,讓模板可直接使用,同時維持我們自家 reactive graph 的行為(push 標髒標記 + pull 重算)、避免雙重依賴與時序打架。

設計原則

  • 單向橋接:只把「值」同步到 Vue ref;不要把 Vue 的 reactivity 反向接回你的圖,避免排程繞圈。
  • 生命週期清楚:在 onUnmounted 清理 createEffect 與(如果有)computed.dispose()
  • 快照來源:初始化用 peek()(不追蹤、必要時 lazy 重算),在 our effect 內用 get()(建立依賴)。
  • 相同心智useComputedRef 的 callback 必須讀 signal.get() 才會被追蹤;若只是純 Vue 計算,請用 Vue 的 computed

誰依賴誰(VUE 版本)

https://ithelp.ithome.com.tw/upload/images/20250822/20129020XhFTjpbZ0c.png

  • 模板與 watch* 只看 Vue ref(透過 useSignalRef),不直接 .get()
  • 我們的 computed 在 callback 裡讀 signal.get() 建依賴。

實作 Adapter

import { shallowRef, onUnmounted, type Ref } from "vue";
import { createEffect, onCleanup } from "../core/effect.js";
import { computed as coreComputed } from "../core/computed.js";

type Readable<T> = { get(): T; peek(): T };

// 將 signal/computed 映射為 Vue ref(tear-free;由我們的 effect 推動)
export function useSignalRef<T>(src: Readable<T>): Ref<T> {
  const r = shallowRef<T>(src.peek()) as Ref<T>; // 初始快照(不追蹤)
  const stop = createEffect(() => {
    // 在追蹤上下文中讀取,值變動時同步寫入 Vue ref
    r.value = src.get();
    onCleanup(() => {
      // optional:保留擴充(例如取消計時器),目前無需特別清理
    });
  });
  onUnmounted(() => stop()); // 元件卸載即解除訂閱
  return r;
}

// 在元件生命週期內建立你的 computed,並以 Vue ref 暴露
export function useComputedRef<T>(
  fn: () => T,
  equals: (a: T, b: T) => boolean = Object.is
): Ref<T> {
  // 注意:fn 內要讀 signal.get() 才會建立依賴
  const memo = coreComputed(fn, equals);
  const r = useSignalRef<T>({ get: () => memo.get(), peek: () => memo.peek() });
  onUnmounted(() => memo.dispose?.());
  return r;
}

為什麼用 shallowRef
我們已在 core 內做等值判斷與快取,Vue 端只需感知「值是否改變」。深層追蹤交給 core 的等值策略(equals)處理。

更新與清理時序

https://ithelp.ithome.com.tw/upload/images/20250825/20129020M0GJlMUN4K.png

  • 初始用 peek() 拿快照;在 effect 中用 get() 建依賴。
  • onUnmounted → stop(),確保不殘留訂閱。

使用範例(SFC)

Counter:signal + 衍生值

<script setup lang="ts">
import { signal } from "../core/signal.js";
import { useSignalRef, useComputedRef } from "./vue-adapter";

const countSig = signal(0);

const count = useSignalRef(countSig); // Vue ref
const doubled = useComputedRef(() => countSig.get() * 2); // 讀 .get() 建依賴

const inc = () => countSig.set(v => v + 1);
</script>

<template>
  <p>{{ count }} / {{ doubled }}</p>
  <button @click="inc">+1</button>
</template>

Selector:只關心部分欄位

<script setup lang="ts">
import { signal } from "../core/signal.js";
import { useComputedRef } from "./vue-adapter";

const userSig = signal({ id: 1, name: "Ada", age: 37 });

// 只暴露 name;即使物件其它欄位變了,只要 name 相等就不觸發模板更新
const nameRef = useComputedRef(
  () => userSig.get().name,
  (a, b) => a === b
);
</script>

<template>
  <h2>{{ nameRef }}</h2>
</template>

模組域衍生值 vs 元件域衍生值

  • 元件域:用 useComputedRef,卸載時自動 dispose()
  • 模組域:若建立全域 computed,務必在確定不再使用時手動 dispose()

交互操作:和 Vue 的 watch / watchEffect 如何分工?

要觀察你的 signal/computed

先用 useSignalRef 變成 Vue ref,再用 watch / watchEffect
這樣 Vue 只觀察「值」,不會參與你的依賴圖,避免雙重追蹤。

const price = useSignalRef(priceSig);

watch(price, (nv, ov) => {
  console.log("price changed:", ov, "→", nv);
});

不要在 watchEffect 內直接讀 signal.get(),那會讓 Vue 的追蹤也加入我們的圖,可能造成不必要重跑與生命週期打結。

責任分工

https://ithelp.ithome.com.tw/upload/images/20250825/20129020lzBZtKfjGO.png

  • 資料/業務:放 our effect(creactEffect)。
  • UI/DOM:放 onMounted / watch
  • watch 要看 useSignalRef 轉出的 Vue ref,不要直接使用 .get()

常見陷阱

useComputedRef(() => ref.value * 2)

  • 問題:這只是純 Vue 計算,不會讓你的 computed 進入 core 的 reactive graph。
  • 修正:在 callback 內讀 signal.get();你如果真的只要 Vue 計算,直接用 Vue 的 computed

在模板或 setup 直讀 signal.get()

  • 問題:這是一次性快照,不會自動更新。
  • 修正:一律用 useSignalRef 暴露為 ref 再給模板。

同一份資料同時被 Vue ref 與 signal 雙向驅動

  • 問題:容易產生排程繞圈與預期外重跑。
  • 修正:確立唯一「資料源」(建議以 signal 為主),Vue 端只「顯示」。

何時用誰?

  • 要把 core 狀態給模板useSignalRef(signalOrComputed)
  • 要在元件內建衍生值useComputedRef(() => signal.get() + ...)
  • 只做 Vue 內部計算/顯示 → 用 Vue 的 computed
  • 要觀察變化 → 先 useSignalRef,再用 watch / watchEffect
  • 避免:在 Vue 的 effect 內直接 .get();在 useComputedRefref.value

結語

如果是從前面一路追過來的朋友,應該很熟悉這套流程了,對於我們開發的 signal 來說,框架只剩下與 UI 綁定渲染的功能,所以製作 Adapter 來說,就是多做多熟練,相較於 React 的特殊性,Vue 這樣採用模板渲染的就會更加貼近我們的心智模型,只要掌握以下就能順順使用了:

Vue 端只把 值(ref) 展現出來;依賴追蹤與快取仍交給我們的 signal 系統。
這樣做能避免雙重依賴與時序打架,保留 push 標髒標記 + pull 重算 的核心優勢。

你現在已能把 signal 穩定地接進 Vue,下一篇我們把交互操作與進階場景補齊。


上一篇
React 應用(VI):高頻陷阱與最佳實務(II)
系列文
Reactivity 小技巧大變革:掌握 Signals 就這麼簡單!21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言