一次在 Vue3 中使用render函式的事故,從原始碼中找到了答案
技術標籤:vuevue3vue3 rendervue3 setupsetup
一次在 Vue3 中使用render函式的事故,從原始碼中找到了答案
第一遍看 vue3
的原始碼,有不對的地方,希望各位大佬能指出,感謝!!!
Vue3 版本 3.0.4
在 Vue3.0
建立元件時,在 render
函式中使用 setup
函式中返回的 ref
資料,無法動態繫結
示例:
Vue.createApp({
setup() {
const elRef = Vue.ref(null)
Vue.onMounted(() => {
elRef. value.innerHTML = "123"
})
return {
elRef
}
},
render() {
return Vue.h("div", {
ref: this.elRef
})
}
}).mount("#app")
瀏覽器報錯
查詢問題
1、找到 render
函式的執行
原始碼位置:https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/componentRenderUtils.ts
刪減後的程式碼:
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
proxy,
withProxy,
props,
render,
renderCache,
data,
setupState,
ctx
} = instance
let result
try {
let fallthroughAttrs
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
const proxyToUse = withProxy || proxy
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
fallthroughAttrs = attrs
} else {
// 函式元件
}
return result
}
render
函式在執行的時候,將 this
改變成了 proxy
,(在生產環境是通過在 with
塊中執行改變)
2、找到 proxy
的定義
原始碼位置 https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/component.ts 565行
刪減後的程式碼:
function setupStatefulComponent(
instance: ComponentInternalInstance,
isSSR: boolean
) {
const Component = instance.type as ComponentOptions // type 就是元件的options
instance.accessCache = Object.create(null)
// proxy 在這裡定義
instance.proxy = new Proxy(instance.ctx, PublicInstanceProxyHandlers)
// setup 方法在這裡執行
const { setup } = Component
if (setup) {
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null)
currentInstance = instance
pauseTracking()
const setupResult = callWithErrorHandling( // 這個方法內部只是使用 try catch 捕捉 setup 函式時的異常
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext]
)
handleSetupResult(instance, setupResult, isSSR) // 這裡處理 setup 的返回值
}
}
PublicInstanceProxyHandlers
對 get
方法的處理
原始碼位置:https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/componentPublicInstance.ts 143行
刪減後的程式碼:
export const PublicInstanceProxyHandlers: ProxyHandler<any> = {
get({ _: instance }: ComponentRenderContext, key: string) {
const {
ctx,
setupState,
data,
props,
accessCache,
type,
appContext
} = instance
let normalizedProps
if (key[0] !== '$') {
// 如果快取裡有 key 對應的資料型別,就不需要在判斷 key 的來源
const n = accessCache![key]
if (n !== undefined) {
switch (n) {
case AccessTypes.SETUP:
return setupState[key]
case AccessTypes.DATA:
return data[key]
case AccessTypes.CONTEXT:
return ctx[key]
case AccessTypes.PROPS:
return props![key]
}
} else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
// 快取中沒有 key 的資料來源,把 key 的資料來源存入到快取中,返回資料
accessCache![key] = AccessTypes.SETUP
return setupState[key]
}
}
}
}
這裡返回的 setupState
的值,setupState
也是一個 proxy
的型別,在 setup
執行完成後建立
原始碼位置:https://github.com/vuejs/vue-next/blob/master/packages/runtime-core/src/component.ts 610行
刪減後的程式碼:
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup 中返回一個函式,這個函式作為render函式使用
instance.render = setupResult as InternalRenderFunction
} else if (isObject(setupResult)) {
// 這裡定義 setupState
instance.setupState = proxyRefs(setupResult) // proxyRefs 函式返回一個 proxy 物件
}
finishComponentSetup(instance, isSSR)
}
proxyRefs
函式 返回一個 proxy
物件,這個 proxy
物件的 get
方法,會返回 ref
資料的 value
值
原始碼位置: https://github.com/vuejs/vue-next/blob/master/packages/reactivity/src/ref.ts 105行
刪減後的程式碼:
export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
// 會返回 ref 資料的 value 屬性,而不是整個物件
return isRef(ref) ? (ref.value as any) : ref
}
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver)),
}
export function proxyRefs<T extends object>(
objectWithRefs: T
): ShallowUnwrapRef<T> {
return isReactive(objectWithRefs)
? objectWithRefs
: new Proxy(objectWithRefs, shallowUnwrapHandlers)
}
到這裡,就可以知道 為什麼在 render
函式中使用 this.
的方法獲取到的不是 ref
物件,而是原始值了
原因總結:
在 render
方法中使用 this.
的方式獲取的是 setup
中返回的 ref
資料的 value
屬性,所以不能給元素做動態繫結
怎麼解決:
在上面提到的
setup
方法執行完成後,會執行handleSetupResult
方法,處理setup
函式返回的資料,再看一遍
刪減後的程式碼:
export function handleSetupResult(
instance: ComponentInternalInstance,
setupResult: unknown,
isSSR: boolean
) {
if (isFunction(setupResult)) {
// setup 中返回一個函式,這個函式作為render函式使用
instance.render = setupResult as InternalRenderFunction
} else if (isObject(setupResult)) {
// 這裡定義 setupState
instance.setupState = proxyRefs(setupResult) // proxyRefs 函式返回一個 proxy 物件
}
finishComponentSetup(instance, isSSR)
}
在 setup
的函式返回值是一個函式的時候,這個函式會被作為 render
函式處理(這個返回值會覆蓋在 opitons
中的 render
函式)
所以我們之前的示例就可以改成下面這個樣子:
Vue.createApp({
setup() {
const elRef = Vue.ref(null)
Vue.onMounted(() => {
elRef.value.innerHTML = "123"
})
return () => Vue.h("div", {
ref: elRef
})
}
}).mount("#app")
頁面中就可以正確的顯示內容了
總結:
setup
中可以返回一個 物件 或者 函式,當返回 函式是,這個函式就是這個元件的 render
函式
當返回 物件的時候,這個物件的資料通過 this
獲取值的時候,只能獲取到 .value
的值,獲取不到 ref
物件