Vue3,用組合編寫更好的程式碼:Async Without Await 模式(4/4)

前端小智 發表於 2022-07-09
人工智慧 Vue

如果能讓非同步程式碼正確工作,它可以大大簡化我們程式碼。但是,處理這種額外的複雜性,特別是與可合一起,可能會令人困惑。這篇文章介紹了無等待的非同步模式。這是一種在組合中編寫非同步程式碼的方法,而不像通常那樣令人頭疼。

無等待的非同步

用組合API編寫非同步行為有時會很麻煩。所有的非同步程式碼必須在任何反應式程式碼之後的設定函式的末端。如果你不這樣做,它可能會干擾你的反應性。

setup函式執行到一個await語句時,它將返回。一旦它返回,該元件就會被掛載,並且應用程式會像往常一樣繼續執行。任何在await之後定義的響應式,無論是 computedwatcher,還是其他什麼,都還沒有被初始化。

這意味著,一個在await之後定義的計算屬性一開始不會被模板使用。相反,只有在非同步程式碼完成,setup 函式完成執行後,它才會存在。

然而,有一種方法可以編寫非同步元件,可以在任何地方使用,而不需要這些麻煩。

const count = ref(0);
// 這種非同步資料獲取不會干擾我們的響應式
const { state } = useAsyncState(fetchData());
const doubleCount = computed(() => count * 2);

實現沒有等待的非同步模式

為了實現這一模式,我們將同步地掛起所有的響應式值。然後,每當非同步程式碼完成後,這些值將被非同步更新。

首先,我們需要把我們的狀態準備好並返回。我們將用一個null的值來初始化,因為我們還不知道這個值是什麼。

export default useMyAsyncComposable(promise) {
  const state = ref(null);
  return state;
}

第二,我們建立一個方法,等待我們的 promise ,然後將結果設定為 state:

const execute = async () => {
  state.value = await promise;
}

每當這個promise 返回時,它就會主動更新我們的state。

現在我們只需要把這個方法新增到組合中。

export default useMyAsyncComposable(promise) {
  const state = ref(null);

  // Add in the execute method...
  const execute = async () => {
    state.value = await promise;
  }

  // ...and execute it!
  execute();

  return state;
}

我們在從useMyAsyncComposable方法返回之前呼叫了execute函式。然而,我們並沒有使用await關鍵字。

當我們停止並等待execute方法中的 promise 時,執行流立即返回到useMyAsyncComposable函式。然後它繼續執行execute()語句並從可組合物件返回。

export default useMyAsyncComposable(promise) {
  const state = ref(null);

  const execute = async () => {
    // 2. 等待 promise 執行完成
    state.value = await promise

    // 5. 一段時間後...
    // Promise 執行完,state 更新
    // execute 執行完成
  }

  // 1. 執行 `execute` 方法
  execute();
  // 3.  await 將控制權返回到這一點上。

  // 4. 返回 state 並繼續執行 "setup" 方法
  return state;
}

promise在後臺執行,因為我們沒有等待它,所以它不會在setup函式中中斷流。我們可以將此可組合放置在任何地方,而不影響響應性。

讓我們看看 VueUse 中一些組合是如何實現這種模式的。

useAsyncState

useAsyncState 可以讓我們在任何地方執行任何非同步方法,並獲得響應性的更新結果。

const { state, isLoading } = useAsyncState(fetchData());

在檢視原始碼時,可以看到它實現了這種精確的模式,但具有更多的特性,並能更好地處理邊界情況。

下面是 useAsyncState 的一個簡化版:

export function useAsyncState(promise, initialState) {
  const state = ref(initialState);
  const isReady = ref(false);
  const isLoading = ref(false);
  const error = ref(undefined);

  async function execute() {
    error.value = undefined;
    isReady.value = false;
    isLoading.value = true;

    try {
      const data = await promise;
      state.value = data;
      isReady.value = true;
    }
    catch (e) {
      error.value = e;
    }

    isLoading.value = false;
  }

  execute();

  return {
    state,
    isReady,
    isLoading,
    error,
  };
}

這個可組合的系統還返回isReady,告訴我們資料何時被取走。我們還得到了isLoadingerror,以跟蹤我們的載入和錯誤狀態。

現在來看看另一個可組合,我認為它有一個迷人的實現方式。

useAsyncQueue

如果傳給useAsyncQueue一個 promise 函式陣列,它會按順序執行每個函式。所以,在開始下一個任務之前,會等待前一個任務的完成。為了使用更靈活,它上一個任務的結果作為輸入傳給下一個任務。

const { result } = useAsyncQueue([getFirstPromise, getSecondPromise]);

下面是一個官網的例子:

const getFirstPromise = () => {
  // Create our first promise
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(1000);
    }, 10);
  });
};

const getSecondPromise = (result) => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(1000 + result);
    }, 20);
  });
};

const { activeIndex, result } = useAsyncQueue([
  getFirstPromise,
  getSecondPromise
]);

即使它在非同步執行程式碼,我們也不需要使用await。即使在內部,可組合的程式也不使用await。相反,我們在 "後臺"執行這些 promise,並讓結果響應式更新。

讓我們看看這個組合是如何工作的。

// 初始一些預設值
const initialResult = Array.from(new Array(tasks.length), () => ({
  state: promiseState.pending,
  data: null,
});

// 將預設值變成響應式
const result = reactive(initialResult);

// 宣告一個響應式的下標
const activeIndex = ref(-1);

主要的功能是由一個reduce來支援的,它逐個處理每個功能

tasks.reduce((prev, curr) => {
  return prev.then((prevRes) => {
    if (result[activeIndex.value]?.state === promiseState.rejected && interrupt) {
      onFinished();
      return;
    }

    return curr(prevRes).then((currentRes) => {
      updateResult(promiseState.fulfilled, currentRes);
      activeIndex.value === tasks.length - 1 && onFinished();
      return currentRes;
    })
  }).catch((e) => {
    updateResult(promiseState.rejected, e);
    onError();
    return e;
  })
}, Promise.resolve());

Reduce 方法有點複雜,我們拆解一下,一個個看:

tasks.reduce((prev, curr) => {
  // ...
}, Promise.resolve());

然後,開始處理每個任務。通過在前一個promise基礎上鍊接一個.then來完成這個任務。如果promise 被拒絕,就提前中止並返回。

if (result[activeIndex.value]?.state === promiseState.rejected && interrupt) {
  onFinished();
  return;
}

如果不提前終止,則執行下一個任務,並傳遞上一個 promise 的結果。我們還呼叫updateResult方法,將其新增到該組合返回的 result 陣列中

return curr(prevRes).then((currentRes) => {
  updateResult(promiseState.fulfilled, currentRes);
  activeIndex.value === tasks.length - 1 && onFinished();
  return currentRes;
});

正如你所看到的,該可組合實現了Async Without Await模式,但該模式只是整個可組合的幾行。所以它不需要很多額外的工作,只要記住把它放在適當的位置

總結

如果我們使用Async Without Await模式,我們可以更容易地使用非同步組合。這種模式可以讓我們把非同步程式碼放在我們想放的地方,而不用擔心破壞響應應性。