The internal principles of composite functions
The Internal Principles of Composable Functions
Composable functions are one of the most important features in Vue 3, fundamentally changing how we organize and reuse logic. Understanding their internal implementation mechanisms is crucial for mastering Vue 3 in depth.
Basic Structure of Composable Functions
Composable functions are essentially ordinary JavaScript functions that leverage Vue's provided APIs to encapsulate and reuse logic. A typical composable function has the following structure:
import { ref, onMounted } from 'vue'
export function useCounter() {
const count = ref(0)
function increment() {
count.value++
}
onMounted(() => {
console.log('Counter mounted')
})
return {
count,
increment
}
}
Vue 3 processes such functions through specific internal mechanisms, enabling them to interact correctly with component instances.
Setup Context Binding
When a composable function is called within a component's setup()
, Vue creates a special execution context:
// Pseudocode showing how Vue handles setup
function callWithSetupContext(component, setup) {
const ctx = {
// Inject various APIs
ref,
reactive,
computed,
// Lifecycle hooks
onMounted,
onUpdated,
// Other APIs...
}
// Bind the current component instance
setCurrentInstance(component)
try {
const result = setup(component.props, ctx)
return result
} finally {
// Clean up the current instance reference
setCurrentInstance(null)
}
}
This context-binding mechanism allows composable functions to be aware of the current component instance.
Reactive State Management
Reactive state in composable functions is maintained through a special referencing mechanism:
function useFeature() {
const state = reactive({ count: 0 })
// State references are preserved even with multiple calls to the composable function
return { state }
}
Vue internally uses a WeakMap to track the relationship between composable functions and component instances:
const instanceMap = new WeakMap<Component, Map<string, any>>()
function trackComposable(instance, key, value) {
if (!instanceMap.has(instance)) {
instanceMap.set(instance, new Map())
}
instanceMap.get(instance).set(key, value)
}
Lifecycle Hook Registration
Lifecycle hooks in composable functions are collected and bound to the current component:
// Pseudocode: Lifecycle handling
function onMounted(hook) {
const instance = getCurrentInstance()
if (instance) {
// Add the hook to the component instance
instance.mountedHooks.push(hook)
}
}
This design allows multiple composable functions to register their own hooks without overwriting each other.
Dependency Injection Mechanism
Composable functions can utilize provide
/inject
for cross-level communication:
// Provider component
const provideData = ref('data')
provide('key', provideData)
// Consumer composable function
function useInjectedData() {
const data = inject('key')
return { data }
}
Vue internally maintains an injector chain at the component tree level:
// Pseudocode: Injection implementation
function inject(key) {
let instance = getCurrentInstance()
while (instance) {
if (instance.provides.has(key)) {
return instance.provides.get(key)
}
instance = instance.parent
}
}
Asynchronous State Handling
Composable functions can conveniently handle asynchronous logic:
function useAsyncData(url) {
const data = ref(null)
const loading = ref(false)
async function fetchData() {
loading.value = true
try {
data.value = await fetch(url).then(r => r.json())
} finally {
loading.value = false
}
}
onMounted(fetchData)
return { data, loading, refresh: fetchData }
}
Vue internally manages asynchronous updates via a scheduler:
// Pseudocode: Asynchronous update queue
const queue = []
let isFlushing = false
function queueJob(job) {
if (!queue.includes(job)) {
queue.push(job)
}
if (!isFlushing) {
isFlushing = true
Promise.resolve().then(flushJobs)
}
}
function flushJobs() {
// Execute all tasks in the queue
}
Reusability Strategy for Composable Functions
Vue 3 adopts a reference-based reusability strategy:
const sharedState = ref(0)
function useSharedCounter() {
// Multiple components share the same state
return sharedState
}
For scenarios requiring independent state, a factory pattern can be used:
function createCounter() {
const count = ref(0)
return {
count,
increment: () => count.value++
}
}
Reactive Effect Tracking
Computed properties in composable functions automatically track dependencies:
function useFeature() {
const count = ref(0)
const doubled = computed(() => count.value * 2)
return { count, doubled }
}
Vue internally implements dependency tracking via effects:
// Pseudocode: Computed property implementation
function computed(getter) {
let value
let dirty = true
const runner = effect(getter, {
lazy: true,
scheduler: () => {
dirty = true
trigger(/* ... */)
}
})
return {
get value() {
if (dirty) {
value = runner()
dirty = false
}
track(/* ... */)
return value
}
}
}
Interaction Between Composable Functions and Templates
Objects returned by composable functions are destructured and exposed to templates:
function useMouse() {
const x = ref(0)
const y = ref(0)
function update(e) {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
Usage in templates:
<template>
<div>Mouse position: {{x}}, {{y}}</div>
</template>
Vue internally uses proxies to enable template access:
// Pseudocode: Handling setup return objects
function handleSetupResult(instance, result) {
if (isObject(result)) {
instance.setupState = new Proxy(result, {
get(target, key) {
if (key in target) {
return target[key]
}
}
})
}
}
Type Support for Composable Functions
Vue 3 provides comprehensive type inference for composable functions:
import { Ref } from 'vue'
interface MousePosition {
x: Ref<number>
y: Ref<number>
}
function useMouse(): MousePosition {
// ...
}
TypeScript can automatically infer the return type:
const { x, y } = useMouse()
// x and y are correctly inferred as Ref<number>
Performance Optimization for Composable Functions
Vue 3 improves composable function performance through compile-time optimizations:
// Before compilation
const { x, y } = useMouse()
// After compilation (possible optimization)
const __temp = useMouse()
const x = __temp.x
const y = __temp.y
For frequently called composable functions, Vue caches their results:
// Pseudocode: Composable function caching
function callComposable(instance, key, fn) {
const cache = instance.composableCache
if (!cache.has(key)) {
cache.set(key, fn())
}
return cache.get(key)
}
Composable Functions and Render Functions
Composable functions can also be used in render functions:
import { h } from 'vue'
function useRenderHelper() {
return {
createNode(text) {
return h('div', text)
}
}
}
export default {
setup() {
const { createNode } = useRenderHelper()
return () => createNode('Hello')
}
}
Vue internally uniformly processes functions returned by setup
:
// Pseudocode: Render handling
function finishSetup(instance) {
if (isFunction(instance.setupState)) {
instance.render = instance.setupState
}
}
Error Handling in Composable Functions
Vue provides an error handling mechanism:
function useFeature() {
onErrorCaptured((err) => {
console.error('Error captured:', err)
// Prevent the error from propagating further
return false
})
}
Internal error handling flow:
// Pseudocode: Error capturing
function callWithErrorHandling(fn) {
try {
return fn()
} catch (err) {
const instance = getCurrentInstance()
if (instance) {
let parent = instance.parent
while (parent) {
if (parent.errorHandlers) {
// Call the parent component's error handler
}
parent = parent.parent
}
}
throw err
}
}
Debugging Support for Composable Functions
DevTools can inspect the state of composable functions:
function useCounter() {
const count = ref(0)
// Provide metadata for DevTools
__DEV__ && (count.__source = 'useCounter')
return { count }
}
Vue internally maintains debugging information:
// Pseudocode: Debug info recording
function trackDebugInfo(instance, key, value) {
if (__DEV__) {
instance.debugInfo[key] = {
value,
stack: new Error().stack
}
}
}
Advanced Patterns for Composable Functions
More complex patterns like middleware can be implemented:
function withLogger(composable) {
return function wrappedComposable(...args) {
console.log('Call started:', composable.name)
const result = composable(...args)
console.log('Call ended:', composable.name)
return result
}
}
const useCounterWithLog = withLogger(useCounter)
Composition of composable functions is also possible:
function useFeatureA() { /* ... */ }
function useFeatureB() { /* ... */ }
function useAllFeatures() {
return {
...useFeatureA(),
...useFeatureB()
}
}
Integration with Vuex/Pinia
Composable functions can encapsulate state management logic:
// Using Pinia example
import { useStore } from 'pinia'
function useUser() {
const store = useStore()
const fullName = computed(() => {
return `${store.firstName} ${store.lastName}`
})
return {
fullName,
updateUser: store.updateUser
}
}
Internal integration principle:
// Pseudocode: Pinia integration
function useStore() {
const instance = getCurrentInstance()
if (instance) {
return inject('pinia')
}
}
SSR Support for Composable Functions
Composable functions need to consider server-side rendering scenarios:
function useAsyncDataSSR(key) {
const data = ref(null)
if (import.meta.env.SSR) {
// Fetch data on the server
useSSRContext().asyncData[key] = fetchData()
} else {
// Fetch data on the client
onMounted(() => fetchData())
}
return { data }
}
Vue's internal SSR handling:
// Pseudocode: SSR context
function renderToString(app) {
const ctx = {}
app.provide('ssrContext', ctx)
// ...Rendering logic
return { html, ctx }
}
Testing Strategies for Composable Functions
Composable functions can be tested independently of components:
import { ref } from 'vue'
import { useCounter } from './counter'
test('useCounter', () => {
const { count, increment } = useCounter()
expect(count.value).toBe(0)
increment()
expect(count.value).toBe(1)
})
Testing tools provided by Vue:
import { renderHook } from '@vue/test-utils'
test('useCounter with lifecycle', async () => {
const { result } = renderHook(() => useCounter())
// Simulate lifecycle
await flushPromises()
expect(result.current.count.value).toBe(0)
})
Edge Case Handling for Composable Functions
Various edge scenarios need to be considered:
function useFeature() {
// Handle potentially missing window object
const isClient = typeof window !== 'undefined'
const width = ref(isClient ? window.innerWidth : 0)
if (isClient) {
onMounted(() => {
window.addEventListener('resize', updateWidth)
})
}
return { width }
}
Vue's internal handling of edge cases:
// Pseudocode: Safe access
function safeInvokeHook(hook) {
try {
hook()
} catch (e) {
if (__DEV__) {
warn(`Hook error: ${e}`)
}
}
}
本站部分内容来自互联网,一切版权均归源网站或源作者所有。
如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn
下一篇:响应式状态在组件间的共享