ÿÿ深入淺出 pinia(三):createSetupStore | Alex Liu

深入淺出 pinia(三):createSetupStore

• 24 min read

Pinia æ˜¯ç›®å‰ Vue 官方首推的狀態管ç†å·¥å…·ã€‚é€™ç³»åˆ—æ–‡ç« ä¸æœƒç‰¹åˆ¥è‘—é‡åœ¨å¦‚何使用 Pinia è€Œæ˜¯æ·±å…¥å‰–æž Pinia çš„åŽŸå§‹ç¢¼ï¼Œç ”ç©¶å®ƒçš„è¨­è¨ˆï¼Œå¾žä¸­å¸æ”¶å¯¶è²´çš„經驗。在上一篇的內容我們先看了 Options Store 的實作,發先最後會é€éŽ Setup Store å®Œæˆæ•´å€‹ Store 的建立。因此接下來會更深入核心了解 Setup Store 內部的實作。

å‰è¨€

本篇的 pinia 版本為 2.1.3

這系列一共有三篇文章,分別是:

  1. 深入淺出 pinia(一):createPiniaã€defineStore
  2. 深入淺出 pinia(二):createOptionsStore
  3. 深入淺出 pinia(三):createSetupStore

如果熟悉 Composition API 的話,Setup Store 在使用上會有éžå¸¸ä¸€è‡´çš„體驗。而 createSetupStore 我個人èªç‚ºç®—是 Pinia 中最核心的部分,幾乎所有的功能都是在這裡實作的。在本篇將會深入了解 Setup Store 的實作細節。

Setup Store

createSetupStore 實作å«åž‹åˆ¥ã€HMRã€è¨»è§£ç­‰æœ‰ 500 多行,也因為大部分會用到的 api 都集中在這裡é¢ï¼Œæ‰€ä»¥æˆ‘æœƒå…ˆåˆ—å‡ºé€™è£¡é¢æœ‰å¯¦åšåˆ°çš„ api,並且在後é¢é€ä¸€è§£é‡‹ã€‚

api功能說明
store.$onAction設定一個 callback function,在 action 被執行å‰èª¿ç”¨ã€‚
store.$subscribe設定一個 callback function,當 state 更新時調用。它會回傳一個用來移除該 callback function 的 function
store.$patchæ›´æ–° state,å¯ä»¥ç›´æŽ¥è³¦å€¼éƒ¨åˆ†æ–°çš„狀態或是使用 callback å–å¾—ç•¶å‰ state 並修改。
store.$stateç•¶å‰ store çš„ state,如果å°ä»–直接設定 state,內部會使用 store.$patch æ›´æ–°
store.$reseté‡ç½®æ•´å€‹ store çš„ stateï¼Œåªæ˜¯é©ç”¨æ–¼ Options Store。
store.$dispose清除整個 store 的「副作用ã€ï¼Œä¸¦ä¸”å°‡ store 從 Pinia Instance 上將該 store 刪除。

ä¸éŽåœ¨ä¸€ä¸€ä»‹ç´¹ api 之å‰ï¼Œæˆ‘們還是需è¦åˆå§‹åŒ– state。

åˆå§‹åŒ– state

這裡è¦åšçš„事情大致與 createOptionsStore 相åŒï¼Œä¸éŽå› ç‚º Option Store çš„ state å…ˆå‰å·²ç¶“é€éŽ state function 來åˆå§‹åŒ–,所以如果是 Options Store 這裡ä¸éœ€è¦å†åšä¸€æ¬¡ã€‚

å¦å¤–在 Setup Store 我們沒有一個專們å–å¾— state çš„ state function,所以我們無法因為 initialState ä¸å­˜åœ¨è€Œé€éŽ state() 來å–å¾— state。所以 initialState ä¸å­˜åœ¨æ™‚,我們先將 pinia.state.value[$id] 設定æˆä¸€å€‹ç©ºç‰©ä»¶ã€‚

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const initialState = pinia.state.value[$id]

  if (!isOptionsStore && !initialState) {
    if (isVue2) {
      set(pinia.state.value, $id, {})
    } else {
      pinia.state.value[$id] = {}
    }
  }

}

接下來我們å¯ä»¥æŠŠ setup function 回傳的 state 一個一個的寫進 pinia.state.value[$id] 中,在整個 setup function 回傳的物件中,我們å¯ä»¥é€éŽ isRefã€isComputedã€isReactive 來判斷是 state 還是 getter 或 action。

判斷是å¦ç‚º state çš„æ¢ä»¶å¦‚下:

  1. 是 Ref 䏔䏿˜¯ Computed。
  2. 是 Reactive。

ä¾ç…§æ¢ä»¶æˆ‘們å¯ä»¥å¯«å‡ºåˆ†é¡žç¨‹å¼ç¢¼ï¼Œå¦‚下:

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const setupStore = setup()

  for (const key in setupStore) {
    const prop = setupStore[key]

    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // state
    } else if (typeof prop === 'function') {
      // action
    }
  }
}

接著我們把 setupStore 寫進 pinia.state.value[$id] 裡é¢ã€‚但是我們已經知é“這裡有一個å•題:如果我們直接將 setupStore 寫進 pinia.state.value[$id],那麼在 Server Side Render 時就有機會é‡åˆ° hydration error。

為了é¿å… hydration error æˆ‘å€‘éœ€è¦æª¢æŸ¥ initialState[key] 是å¦å­˜åœ¨ï¼Œå¦‚果存在,就使沿用,å之則使用 setupStore[key] 的值。

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const setupStore = setup()

  for (const key in setupStore) {
    const prop = setupStore[key]

    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // Options Store çš„ state 會在 state function 中åˆå§‹åŒ–,所以這裡ä¸éœ€è¦å†åˆå§‹åŒ–
      if (!isOptionsStore) {
        if (initialState) {
          if (isRef(prop)) {
            prop.value = initialState[key]
          } else {
            // reactive ä¸èƒ½ç›´æŽ¥è¤‡å¯«ï¼Œæ‰€ä»¥é€™é‚Šè¦é€éŽ `mergeReactiveObjects` 使用éžè¿´çš„æ–¹å¼ä¾†è¤‡å¯«ã€‚
            mergeReactiveObjects(prop, initialState[key])
          }
        }

        if (isVue2) {
          set(pinia.state.value[$id], key, prop)
        } else {
          pinia.state.value[$id][key] = prop
        }
      }
    } else if (typeof prop === 'function') {
      // action
      // ...
    }
  }
}

å¦å¤–第一篇有æåˆ° Effect Scope,æ¯ä¸€å€‹ Store çš„ setup 都會在 Pinia instance 上的 Effect Scope 中建立自己的 Effect Scope,形æˆä¸€å€‹æ¨¹ç‹€çš„ Effect Scopeã€‚é€™æ¨£çš„ç”¨æ„æ˜¯ä¸€ä½†ç•¶ Pinia instance 被銷毀時,å¯ä»¥é€éŽé€™å€‹æ¨¹ç‹€çš„ Effect Scope 關係來清除所有的副作用。

setup function 中除了å¯ä»¥ä½¿ç”¨ computed 之外還å¯ä»¥å®šç¾© watchï¼Œé€™äº›éƒ½æœƒæœ‰å‰¯ä½œç”¨éœ€è¦æ¸…除,所以我們需è¦ä¸€å€‹å°ˆå€‘çš„ Effect Scope 來收集這些副作用,我們將 setupStore 的部分改寫æˆé€™æ¨£ï¼š


function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const setupStore = pinia._e.run(() => {
    scope = effectScope()
    return scope.run(() => setup())
  })

  //
}

這樣我們就å¯ä»¥æ”¶é›†åˆ° setupStore 中所有的副作用了。

åŒ…è£ Actions

在剛剛 setupStore 的物件中,我們挑出了 state ä»¥åŠ getter。而剩下的如果型別為 function 的話,就會被當作 actions 來處ç†ã€‚

基本上 action 是å¯ä»¥è¢«ç›´æŽ¥ä½¿ç”¨ä¸éœ€é€²éŽç‰¹åˆ¥è™•ç†çš„,但是在 Pinia 中我們需è¦å° action åšåŒ…è£ï¼Œå› ç‚º Pinia æä¾›äº†ä¸€å€‹ API å¯ä»¥è®“我們在 action 執行å‰å¾Œèª¿ç”¨ callback function,這個 API 就是 store.$onAction。

所以們å¯ä»¥å®šç¾©ä¸€å€‹ä¸¦ä½¿ç”¨ wrapAction ä¾†åŒ…è£ actions 負責攔截æ¯ä¸€å€‹ action 的執行。

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  let scope

  // ...

  const setupStore = pinia._e.run(() => {
    scope = effectScope()
    return scope.run(() => setup())
  })!

  for (const key in setupStore) {
    const prop = setupStore[key]

    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // state
    } else if (typeof prop === 'function') {
      const actionValue = wrapAction(key, prop)

      if (isVue2) {
        set(setupStore, key, actionValue)
      } else {
        setupStore[key] = actionValue
      }
    }
  }
}

wrapAction è¦æ€Žéº¼æ””截 actions 的值行呢?

API: store.$onAction

開始分æžå¯¦ä½œå‰æˆ‘們å¯ä»¥å…ˆçœ‹çœ‹é€™å€‹ API 的使用方å¼ã€‚

// 回傳一個 function 用來移除 callback function
const removeSubscribe = store.$onAction(
  ({
    name,    // action å稱(物件上的屬性å稱)
    store,   // Store instance
    args,    // 調用 action æ™‚å‚³å…¥çš„åƒæ•¸
    after,   // 新增在 action æˆåŠŸå¾Œèª¿ç”¨çš„ callback function
    onError, // 新增在 action 失敗後調用的 callback function
  }) => {
    // action 被調用時會觸發

    after((result) => {
      // action æˆåŠŸå¾Œå¯ä»¥åœ¨é€™è£¡åšä¸€äº›äº‹æƒ…
    })

    onError((error) => {
      // action 失敗後å¯ä»¥åœ¨é€™è£¡åšä¸€äº›äº‹æƒ…
    })
  }
)

ç‚ºäº†å¯¦ç¾ store.$onAction 我們必須將原本平凡無奇的 action function 包è£èµ·ä¾†ï¼Œä¸¦ä¸”在執行 action å‰å¾Œèª¿ç”¨ callback function。

所以我們需è¦ï¼š

  1. $onAction 用來æœé›† callback function,並且回傳一個 function 用來移除 callback function。
  2. actionSubscriptions 用來存放所有的 callback function。
  3. wrapActionï¼Œç”¨ä¾†åŒ…è£ action function,並回傳一個包è£å¾Œçš„ function。

並且在執行 action å‰èª¿ç”¨ actionSubscriptions 中的所有 callback function。

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const actionSubscriptions = []

  const store = reactive({
    // ...
    $onAction(callback) {
      actionSubscriptions.push(callback)
      return () => {
        const index = actionSubscriptions.indexOf(callback)
        if (index > -1) {
          actionSubscriptions.splice(index, 1)
        }
      }
    },
  })

  // â¬‡ï¸ åŒ…è£ action function
  function wrapAction(name, action) {
    return function () {
      setActivePinia(pinia) // 這個在上一篇的 getter 中有æåˆ°ï¼Œé€™è£¡å°±ä¸å¤šèªªäº†

      const args = Array.from(arguments)

      actionSubscriptions.slice().forEach((callback) => {
        callback({
          name,
          store,
          args,
        })
      })
    }
  }
}

但這裡還少了一些æ±è¥¿ï¼Œå°±æ˜¯ action 執行後的 callback function,以åŠç™¼ç”ŸéŒ¯èª¤æ™‚調用的 callback functionã€‚é€™è£¡éœ€è¦æ–°å¢žå…©å€‹é™£åˆ—,與兩個新增 callback function 的方法,分別是:

  1. afterCallbackList 存放 action 執行後的 callback function。
  2. onErrorCallbackList 存放 action 執行失敗後的 callback function。
  3. after 新增一個 callback function 到 afterCallbackList。
  4. onError 新增一個 callback function 到 onErrorCallbackList。
function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const actionSubscriptions = []

  // ...

  function wrapAction(name, action) {
    return function () {
      setActivePinia(pinia)

      const args = Array.from(arguments)

      const afterCallbackList = []
      const onErrorCallbackList = []
      
      function after(callback) {
        afterCallbackList.push(callback)
      }
      function onError(callback) {
        onErrorCallbackList.push(callback)
      }

      actionSubscriptions.slice().forEach((callback) => {
        callback({
          name,
          store,
          args,
          after,
          onError,
        })
      })
    }
  }
}

所以我們åªè¦åœ¨åŸ·è¡Œæžœå¾Œï¼Œä¾ç…§æˆåŠŸæ–¼å¦èª¿ç”¨ afterCallbackList 或是 onErrorCallbackList 中的 callback function å°±å¯ä»¥äº†ã€‚

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  // ...

  function wrapAction(name, action) {
    return function (this: any) {
      // ...

      let maybePromiseResult
      try {
        maybePromiseResult = action.apply(this && this.$id === $id ? this : store, args)
      } catch (error) {
        triggerSubscriptions(onErrorCallbackList, error)
        throw error
      }

      if (maybePromiseResult instanceof Promise) {
        return maybePromiseResult
          .then((value) => {
            afterCallbackList.slice().forEach((callback) => {
              callback(value)
            })
            
            return value
          })
          .catch((error) => {
            onErrorCallbackList.slice().forEach((callback) => {
              callback(error)
            })

            return Promise.reject(error)
          })
      }

      afterCallbackList.slice().forEach((callback) => {
        callback(maybePromiseResult)
      })

      return maybePromiseResult
    }
  }
}

API: store.$subscribe

一樣先看看這個 API 的使用方å¼ã€‚

const removeSubscribe = store.$subscribe((mutation, state) => {
  // import type { MutationType } from 'pinia'
  // 'direct' | 'patch object' | 'patch function'
  mutation.type 

  // 和 store.$id 一樣
  mutation.storeId

  // åªæœ‰åœ¨ mutation.type === 'patch object' 的時候æ‰å¯ä»¥ä½¿ç”¨
  mutation.payload

  // store çš„ state
  state
}, {
  // 是å¦è¦åœ¨éŠ·æ¯€ store 時自動移除 callback function
  detached: true,

  // 這邊就是 `watch` 的 options
  flush: 'post',
  immediate: false,
  deep: true, // <--- é è¨­ç‚º true
})

ä¾å¾ªè‘— store.$onAction 的實作é‚輯,我們å¯ä»¥å¾ˆå¿«åœ°åˆæ­¥å¯¦ä½œå‡º store.$subscribe。

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const subscriptions: SubscriptionCallback<S>[] = []

  const store = reactive({
    // ...
    $subscribe(callback) {
      subscriptions.push(callback)

      return () => {
        const index = subscriptions.indexOf(callback)
        if (index > -1) {
          subscriptions.splice(index, 1)
        }
      }
    },
  })
}

ä¸éŽæˆ‘å€‘é‚„éœ€è¦æ™‚åš { detached: true } 的功能,當 detached 為 true æ™‚ï¼Œä»–å°±ã€Œä¸æœƒã€åœ¨éŠ·æ¯€ store 時自動移除 callback function。

<script setup>
const store = useStore()

// å³ä½¿åœ¨ component 銷毀後,這訂個 subscription 也會ä¿ç•™
store.$subscribe(callback, { detached: true })
</script>

為了時åšé€™å€‹åŠŸèƒ½ï¼Œæˆ‘å€‘éœ€è¦åœ¨ store.$subscribe 中加入一些é‚輯處ç†ã€‚並且使用當 Effect Scope 被銷毀時會觸發的 hook:onScopeDispose。onScopeDispose å¯ä»¥æƒ³åƒåƒæ˜¯ onUnmounted,但是他是在 Effect Scope 被銷毀時觸發,而æ¯å€‹å…ƒä»¶éƒ½æœ‰è‡ªå·±çš„ Effect Scoped,所以元件在銷毀時也會銷毀自身的 Effect Scope 。

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const subscriptions: SubscriptionCallback<S>[] = []

  const store = reactive({
    // ...
    $subscribe(callback, options) {
      subscriptions.push(callback)

      const removeSubscription () => {
        const index = subscriptions.indexOf(callback)
        if (index > -1) {
          subscriptions.splice(index, 1)
        }
      }

      // ä¾ç…§ä¸Šé¢ç¯„例 
      // 這裡的 Current Scope 為該 component 的 Effect Scope
      if (!options?.detached && getCurrentScope()) {

        // 當 Current Scope 銷毀時執行 removeSubscription
        onScopeDispose(removeSubscription)
      }

      return removeSubscription
    },
  })
}

觸發 subscriptions 中的 callback function 有兩種方å¼ï¼Œå…¶ä¸­ä¸€ç¨®å°±æ˜¯ç›´æŽ¥ä¿®æ”¹ state,例如:

const store = useStore()

// 直接修改 state
store.count++

è¦è£œæ‰ç›´æŽ¥ä¿®æ”¹ state 的最簡單方法就是 watch。所以我們å¯ä»¥å° $subscribe ç¨å¾®åŠ å·¥ï¼Œã€‚

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  let scope!: EffectScope

  const subscriptions: SubscriptionCallback<S>[] = []

  const store = reactive({
    // ...
    $subscribe(callback, options) {
      subscriptions.push(callback)

      const stopWatch = scope.run(() => {
        watch(
          () => pinia.state.value[$id],
          (state) => {
            callback({ storeId: $id, type: 'direct' }, state)
          },
          //                â¬‡ï¸ é è¨­ deep 為 true
          Object.assign({}, $subscribeOptions, options)
        )
      })

      return () => {
        const index = subscriptions.indexOf(callback)
        if (index > -1) {
          subscriptions.splice(index, 1)
          stopWatch()
        }
      }
    },
  })
}

這樣就å¯ä»¥è£œæ‰åˆ°ç›´æŽ¥ä¿®æ”¹ state 的行為了。

å¦å¤–一種觸發 subscriptions callback function çš„åšæ–¹å¼å‰‡æ˜¯é€éŽ store.$patch 修改 state。

儘管這裡我們還沒有看到 store.$patch 的實作,但為了完整了解 store.$subscribe 的實作,我們先æ’入一點點 store.$patch 的實ç¾ã€‚

è¦å¯¦ç¾é€éŽ store.$patch 來觸發 store.$subscribe çš„ callback function,其實很簡單,實作方法就跟å‰é¢æåˆ°çš„ store.$onAction 一樣,åªè¦åœ¨ store.$patch 被呼使用時將 subscriptions 中的 callback function é€ä¸€åŸ·è¡Œå°±å¯ä»¥äº†ã€‚

function createSetupStore($id, setup, options, pinia, isOptionsStore) {

  // override
  function $patch(stateMutation: (state: UnwrapRef<S>) => void): void
  function $patch(partialState: _DeepPartial<UnwrapRef<S>>): void
  function $patch(partialStateOrMutator: (state: UnwrapRef<S>) => void | _DeepPartial<UnwrapRef<S>>): void {
    let subscriptionMutation: SubscriptionCallbackMutation<S>

    if (typeof partialStateOrMutator === 'function') {
      // 這裡修改 state,先略

      subscriptionMutation = {
        type: 'patch function',
        storeId: $id,
      }
    } else {
      // 這裡修改 state,先略

      subscriptionMutation = {
        type: 'patch object',
        storeId: $id,

        // åªæœ‰åœ¨ mutation.type === 'patch object' 的時候æ‰å¯ä»¥ä½¿ç”¨
        payload: partialStateOrMutator,
      }
    }

    // ... 

    // 執行所有 callback function
    subscriptions.slice().forEach((callback) => {
      callback(subscriptionMutation, pinia.state.value[$id])
    })
  }
}

到這裡 store.$subscribe 的實作就大致完æˆäº†ã€‚

但我們會é‡åˆ°ä¸€å€‹å•題,當我們é€éŽ store.$patch 修改 state 時,subscriptions 會被觸發兩次。

store.$subscribe((mutation, state) => {
  // 這裡會執行兩次
  // mutation.type === 'patch object'
  // mutation.type === 'direct'
  console.log('mutation', mutation)
  console.log('state', state)
})

store.$patch({
  name: 'Pinia',
  count: 1,
})

原本寫在 $subscribe 中的 watch 也因為 state 的變化多執行了一次。所以我們需è¦ä¸€å€‹é–‹é—œï¼Œç•¶ state 是é€éŽ store.$patch 修改時在 watch 中ä¸è¦åŸ·è¡Œ subscribes 中的 callback function。

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  let isListening: boolean
  let isSyncListening: boolean

  function $patch(partialStateOrMutator: (state: _DeepPartial<UnwrapRef<S>> | (UnwrapRef<S>) => void)): void {
    isListening = isSyncListening = false

    let subscriptionMutation: SubscriptionCallbackMutation<S>

    if (typeof partialStateOrMutator === 'function') {
      // 這裡修改 state,先略
      // å‰é¢æŠŠ watch çš„é–‹é—œé—œæŽ‰æ‰€ä»¥é€™è£¡çš„ä¿®æ”¹ä¸æœƒè§¸ç™¼ watch

      subscriptionMutation = {
        type: 'patch function',
        storeId: $id,
      }
    } else {
      // 這裡修改 state,先略
      // å‰é¢æŠŠ watch çš„é–‹é—œé—œæŽ‰æ‰€ä»¥é€™è£¡çš„ä¿®æ”¹ä¸æœƒè§¸ç™¼ watch

      subscriptionMutation = {
        type: 'patch object',
        storeId: $id,

        // åªæœ‰åœ¨ mutation.type === 'patch object' 的時候æ‰å¯ä»¥ä½¿ç”¨
        payload: partialStateOrMutator,
      }
    }

    // ... 

    nextTick().then(() => {
        // 打開 pre 跟 post 的 watch 開關
       isListening = true
    })

    // 打開 sync watch 開關
    isSyncListening = true

    subscriptions.slice().forEach((callback) => {
      callback(subscriptionMutation, pinia.state.value[$id])
    })
  }
}

並且 $subscribe 中的 watch 也è¦åŠ ä¸Šé€™å€‹é–‹é—œã€‚

const store = reactive({
  $subscribe(callback, options) {
    // ç•¥

    const stopWatch = scope.run(() => {
      watch(
        () => pinia.state.value[$id],
        (state) => {
          if (options.flush === 'sync' ? isSyncListening : isListening) {
            // 執行 callback
          }
        },
        Object.assign({}, $subscribeOptions, options)
      )
    })

    return () => {
      // ...
    }
  },
})

按照 watch çš„ APIï¼Œç¬¬ä¸‰å€‹åƒæ•¸ options 中的 flush å¯ä»¥è¨­å®š watch çš„ callback function 在何時被執行,é è¨­è¡Œç‚ºç‚ºæº–備更新畫é¢å‰ï¼špre。

isListening 是用來控制 flush 為 pre çš„ watch,因此 isListening 需è¦åœ¨ nextTick å¾Œæ‰æ‰“開,這樣就æ‰ä»¥ç¢ºä¿åœ¨ watch 執行時,知é“è¦å¿½ç•¥é€™æ¬¡çš„ callback 執行;而 isSyncListening 則是用來控制 flush 為 sync çš„ watch,這種 watch 會在資料一改變就馬上執行,所以我們å¯ä»¥ç›´æŽ¥åœ¨ store.$patch 的最後直接æ¢å¾©é–‹é—œã€‚

但å•題åˆä¾†äº†ï¼
但å•題åˆä¾†äº†ï¼
但å•題åˆä¾†äº†ï¼

ç¾åœ¨çš„程å¼ç¢¼åœ¨é€™å€‹æ™‚候會出å•é¡Œï¼ˆå¯æ­é…é‡ç¾ç¯„例:Pinia #1129 é‡ç¾ï¼‰ï¼š

store.$patch({ count: 2 })
await Promise.resolve()
store.$patch({ count: 20 })

如果我們把æ¯ä¸€æ¬¡ callback function 接收到的 type å°å‡ºä¾†çœ‹ï¼Œæœƒç™¼ç¾é€™æ¨£çš„çµæžœ

patch object
patch object
direct <--------- !!?

為什麼會這樣,我們把事發經éŽä¸€æ­¥ä¸€æ­¥æ”¤é–‹ä¾†çœ‹ã€‚

  1. 執行 store.$patch({ count: 2 }) 在這時 isListening 被關閉。
  2. ç­‰å¾…å¾®ä»»å‹™çµæŸï¼Œä¸¦ä¸”執行 watch çš„ callback function,ä¸éŽé€™æ™‚候 isListening 是處於被關閉的狀態所以沒有åšä»»ä½•事情。
  3. 執行 store.$patch({ count: 20 }) 在這時 isListening ç¶­æŒè¢«é—œé–‰ã€‚
  4. 進到第一次執行 store.$patch çš„ nextTick().then() 裡é¢å°‡ isListening 打開。
  5. 執行 watch çš„ callback function,這時候 isListening 已經被打開,所以會執行 callback function。(抓到你了ï¼ï¼ï¼ï¼‰

原來是因為第一次執行 store.$patch çš„ nextTick().then() 裡é¢å…ˆå°‡ isListening æ‰“é–‹äº†ï¼Œé€™æ™‚å€™å°æ‡‰çš„第二次的 watch çš„ callback function 來說 isListening 已經被打開,所以會執行 subscriptions çš„ callback function。

解決方法就是確ä¿ç¬¬äºŒæ¬¡ï¼ˆæœ€å¾Œä¸€æ¬¡ï¼‰çš„ nextTick().then() æ‰æŠŠ isListening 打開,我們需è¦ä¸€å€‹æ–¹æ³•ä¾†ç¢ºèªæ˜¯å¦ç‚ºæœ€å¾Œä¸€æ¬¡åŸ·è¡Œçš„ store.$patch。

let activeListener: Symbol | undefined
function $patch(partialStateOrMutator: (state: UnwrapRef<S>) => void | _DeepPartial<UnwrapRef<S>>): void {
  isListening = isSyncListening = false

  // ... 

  const myListenerId = (activeListener = Symbol())
  nextTick().then(() => {
    if (activeListener === myListenerId) {
      isListening = true
    }
  })
}

é€™æ¨£å°±åªæœƒåœ¨æœ€å¾Œä¸€æ¬¡åŸ·è¡ŒçŽ©å¾Œçš„ nextTick().then() å°‡ isListening 打開。

到這裡 store.$subscribe 的實作就完æˆäº†ã€‚

API: store.$patch

store.$patch 的使用方å¼å¦‚下:

// 直接傳入一個物件
store.$patch({
  name: 'Pinia',
  count: 1,
})

// 傳入一個 callback function
store.$patch((state) => {
  state.items.push({ name: 'shoes', quantity: 1 })
  state.hasChanged = true
})

我們上é¢å·²ç¶“看到 store.$patch 的部分實作,這邊繼續完æˆå®ƒã€‚

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  function $patch(partialStateOrMutator: (state: _DeepPartial<UnwrapRef<S>> | (UnwrapRef<S>) => void)): void {

    if (typeof partialStateOrMutator === 'function') {
      partialStateOrMutator(pinia.state.value[$id])

      // çœç•¥ä¹‹å‰çš„程å¼ç¢¼
    } else {
      mergeReactiveObjects(pinia.state.value[$id], partialStateOrMutator)

      // çœç•¥ä¹‹å‰çš„程å¼ç¢¼
    }
  }
}

撇除掉訂閱的程å¼ç¢¼ï¼Œé€™è£¡æˆ‘們åªè¦è™•ç†ä¸åŒåƒæ•¸ä¿®æ”¹ state 的方å¼å°±å¯ä»¥äº†ã€‚

API: store.$state

store.$state 的使用方å¼å¦‚下,順邊想想下邊é¢çš„æ“ä½œçš„çµæžœ store.$state 會變æˆä»€éº¼ï¼š

/**
 * å‡è¨­ç•¶å‰ `$state` 是這樣的
 * 
 * ```ts
 * {
 *   count: 0,
 *   name: 'Vuex',
 * }
 * ```
 */
const store = useStore()

store.$state.count = 1
store.$state = {
  name: 'Pinia',
}

// store.$state 變æˆï¼Ÿ

store.$state.count = 1 çš„çµæžœå¯æƒ³è€ŒçŸ¥ count çš„å€¼æœƒè®Šæˆ 1,但 store.$state = { name: 'Pinia' } çš„çµæžœæœƒæ˜¯ä»€éº¼å‘¢ï¼Ÿ

實作部份如下:

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  Object.defineProperty(store, '$state', {
    get: () => pinia.state.value[$id],
    set: (state) => {
      $patch(($state) => {
        Object.assign($state, state)
      })
    },
  })
}

當我們執行 store.$state.count = 1 æ™‚ï¼Œå¯¦éš›ä¸Šæ˜¯ç›´æŽ¥å° pinia.state.value[$id] é€²è¡Œä¿®æ”¹ï¼Œå°±æœƒåƒæ˜¯ pinia.state.value[$id].count = 1 ä¸€æ¨£ï¼Œæ‰€ä»¥ä¸æœƒè§¸ç™¼ setã€‚è€Œç•¶æˆ‘å€‘å° store.$state 直接賦值時,則會觸發 set。

所以上é¢åŸ·è¡Œçš„程å¼ç¢¼ï¼Œå¯¦é𛿕ˆæžœå¦‚下:

// store.$state.count = 1
pinia.state.value[$id].count = 1

// store.$state = {
//   name: 'Pinia',
// }
store.$patch((state) => {
  Object.assign(state, {
    name: 'Pinia',
  })
})

API: store.$reset

在 Options Store 建立的 Store instance 上我們å¯ä»¥ä½¿ç”¨ store.$reset 來é‡ç½®æ•´å€‹ store çš„ state。

store.$reset 的使用方å¼å¦‚下:

const store = useStore()

store.$reset()

實作部份如下

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const $reset = isOptionsStore
    //                â¬‡ï¸ Options Store çš„ Options
    ? function $reset(this: _StoreWithState<Id, S, G, A>) {
        const { state } = options 
        const newState = state ? state() : {}
        this.$patch(($state) => {
          Object.assign($state, newState)
        })
      }
    : __DEV__
    ? () => {
        throw new Error(
          `ðŸ: Store "${$id}" is built using the setup syntax and does not implement $reset().`
        )
      }
    : noop
}

ç‚ºä»€éº¼åªæœ‰ Options Store å¯ä»¥ä½¿ç”¨ store.$reset 呢?

因為 Options Store çš„ state 是é€éŽ state function 來åˆå§‹åŒ–的,所以我們å¯ä»¥é€éŽ state function 來å–å¾—åˆå§‹çš„ state,但 Setup Store çš„ state 是é€éŽ setup function 來åˆå§‹åŒ–的,而我們無法有效地使用 setup function 來å–å¾—åˆå§‹çš„ state。

API: store.$dispose

store.$dispose 的使用方å¼å¦‚下:

const store = useStore()

store.$dispose()

在這裡我們會將 EffectScope åœæ­¢ï¼Œä¸¦ä¸”清除所有的 subscriptions è·Ÿ actionSubscriptions,最後將 store 從 Pinia instance 上刪除。

實作部份如下:

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  function $dispose() {
    scope.stop()
    subscriptions = []
    actionSubscriptions = []
    pinia._s.delete($id)
  }
}

在這裡 Pinia 僅僅將 Store instance 從 Pinia instance 上刪除,並沒有將 store çš„ state 刪除,所以如果我們在 store.$dispose å¾Œå†æ¬¡ä½¿ç”¨ useStore 來å–å¾— store,那麼這個新的 store 會延用舊的 state。

原因在 state çš„åˆå§‹åŒ–æµç¨‹ä¸­ï¼Œå¦‚果看到這裡已經å°è±¡æ¨¡ç³Šçš„話,å¯ä»¥å›žåˆ°ä¸Šé¢çš„æ®µè½è¤‡ç¿’一下ï¼

å®‰è£ Plugins

在研究 Plugins 安è£åŠŸèƒ½å‰å…ˆçœ‹çœ‹æ€Žéº¼ä½¿ç”¨å§ï¼š

import { createPinia } from 'pinia'

function plugin({ 
  pinia,    // pinia instance
  app,      // ç•¶å‰çš„ vue application instance
  store,    // ç•¶å‰çš„ store
  options   // å–å¾— `defineStore()` 時定義商店的åˆå§‹é¸é …
})  {
  return { secret: 'the cake is a lie' }
}

const pinia = createPinia()
pinia.use(plugin)

而實際設計éžå¸¸ç°¡å–®ï¼š

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const optionsForPlugin = Object.assign({ actions: {} }, options)

  for (const key in setupStore) {
    const prop = setupStore[key]

    if ((isRef(prop) && !isComputed(prop)) || isReactive(prop)) {
      // ...
      // action
    } else if (typeof prop === 'function') {
      
      â¬‡ï¸ åŠ ä¸ŠåŒ…è£éŽçš„ action
      optionsForPlugin.actions[key] = prop
    }
  }

  pinia._p.forEach((extender) => {
    Object.assign(
      store,
      scope.run(() =>
        extender({
          store,
          app: pinia._a,
          pinia,
          options: optionsForPlugin,
        })
      )!
    )
  })
}

å…¶ä»–æ²’æåˆ°çš„部分

hydrate

這是一個 Options Store 用來補水的 API。以下為使用範例:

const useStore = defineStore('main', {
  state: () => ({
    n: useLocalStorage('key', 0)
  }),
  hydrate(storeState, initialState) {
    storeState.n = useLocalStorage('key', 0)
  }
})

實作如下:

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  if (
    initialState &&
    isOptionsStore &&
    options.hydrate
  ) {
    options.hydrate(store.$state, initialState)
  }
}

共用 Store(單例模å¼ï¼‰

在å‰ä¸€ç¯‡æœ‰æåˆ° Pinia 會把建立éŽçš„ Store instance 存在 pinia._s 這個「全域存å–點ã€ä¸Šï¼Œæ‰€ä»¥åœ¨ setup store 建立好 store 後åªè¦é€™æ¨£åšå°±å¯ä»¥é‡è¤‡åˆ©ç”¨ Store instance 了:

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const store = reactive({
    _p: pinia,
    $id,
    $onAction,
    $patch,
    $reset,
    $subscribe,
    $dispose,
  })

  pinia._s.set($id, store)
}

markRaw

在上一部份我們看到了 Pinia instance æœƒè¢«ä»¥ç§æœ‰å±¬æ€§çš„æ–¹å¼å­˜åˆ° Store instance 上,這裡我們å¯ä»¥ä½¿ç”¨ markRaw 來é¿å… Pinia instance 被 reactive。

function createSetupStore($id, setup, options, pinia, isOptionsStore) {
  const store = reactive({
    _p: markRaw(pinia),
    $id,
    $onAction,
    $patch,
    $reset,
    $subscribe,
    $dispose,
  })

  pinia._s.set($id, store)
}

ä¸éŽå¯¦éš›ä¸Šé€™è£¡çš„ markRaw 是被寫在第一篇的 createPinia 裡é¢

export function createPinia(): Pinia {

  let _p = []
  let toBeInstalled = []

  const pinia: Pinia = markRaw({
    // ç•¥
  })

  return pinia
}

çµèªž

ç¶œåˆä»¥ä¸Šçš„內容,我們å¯ä»¥æ•´ç†å‡º Setup Store 的實作內容:

  • åˆå§‹åŒ– state,這裡在åˆå§‹åŒ–時一樣需è¦è€ƒé‡ SSR çš„ hydration å•題,但ä¸å¤ªä¸€æ¨£çš„部分是這裡會é‡å°æ¯ä¸€å€‹å±¬æ€§æª¢æŸ¥æ˜¯è£œæ°´ã€‚
  • åŒ…è£ actions,在這裡會將 action function å°è£ï¼Œä¸¦ä¸”在執行 action å‰å¾Œèª¿ç”¨ store.$onAction çš„ subscription functionï¼›å¦å¤–é›–ç„¶é€™ä¸€ç¯‡æ²’æœ‰ç´°è¬›è·¨è«‹æ±‚ç‹€æ…‹æ±¡æŸ“çš„è­°é¡Œï¼Œä½†æ˜¯æ¯æ¬¡åœ¨å‘¼å« action 剿ˆ‘å€‘é‚„æ˜¯éœ€è¦ setActivePinia(pinia) 來é¿å…污染的å•題,詳情å¯ä»¥å›žé¡§ç¬¬äºŒç¯‡ã€Œæ•´ç† gettersã€çš„部分。
  • 實作:store.$onActionã€store.$subscribeã€store.$patchã€store.$stateã€store.$resetã€store.$dispose。

深入了解 Pinia 的實作後,我們å¯ä»¥ç™¼ç¾ Pinia 的實作其實很簡單,但也照顧到了éžå¸¸å¤šé¢å‘以åŠä¸€äº›ç‰¹æ®Šæ¡ˆä¾‹ï¼Œä¾‹å¦‚:Server Side Renderã€éžåŒæ­¥ç­‰å•é¡Œï¼æœ€å¾Œå¸Œæœ›é€™ç¯‡æ–‡ç« å¯ä»¥è®“å¤§å®¶å° Pinia 的實作有更深入的了解。

因為篇幅考é‡ï¼Œé€™è£¡å°‡è™•ç† HMR 的細節çœå…¨éƒ¨ç•¥äº†ï¼Œå¦å¤–還有一些 API 沒有æåŠæˆ–更詳細探討,如果å°é€™äº›éƒ¨åˆ†æœ‰èˆˆè¶£æ­¡è¿Žèˆ‡æˆ‘討埨或是到 GitHub 上看更完整的原始碼ï¼

åƒè€ƒè³‡æ–™

è«‹æˆ‘å–æ¯å’–å•¡

如果這裡的內容有幫助到你的話,一æ¯å’–å•¡å°±æ˜¯å°æˆ‘最大的鼓勵。

è«‹æˆ‘å–æ¯å’–å•¡
ÿÿÿÿ