Template?ref在Vue3中的實(shí)現(xiàn)原理詳解
背景
最近我的 Vue3 音樂(lè)課程后臺(tái)問(wèn)答區(qū)頻繁出現(xiàn)一個(gè)關(guān)于 Template ref 在 Composition API 中使用的問(wèn)題,于是我就想寫一篇文章詳細(xì)解答這個(gè)問(wèn)題。
先來(lái)看一個(gè)簡(jiǎn)單的例子:
<template> <div ref="root">This is a root element</div> </template> <script> import { ref, onMounted } from 'vue' export default { setup() { const root = ref(null) onMounted(() => { // DOM 元素將在初始渲染后分配給 ref console.log(root.value) // <div>This is a root element</div> }) return { root } } } </script>
首先我們?cè)谀0逯薪o div 添加了 ref 屬性,并且它的值是 root 字符串,接下來(lái)我們?cè)?setup 函數(shù)中使用 ref API 定義了一個(gè) root 的響應(yīng)式對(duì)象,并把它放到 setup 函數(shù)的返回對(duì)象中。
那么有小伙伴就問(wèn)了,setup 函數(shù)中使用 ref API 定義的 root 和在模板中定義的 ref 是同一個(gè)東西嗎?如果不是,那為什么需要同名呢,不同名可以嗎?
帶著這些疑問(wèn),我們來(lái)分析一下 Template ref 的實(shí)現(xiàn)原理。
模板的編譯
我們還是結(jié)合示例來(lái)分析,首先借助于模板導(dǎo)出工具,可以看到它編譯后的 render 函數(shù):
import { openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue" const _hoisted_1 = { ref: "root" } export function render(_ctx, _cache, $props, $setup, $data, $options) { return (_openBlock(), _createElementBlock("div", _hoisted_1, "This is a root element", 512 /* NEED_PATCH */)) }
可以看到,render 函數(shù)內(nèi)部使用了 createElementBlock 函數(shù)來(lái)創(chuàng)建對(duì)應(yīng)的元素 vnode,來(lái)看它實(shí)現(xiàn):
function createElementBlock(type, props, children, patchFlag, dynamicProps, shapeFlag) { return setupBlock(createBaseVNode(type, props, children, patchFlag, dynamicProps, shapeFlag, true /* isBlock */)); } function createBaseVNode(type, props = null, children = null, patchFlag = 0, dynamicProps = null, shapeFlag = type === Fragment ? 0 : 1 /* ELEMENT */, isBlockNode = false, needFullChildrenNormalization = false) { const vnode = { __v_isVNode: true, __v_skip: true, type, props, key: props && normalizeKey(props), ref: props && normalizeRef(props), scopeId: currentScopeId, slotScopeIds: null, children, component: null, suspense: null, ssContent: null, ssFallback: null, dirs: null, transition: null, el: null, anchor: null, target: null, targetAnchor: null, staticCount: 0, shapeFlag, patchFlag, dynamicProps, dynamicChildren: null, appContext: null } if (needFullChildrenNormalization) { normalizeChildren(vnode, children) if (shapeFlag & 128 /* SUSPENSE */) { type.normalize(vnode) } } else if (children) { vnode.shapeFlag |= isString(children) ? 8 /* TEXT_CHILDREN */ : 16 /* ARRAY_CHILDREN */ } // ... // 處理 Block Tree return vnode }
這里我們先不用管 Block 的相關(guān)邏輯,重點(diǎn)看 vnode 的創(chuàng)建過(guò)程。
createElementBlock 函數(shù)內(nèi)部通過(guò) createBaseVNode 函數(shù)來(lái)創(chuàng)建生成 vnode 對(duì)象,其中第二個(gè)參數(shù) props 就是用來(lái)描述 vnode 的一些屬性,在這個(gè)例子中 props 的值是 {ref: "root"}。
生成的 vnode 對(duì)象中,有一個(gè) ref 屬性,在 props 存在的情況下,會(huì)經(jīng)過(guò)一層 normalizeRef 的處理:
const normalizeRef = ({ ref }) => { return (ref != null ? isString(ref) || isRef(ref) || isFunction(ref) ? { i: currentRenderingInstance, r: ref } : ref : null) }
至此,我們已知 div 標(biāo)簽在 render 函數(shù)執(zhí)行之后轉(zhuǎn)換成一個(gè)元素 vnode,它對(duì)應(yīng)的 type 是 div,props 值是 {ref: "root"}、ref 的值是 {i: currentRenderingInstance, r: "root"}。
setup 函數(shù)返回值的處理
接下來(lái),我們順著組件的掛載過(guò)程,來(lái)分析 setup 函數(shù)的返回值是如何處理的。一個(gè)組件的掛載,首先會(huì)執(zhí)行 mountComponent 函數(shù):
const mountComponent = (initialVNode, container, anchor, parentComponent) => { // 創(chuàng)建組件實(shí)例 const instance = initialVNode.component = createComponentInstance(initialVNode, parentComponent) // 設(shè)置組件實(shí)例 setupComponent(instance) // 設(shè)置并運(yùn)行帶副作用的渲染函數(shù) setupRenderEffect(instance, initialVNode, container, anchor) }
可以看到,mountComponent 主要做了三件事情:創(chuàng)建組件實(shí)例、設(shè)置組件實(shí)例和設(shè)置并運(yùn)行帶副作用的渲染函數(shù)。
其中 setup 函數(shù)的處理邏輯在 setupComponent 函數(shù)內(nèi)部:
function setupComponent (instance, isSSR = false) { const { props, children, shapeFlag } = instance.vnode // 判斷是否是一個(gè)有狀態(tài)的組件 const isStateful = shapeFlag & 4 // 初始化 props initProps(instance, props, isStateful, isSSR) // 初始化 插槽 initSlots(instance, children) // 設(shè)置有狀態(tài)的組件實(shí)例 const setupResult = isStateful ? setupStatefulComponent(instance, isSSR) : undefined return setupResult }
setupComponent 內(nèi)部會(huì)根據(jù) shapeFlag 的值判斷這是不是一個(gè)有狀態(tài)組件,如果是則要進(jìn)一步去設(shè)置有狀態(tài)組件的實(shí)例。
通常我們寫的組件就是一個(gè)有狀態(tài)的組件,所謂有狀態(tài),指的就是組件在渲染過(guò)程中,會(huì)把它的一些狀態(tài)掛載到組件實(shí)例對(duì)應(yīng)的屬性上。
接下來(lái)看 setupStatefulComponent 函數(shù):
function setupStatefulComponent (instance, isSSR) { const Component = instance.type // 創(chuàng)建渲染代理的屬性訪問(wèn)緩存 instance.accessCache = {} // 創(chuàng)建渲染上下文代理 instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers)) // 判斷處理 setup 函數(shù) const { setup } = Component if (setup) { // 如果 setup 函數(shù)帶參數(shù),則創(chuàng)建一個(gè) setupContext const setupContext = (instance.setupContext = setup.length > 1 ? createSetupContext(instance) : null) // 執(zhí)行 setup 函數(shù),獲取返回值 const setupResult = callWithErrorHandling(setup, instance, 0 /* SETUP_FUNCTION */, [instance.props, setupContext]) // 處理 setup 返回值 handleSetupResult(instance, setupResult) } else { // 完成組件實(shí)例設(shè)置 finishComponentSetup(instance) } }
setupStatefulComponent 函數(shù)主要做了三件事:創(chuàng)建渲染上下文代理、判斷處理 setup 函數(shù)和完成組件實(shí)例設(shè)置。
這里我們重點(diǎn)關(guān)注判斷處理 setup 函數(shù)部分,首先它會(huì)通過(guò) callWithErrorHandling 的方式來(lái)執(zhí)行組件定義的 setup 函數(shù),并把它的返回值放到 setupResult 中,然后執(zhí)行 handleSetupResult 函數(shù)來(lái)處理 setup 執(zhí)行結(jié)果,來(lái)看它的實(shí)現(xiàn):
function handleSetupResult(instance, setupResult) { if (isFunction(setupResult)) { // setup 返回渲染函數(shù) instance.render = setupResult } else if (isObject(setupResult)) { // 把 setup 返回結(jié)果做一層代理 instance.setupState = proxyRefs(setupResult) } finishComponentSetup(instance) }
可以看到,如果 setup 函數(shù)返回值是一個(gè)函數(shù),那么該函數(shù)就作為組件的 render 函數(shù);如果 setup 返回值是一個(gè)對(duì)象,那么則把它的值做一層代理,賦值給 instance.setupState。
組件的渲染
setupComponent 函數(shù)執(zhí)行完,就要執(zhí)行 setupRenderEffect 設(shè)置并運(yùn)行帶副作用的渲染函數(shù),簡(jiǎn)化后的實(shí)現(xiàn)如下:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => { // 組件的渲染和更新函數(shù) const componentUpdateFn = () => { if (!instance.isMounted) { // 渲染組件生成子樹 vnode const subTree = (instance.subTree = renderComponentRoot(instance)) // 把子樹 vnode 掛載到 container 中 patch(null, subTree, container, anchor, instance, parentSuspense, isSVG) // 保留渲染生成的子樹根 DOM 節(jié)點(diǎn) initialVNode.el = subTree.el instance.isMounted = true } else { // 更新組件 } } // 創(chuàng)建組件渲染的副作用響應(yīng)式對(duì)象 const effect = new ReactiveEffect(componentUpdateFn, () => queueJob(instance.update), instance.scope) const update = (instance.update = effect.run.bind(effect)) update.id = instance.uid // 允許遞歸更新自己 effect.allowRecurse = update.allowRecurse = true update() }
setupRenderEffect 函數(shù)內(nèi)部利用響應(yīng)式庫(kù)的 ReactiveEffect 函數(shù)創(chuàng)建了一個(gè)副作用實(shí)例 effect,并且把 instance.update 函數(shù)指向了 effect.run。
當(dāng)首次執(zhí)行 instance.update 時(shí),內(nèi)部就會(huì)執(zhí)行 componentUpdateFn 函數(shù)觸發(fā)組件的首次渲染。
當(dāng)組件的數(shù)據(jù)發(fā)生變化時(shí),組件渲染函數(shù) componentUpdateFn 會(huì)重新執(zhí)行一遍,從而達(dá)到重新渲染組件的目的。
componentUpdateFn 函數(shù)內(nèi)部會(huì)判斷這是一次初始渲染還是組件的更新渲染,目前我們只需關(guān)注初始渲染流程。
初始渲染主要做兩件事情:執(zhí)行 renderComponentRoot 函數(shù)渲染組件生成 subTree 子樹 vnode,執(zhí)行 patch 函數(shù)把 subTree 掛載到 container 中。
renderComponentRoot 內(nèi)部會(huì)執(zhí)行組件的 render 函數(shù)渲染生成一棵 vnode 樹,然后在 patch 過(guò)程中把 vnode 樹渲染生成真正的 DOM 樹。
接下來(lái)看 patch 函數(shù)的實(shí)現(xiàn):
const patch = (n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, slotScopeIds = null, optimized = false) => { const { type, ref, shapeFlag } = n2 switch (type) { case Text: // 處理文本節(jié)點(diǎn) break case Comment: // 處理注釋節(jié)點(diǎn) break case Static: // 處理靜態(tài)節(jié)點(diǎn) break case Fragment: // 處理 Fragment 元素 break default: if (shapeFlag & 1 /* ELEMENT */) { // 處理普通 DOM 元素 processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) } else if (shapeFlag & 6 /* COMPONENT */) { // 處理組件 processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) } else if (shapeFlag & 64 /* TELEPORT */) { // 處理 TELEPORT } else if (shapeFlag & 128 /* SUSPENSE */) { // 處理 SUSPENSE } } // 設(shè)置 ref if (ref != null && parentComponent) { setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2) } }
在組件的首次渲染階段,patch 函數(shù)內(nèi)部會(huì)根據(jù) vnode 節(jié)點(diǎn)類型的不同,執(zhí)行不同的處理邏輯,最終目的就是構(gòu)造出一棵 DOM 樹。
Template Ref 的注冊(cè)
遍歷渲染 DOM 樹的過(guò)程,實(shí)際上就是一個(gè)遞歸執(zhí)行 patch 函數(shù)的過(guò)程,在 patch 函數(shù)的最后,也就是當(dāng)前節(jié)點(diǎn)掛載后,會(huì)判斷如果新的 vnode 存在 ref 屬性,則執(zhí)行 setRef 完成 Template Ref 的注冊(cè)邏輯。
顯然,對(duì)于我們示例來(lái)說(shuō),div 標(biāo)簽生成的元素 vnode,它對(duì)應(yīng)的 ref 的值是 {i: currentRenderingInstance, r: "root"},滿足條件,則會(huì)執(zhí)行 setRef 函數(shù),來(lái)看它的實(shí)現(xiàn):
function setRef(rawRef, oldRawRef, parentSuspense, vnode, isUnmount = false) { // 如果 rawRef 是數(shù)組,則遍歷遞歸執(zhí)行 setRef if (isArray(rawRef)) { rawRef.forEach((r, i) => setRef(r, oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef), parentSuspense, vnode, isUnmount)) return } if (isAsyncWrapper(vnode) && !isUnmount) { return } // 如果 vnode 是組件 vnode,refValue 指向組件的實(shí)例,否則指向元素的 DOM const refValue = vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */ ? getExposeProxy(vnode.component) || vnode.component.proxy : vnode.el const value = isUnmount ? null : refValue const { i: owner, r: ref } = rawRef if ((process.env.NODE_ENV !== 'production') && !owner) { warn(`Missing ref owner context. ref cannot be used on hoisted vnodes. ` + `A vnode with ref must be created inside the render function.`) return } const oldRef = oldRawRef && oldRawRef.r const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs const setupState = owner.setupState // ref 動(dòng)態(tài)更新,刪除舊的 if (oldRef != null && oldRef !== ref) { if (isString(oldRef)) { refs[oldRef] = null if (hasOwn(setupState, oldRef)) { setupState[oldRef] = null } } else if (isRef(oldRef)) { oldRef.value = null } } if (isString(ref)) { const doSet = () => { { refs[ref] = value } if (hasOwn(setupState, ref)) { setupState[ref] = value } } if (value) { doSet.id = -1 queuePostRenderEffect(doSet, parentSuspense) } else { doSet() } } else if (isRef(ref)) { const doSet = () => { ref.value = value } if (value) { doSet.id = -1 queuePostRenderEffect(doSet, parentSuspense) } else { doSet() } } else if (isFunction(ref)) { callWithErrorHandling(ref, owner, 12 /* FUNCTION_REF */, [value, refs]) } else if ((process.env.NODE_ENV !== 'production')) { warn('Invalid template ref type:', value, `(${typeof value})`) } }
setRef 函數(shù)的目的就是對(duì)當(dāng)前節(jié)點(diǎn)的引用求值,并保存下來(lái)。節(jié)點(diǎn)的引用值 refValue 會(huì)根據(jù) vnode 的類型有所區(qū)別,如果 vnode 是組件 vnode,refValue 指向組件的實(shí)例,否則指向元素的 DOM。
這就是平時(shí)我們給普通元素節(jié)點(diǎn)設(shè)置 ref 能拿到對(duì)應(yīng)的 DOM,而對(duì)組件節(jié)點(diǎn)設(shè)置 ref 能拿到組件實(shí)例的原因。
從傳遞的參數(shù) rawRef 中,可以獲取到當(dāng)前組件實(shí)例 owner,以及對(duì)應(yīng)的 ref 值,對(duì)于我們的示例,rawRef 的值是 {i: currentRenderingInstance, r: "root"},那么對(duì)應(yīng)的 ref 就是 root 字符串。
如果 ref 是字符串類型,且 owner.setupState 中包含了這個(gè)字符串屬性,那么則把這個(gè) refValue 保留到 owner.setupStatep[ref] 中。
前面說(shuō)到,在 handleSetupResult 的時(shí)候,我們已經(jīng)把 setup 函數(shù)的返回值保留到 instance.setupState 中了:
instance.setupState = proxyRefs(setupResult)
這里要注意,使用了 proxyRefs 函數(shù)對(duì) setupResult 做了響應(yīng)式處理,來(lái)看它的實(shí)現(xiàn):
function proxyRefs(objectWithRefs) { return isReactive(objectWithRefs) ? objectWithRefs : new Proxy(objectWithRefs, shallowUnwrapHandlers) } const shallowUnwrapHandlers = { 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) } } }
如果 setupResult 不是響應(yīng)式對(duì)象,那么會(huì)使用 Proxy 對(duì)它做一層代理,它有什么作用呢?接下來(lái)結(jié)合前面的示例進(jìn)行分析。
示例中,在 setup 函數(shù)內(nèi)部,我們利用了 ref API 定義了響應(yīng)式對(duì)象 root:
const root = ref(null)
然后把這個(gè)響應(yīng)式對(duì)象作為 setupResult 返回:
const root = ref(null) return { root }
在 handleSetupResult 的時(shí)候,相當(dāng)于執(zhí)行:
instance.setupState = proxyRefs({ root: root})
經(jīng)過(guò) setRef 的處理,會(huì)執(zhí)行:
instance.setupState['root'] = refValue // DOM
執(zhí)行這個(gè)操作的時(shí)候,會(huì)觸發(fā) shallowUnwrapHandlers 的 setter:
const shallowUnwrapHandlers = { 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) } } }
這里的 target 指向的是 { root: root },key 是 "root",value 是 DOM 對(duì)象。那么 oldValue 指向的就是響應(yīng)式對(duì)象 root,并且滿足 isRef(oldValue) && !isRef(value),因此會(huì)執(zhí)行:
oldValue.value = value
這樣響應(yīng)式對(duì)象 root 的值就指向了 DOM 對(duì)象,所以在 onMounted 后就可以通過(guò) root.value 訪問(wèn)到對(duì)應(yīng)的 DOM 對(duì)象了。
總結(jié)
ref API 定義的 root 和在模板中定義的 ref=root 是并不是一個(gè)東西,它們之所以能關(guān)聯(lián)起來(lái),是返回的 setupResult 中的屬性名和 Template ref 中指向的字符串同名。我們對(duì)示例稍加修改:
<template> <div ref="root">This is a root element</div> </template> <script> import { ref, onMounted } from 'vue' export default { setup() { const rootRef = ref(null) onMounted(() => { // DOM 元素將在初始渲染后分配給 ref console.log(rootRef.value) // <div>This is a root element</div> }) return { root: rootRef } } } </script>
這樣也可以在 onMounted 后通過(guò) rootRef.value 訪問(wèn)到對(duì)應(yīng)的 DOM 對(duì)象的。
因此局部定義的 ref 響應(yīng)式變量并不需要和 Template ref 指向的字符串同名,只需要 setupResult 中保存 ref 響應(yīng)式變量的屬性名和 Template ref 指向的字符串同名即可,因?yàn)閮?nèi)部是通過(guò)該字符串檢索的。
以上就是Template ref在Vue3中的實(shí)現(xiàn)示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue3實(shí)現(xiàn)Template ref的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
如何巧用Vue.extend繼承組件實(shí)現(xiàn)el-table雙擊可編輯(不使用v-if、v-else)
這篇文章主要給大家介紹了關(guān)于如何巧用Vue.extend繼承組件實(shí)現(xiàn)el-table雙擊可編輯的相關(guān)資料,不使用v-if、v-else,文中通過(guò)實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-06-06Vue實(shí)現(xiàn)點(diǎn)擊時(shí)間獲取時(shí)間段查詢功能
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)點(diǎn)擊時(shí)間獲取時(shí)間段查詢功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-04-04vue框架制作購(gòu)物車小球動(dòng)畫效果實(shí)例代碼
最近在學(xué)習(xí)前端制作了一個(gè)購(gòu)物車小球的動(dòng)畫效果,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友參考下吧2019-09-09vue恢復(fù)初始數(shù)據(jù)this.$data,this.$options.data()解析
這篇文章主要介紹了vue恢復(fù)初始數(shù)據(jù)this.$data,this.$options.data()解析,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03vue實(shí)現(xiàn)tab切換的3種方式及切換保持?jǐn)?shù)據(jù)狀態(tài)
這篇文章主要給大家介紹了關(guān)于vue實(shí)現(xiàn)tab切換的3種方式及切換保持?jǐn)?shù)據(jù)狀態(tài)的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-05-05vuex actions異步修改狀態(tài)的實(shí)例詳解
今天小編就為大家分享一篇vuex actions異步修改狀態(tài)的實(shí)例詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2019-11-11