iT邦幫忙

2024 iThome 鐵人賽

DAY 13
0
JavaScript

不會 VueUse 而被提分手的我系列 第 13

D-13 useAsyncState 解析與動機 — 取經的路上有你有我

  • 分享至 

  • xImage
  •  

想到了我和程世社季子的感情,取經路上有困難,但最終的結果是值得的。我們的每一次成長與等待,都讓我更加相信,無論過程多麼艱辛,最終會走到一起。程世社季子,取經的路上有你有我,我願意陪你一起經歷這段旅程,直到最終我們的心靈再度相通。
https://ithelp.ithome.com.tw/upload/images/20240926/20162115bA5p9kQIfa.png

前置知識

1. 什麼是 ref?什麼又是 shallowRef?

  • ref: 建立一個響應式引用,對於物件類型會進行深層響應式轉換。
  • shallowRef: 建立一個淺層響應式引用,只有頂層屬性是響應式的。

比較:

const obj = { nested: { count: 0 } };
const deepRef = ref(obj);
const shallowRef = shallowRef(obj);

// 深層響應
deepRef.value.nested.count++; // 觸發更新

// 淺層響應
shallowRef.value.nested.count++; // 不觸發更新
shallowRef.value = { nested: { count: 1 } }; // 觸發更新

使用 shallowRef 可以在處理大型物件時提高性能,但需要注意深層屬性變化不會觸發更新。

2. promiseTimeout

promiseTimeout 是一個工具函數,用於建立一個延遲指定時間後解決的 Promise:

function promiseTimeout(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms));
}

使用範例:

async function delayedGreeting() {
  console.log("Wait for it...");
  await promiseTimeout(2000);
  console.log("Hello!");
}

3. until

until 是一個用於建立一個 Promise,該 Promise 會等待直到指定的響應式引用(Ref)達到預期的值才解決,主要用於在異步操作中等待某個條件成立。

使用範例:

const isReady = ref(false);
setTimeout(() => { isReady.value = true; }, 1000);

await until(isReady).toBe(true);
console.log("Ready!");

useAsyncState 詳細解析

現在,讓我們更深入地分析 useAsyncState 的實現:

1. 型別定義的深度解析

export interface UseAsyncStateReturnBase<Data, Params extends any[], Shallow extends boolean> {
  state: Shallow extends true ? Ref<Data> : Ref<UnwrapRef<Data>>;
  isReady: Ref<boolean>;
  isLoading: Ref<boolean>;
  error: Ref<unknown>;
  execute: (delay?: number, ...args: Params) => Promise<Data>;
}

這個介面使用了條件類型 Shallow extends true ? Ref<Data> : Ref<UnwrapRef<Data>> 來決定 state 的類型。如果 Shallow 為 true,則直接使用 Ref<Data>,否則使用 Ref<UnwrapRef<Data>>UnwrapRef 是一個工具類型,用於解包 ref 的值類型。

補充小知識 UnwrapRef

UnwrapRef 是一個用於『解包』嵌套的 ref 類型。它的主要作用是去除多餘的 ref 包裝,特別是在處理複雜的嵌套結構時。

Ref vs Ref<UnwrapRef>

  1. Data 是簡單類型時(如 string, number, boolean):
    • Ref<Data>Ref<UnwrapRef<Data>> 是相同的。
  2. Data 是物件或陣列,並且可能包含嵌套的 ref 時:
    • Ref<Data> 保留了原始的結構,包括可能存在的嵌套 ref。
    • Ref<UnwrapRef<Data>> 會解開所有嵌套的 ref,給出一個"扁平化"的類型。

實際例子

讓我們看一些例子來說明這個區別:

type NestedData = {
  count: Ref<number>,
  items: Ref<string[]>
}

// 使用 Ref<Data>
type WithRef = Ref<NestedData>
// 結果:Ref<{ count: Ref<number>, items: Ref<string[]> }>

// 使用 Ref<UnwrapRef<Data>>
type WithUnwrapRef = Ref<UnwrapRef<NestedData>>
// 結果:Ref<{ count: number, items: string[] }>

在這個例子中:

  • WithRef 保留了 countitems 作為 Ref
  • WithUnwrapRefcountitems 解包,去掉了內部的 Ref 包裝。

為什麼這很重要?

  1. 類型安全UnwrapRef 確保在處理複雜的嵌套 ref 結構時能獲得正確的類型。
  2. 使用便利性:使用 shallowRef 時,解開內部 ref 結構可避免多次使用 .value,簡化值的訪問。
  3. 一致性:它為處理可能包含嵌套 ref 的複雜資料結構提供了統一的方法。

在 useAsyncState 中 UseAsyncStateReturnBase 的 state

  • Shallowtrue 時,使用 Ref<Data>。這是因為 shallowRef 不處理嵌套的響應性。
  • Shallowfalse 時,使用 Ref<UnwrapRef<Data>>。這反映了 ref 會遞迴地將嵌套值轉換為響應式的行為。

這個設計很厲害的確保了類型定義與實際運行時行為的一致性,無論是使用淺層還是深層的響應式引用。真不愧是我大哥

2. 選項解構和默認值設定

const {
  immediate = true,
  delay = 0,
  onError = noop,
  onSuccess = noop,
  resetOnExecute = true,
  shallow = true,
  throwError,
} = options ?? {}

這裡使用了解構賦值和默認值設定。noop 是一個空函數 () => {},用作默認的錯誤和成功回調。

3. 狀態初始化

// 可以回去看前置知識的第一點:什麼是 ref?什麼又是 shallowRef?
const state = shallow ? shallowRef(initialState) : ref(initialState)
const isReady = ref(false)
const isLoading = ref(false)
const error = shallowRef<unknown | undefined>(undefined)

這裡根據 shallow 選項決定使用 shallowRef 還是 ref 來建立 state。使用 shallowRef 可以在處理大型物件時提高性能。

4. execute 函數的深入分析

async function execute(delay = 0, ...args: any[]) {
  if (resetOnExecute)
    state.value = initialState
  error.value = undefined
  isReady.value = false
  isLoading.value = true

  if (delay > 0)
    await promiseTimeout(delay)

  const _promise = typeof promise === 'function'
    ? promise(...args as Params)
    : promise

  try {
    const data = await _promise
    state.value = data
    isReady.value = true
    onSuccess(data)
  }
  catch (e) {
    error.value = e
    onError(e)
    if (throwError)
      throw e
  }
  finally {
    isLoading.value = false
  }

  return state.value as Data
}

這個函數包含了幾個關鍵點:

  1. 狀態重置: 如果 resetOnExecute 為 true,則重置狀態。
  2. 延遲執行: 使用 promiseTimeout 實現延遲。
  3. Promise 處理: 支持傳入 Promise 或返回 Promise 的函數。
  4. 錯誤處理: 捕獲錯誤並更新狀態,可選擇是否拋出錯誤。
  5. 狀態更新: 在不同階段更新 isLoadingisReadyerror 等狀態。

5. Promise-like 接口的實現

這個寫法很有趣,不僅保留了async await 還保留了 then (可以看下面的實際使用範例)

function waitUntilIsLoaded() {
  return new Promise<UseAsyncStateReturnBase<Data, Params, Shallow>>((resolve, reject) => {
    until(isLoading).toBe(false).then(() => resolve(shell)).catch(reject)
  })
}

return {
  ...shell,
  then(onFulfilled, onRejected) {
    return waitUntilIsLoaded()
      .then(onFulfilled, onRejected)
  },
}

這部分實現了 Promise-like 接口:

  1. waitUntilIsLoaded 函數建立一個 Promise,該 Promise 在 isLoading 變為 false 時解決。
  2. 返回的物件包含一個 then 方法,使其成為 thenable。
  3. 這允許使用者像使用 Promise 一樣使用 useAsyncState 的返回值。

實際使用範例

const { state, isLoading, error, execute } = useAsyncState(
  async () => {
    const response = await fetch('https://api.example.com/data');
    return response.json();
  },
  { data: null },
  { immediate: true, delay: 1000 }
);

// 使用 Promise-like 接口
useAsyncState(/* ... */).then(({ state }) => {
  console.log(state.value);
});

// 手動執行
const refreshData = () => {
  execute();
};

這個例子示範了如何使用 useAsyncState 來管理 API 請求的狀態,包括自動執行、延遲執行、錯誤處理,以及手動更新資料。

總結

useAsyncState 結合了 Vue 的響應式系統、TypeScript 的特性和 JavaScript 的異步處理。它提供了一個強大且靈活的方式,來管理異步操作的狀態,同時保持了良好的類型安全性和錯誤處理能力。這個實現不僅功能豐富,而且考慮到了性能優化(通過 shallowRef)和使用便利性(Promise-like 接口)。


上一篇
D-12 useAsyncState 文件說明與範例 — 解析取經的過程
系列文
不會 VueUse 而被提分手的我13
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言