元件的代理和 setupContext
元件的代理
元件有一個重要概念叫做代理(Proxy)。 簡單來說,它是一個允許存取元件實例資料(公開屬性)的代理。 代理結合了 setup 的結果(狀態,函式),data,props 和其他存取。
讓我們考慮以下程式碼(包括在 chibivue 中未實作的部分,所以請將其視為常規 Vue):
<script>
export default defineComponent({
props: { parentCount: { type: Number, default: 0 } },
data() {
return { dataState: { count: 0 } }
},
methods: {
incrementData() {
this.dataState.count++
},
},
setup() {
const state = reactive({ count: 0 })
const increment = () => {
state.count++
}
return { state, increment }
},
})
</script>
<template>
<div>
<p>count (parent): {{ parentCount }}</p>
<br />
<p>count (data): {{ dataState.count }}</p>
<button @click="incrementData">increment (data)</button>
<br />
<p>count: {{ state.count }}</p>
<button @click="increment">increment</button>
</div>
</template>這段程式碼工作正常,但它是如何綁定到模板的?
讓我們考慮另一個例子。
<script setup>
const ChildRef = ref()
// 存取元件的方法和資料
// ChildRef.value?.incrementData
// ChildRef.value?.increment
</script>
<template>
<!-- Child 是前面提到的元件 -->
<Child :ref="ChildRef" />
</template>在這種情況下,你可以透過 ref 存取元件的資訊。
為了實作這一點,ComponentInternalInstance 有一個名為 proxy 的屬性,它儲存用於資料存取的代理。
換句話說,模板(渲染函式)和 ref 引用 instance.proxy。
interface ComponentInternalInstance {
proxy: ComponentPublicInstance | null
}這個代理的實作是使用 Proxy 完成的,大致如下:
instance.proxy = instance.proxy = new Proxy(
instance,
PublicInstanceProxyHandlers,
)
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get(instance: ComponentRenderContext, key: string) {
const { setupState, ctx, props } = instance
// 根據鍵按 setupState -> props -> ctx 的順序檢查,如果存在則回傳值
},
}讓我們實作這個代理!
一旦實作,讓我們修改程式碼以將此代理傳遞給渲染函式和 ref。
到此為止的原始碼: chibivue (GitHub)
※ 順便說一下,我還實作了 defineComponent 和相關型別檢查的實作(這允許我們推斷代理資料的型別)。

setupContext
https://ja.vuejs.org/api/composition-api-setup.html#setup-context
Vue 有一個名為 setupContext 的概念。這是在 setup 函式中暴露的上下文,包括 emit 和 expose。
目前,emit 正在工作,但實作有些粗糙。
const setupResult = component.setup(instance.props, {
emit: instance.emit,
})讓我們正確定義 SetupContext 介面,並將其表示為實例持有的物件。
export interface ComponentInternalInstance {
// .
// .
// .
setupContext: SetupContext | null // 加入
}
export type SetupContext = {
emit: (e: string, ...args: any[]) => void
}然後,在建立實例時,產生 setupContext 並在執行 setup 函式時將此物件作為第二個參數傳遞。
expose
一旦你到達這一點,讓我們嘗試實作除 emit 之外的 SetupContext。 作為這次的例子,讓我們實作 expose。
expose 是一個允許你明確定義公開屬性的函式。 讓我們目標實作如下的開發者介面:
const Child = defineComponent({
setup(_, { expose }) {
const count = ref(0)
const count2 = ref(0)
expose({ count })
return { count, count2 }
},
template: `<p>hello</p>`,
})
const Child2 = defineComponent({
setup() {
const count = ref(0)
const count2 = ref(0)
return { count, count2 }
},
template: `<p>hello</p>`,
})
const app = createApp({
setup() {
const child = ref()
const child2 = ref()
const log = () => {
console.log(
child.value.count,
child.value.count2, // 無法存取
child2.value.count,
child2.value.count2,
)
}
return () =>
h('div', {}, [
h(Child, { ref: child }, []),
h(Child2, { ref: child2 }, []),
h('button', { onClick: log }, ['log']),
])
},
})對於不使用 expose 的元件,預設情況下一切仍然是公開的。
作為方向,讓我們在實例內部有一個名為 exposed 的物件,如果這裡設定了值,我們將把這個物件傳遞給 templateRef 的 ref。
export interface ComponentInternalInstance {
// .
// .
// .
exposed: Record<string, any> | null // 加入
}讓我們實作 expose 函式,以便可以在這裡註冊物件。
ProxyRefs
在本章中,我們實作了 proxy 和 exposedProxy,但實際上與原始 Vue 有一些差異。 那就是"ref 被解包"。(在 proxy 的情況下,setupState 具有此屬性而不是 proxy。)
這些是用 ProxyRefs 實作的,處理器在名為 shallowUnwrapHandlers 的名稱下實作。 這允許我們在編寫模板或處理代理時消除 ref 特定值的冗餘。
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
set: (target, key, value, receiver) => {
const oldValue = target[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
} else {
return Reflect.set(target, key, value, receiver)
}
},
}<template>
<!-- <p>{{ count.value }}</p> 不需要這樣寫 -->
<p>{{ count }}</p>
</template>如果你實作到這一點,以下程式碼應該工作。
import { createApp, defineComponent, h, ref } from 'chibivue'
const Child = defineComponent({
setup(_, { expose }) {
const count = ref(0)
const count2 = ref(0)
expose({ count })
return { count, count2 }
},
template: `<p>child {{ count }} {{ count2 }}</p>`,
})
const Child2 = defineComponent({
setup() {
const count = ref(0)
const count2 = ref(0)
return { count, count2 }
},
template: `<p>child2 {{ count }} {{ count2 }}</p>`,
})
const app = createApp({
setup() {
const child = ref()
const child2 = ref()
const increment = () => {
child.value.count++
child.value.count2++ // 無法存取
child2.value.count++
child2.value.count2++
}
return () =>
h('div', {}, [
h(Child, { ref: child }, []),
h(Child2, { ref: child2 }, []),
h('button', { onClick: increment }, ['increment']),
])
},
})
app.mount('#app')模板綁定和 with 語句
實際上,本章的更改存在一個問題。 讓我們嘗試執行以下程式碼:
const Child2 = {
setup() {
const state = reactive({ count: 0 })
return { state }
},
template: `<p>child2 count: {{ state.count }}</p>`,
}這只是一個簡單的程式碼,但它不工作。 它抱怨 state 未定義。

原因是當將代理作為參數傳遞給 with 語句時,必須定義 has。
所以讓我們在 PublicInstanceProxyHandlers 中實作 has。 如果鍵存在於 setupState,props 或 ctx 中,它應該回傳 true。
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
// .
// .
// .
has(
{ _: { setupState, ctx, propsOptions } }: ComponentRenderContext,
key: string,
) {
let normalizedProps
return (
hasOwn(setupState, key) ||
((normalizedProps = propsOptions[0]) && hasOwn(normalizedProps, key)) ||
hasOwn(ctx, key)
)
},
}如果它工作正常,應該可以正常工作!
到此為止的原始碼: chibivue (GitHub)
