本來預計今天要寫 useDeviceOrientation,但發現 useDeviceOrientation 有用到 useSupported,而 useSupported 有用到 useMounted,所以今天先把 useMounted & useSupported 一起看完,明天的話應該會 useDeviceOrientation & useScreenOrientation 一起看~
<!-- src/components/useMountedDemo.vue -->
<script setup>
import { useMounted } from '@/compositions/useMounted'
const isMounted = useMounted()
</script>
<template>
<h2>useMountedDemo</h2>
<div>{{ isMounted ? 'mounted' : 'unmounted' }}</div>
</template>
先看用法,就是以下這段的縮寫:
const isMounted = ref(false)
onMounted(() => {
isMounted.value = true
})
不過我猜應該是會在其他的 composition 中比較常用到,像是稍後要講的 useSupported。
// src/compositions/useMounted.js
import { getCurrentInstance, onMounted, ref } from 'vue'
export function useMounted() {
const isMounted = ref(false)
const instance = getCurrentInstance()
if (instance) {
onMounted(() => {
isMounted.value = true
}, instance)
}
return isMounted
}
vueuse 原始碼有針對 Vue2 做判斷,上面我實作的版本把這個判斷移除了,先不理 Vue2(?)
這邊有用到 vue3 提供的 API getCurrentInstance
,注意這個 API 不推薦在業務功能上使用,因為他是非公開的 API,通常是給一些 package 使用,在官方文件中也找不到這個 API。
可以先想像成 getCurrentInstance
可以取得當下使用 useMounted 的組件實例。
再來一個新奇的就是,原來 onMounted 有第二個參數,這個應該也是非公開用法,文件上找不到。
但這段可以去翻翻看 vue 的原始碼。以下可以先去 clone vue 專案,用全域搜尋對照著看~
先找到 onMounted 看看是否可以傳入第二個參數:
// packages/runtime-core/src/apiLifecycle.ts
export const createHook =
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
injectHook(lifecycle, hook, target)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
可以看到 onMounted 是 createHook 執行後的回傳值,也就是 (hook: T, target: ComponentInternalInstance | null = currentInstance) => ( // ... 略)
,所以除了我們平常傳入的 callback function 之外,還能傳入 target,上面提到的 vueuse useMounted 就是把 component instance 當作第二個參數傳給 onMounted 的。
看到這段就有點好奇,我們平常沒傳入 target 的時候,onMounted 是怎麼自動找到 componenet instance 的?
vue 原始碼,由內往外找:
setCurrentInstance(instance) → setupStatefulComponent → setupComponent → baseCreateRenderer.mountComponent → createRenderer → baseCreateRenderer → 最後 return 出熟悉的 createApp
// packages/runtime-core/src/renderer.ts
function baseCreateRenderer() {
// ...略
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
以流程大概是,呼叫 createApp → createRenderer → 接著重點在 baseCreateRenderer.mountComponent 這邊設定好 instance,呼叫 setupComponent(instance) ,setupComponent 裡面又呼叫 setupStatefulComponent(instance) ,在 setupStatefulComponent 裡面呼叫 setCurrentInstance(instance) ,這時候 currentInstance 就被設定為一開始在 mountComponent 拿到的 instance。
回到剛剛提到的 onMounted 那段:
// packages/runtime-core/src/apiLifecycle.ts
export const createHook =
<T extends Function = () => any>(lifecycle: LifecycleHooks) =>
(hook: T, target: ComponentInternalInstance | null = currentInstance) =>
// post-create lifecycle registrations are noops during SSR (except for serverPrefetch)
(!isInSSRComponentSetup || lifecycle === LifecycleHooks.SERVER_PREFETCH) &&
injectHook(lifecycle, hook, target)
export const onMounted = createHook(LifecycleHooks.MOUNTED)
所以推測 target 參數的預設值 currentInstance
會拿到設定好的 currentInstance。
以上實作的 GitHub PR:https://github.com/RhinoLee/30days_vue/pull/11/files
// src/compositions/useSupported.js
import { computed } from 'vue'
import { useMounted } from '@/compositions/useMounted'
export function useSupported(callback) {
const isMounted = useMounted()
return computed(() => {
// to trigger the ref
// eslint-disable-next-line no-unused-expressions
isMounted.value
return Boolean(callback())
})
}
滿單純的 API,比較妙的是直接執行 isMounted.value
來觸發 computed 的響應式依賴 XD。用法的話,以明天會提到的 useDeviceOrientation 來說:
const isSupported = useSupported(() => window && 'DeviceOrientationEvent' in window)
useSupported 會回傳 () => window && 'DeviceOrientationEvent' in window
的 Boolean 結果,就會得到瀏覽器是否支援 DeviceOrientationEvent 的結果。
以上實作的 GitHub PR:https://github.com/RhinoLee/30days_vue/pull/12/files
今天花了一點篇幅在看 Vue 的原始碼,滿慶幸一開始主題不是 Vue 原始碼 30 天,感覺完全是另外一個世界 XD,明天就從 useDeviceOrientation & useScreenOrientation 繼續看下去~