亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

Template?ref在Vue3中的實(shí)現(xiàn)原理詳解

 更新時(shí)間:2022年07月01日 15:53:39   作者:黃軼  
這篇文章主要為大家介紹了Template?ref在Vue3中的實(shí)現(xiàn)原理示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jì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 }) =&gt; {
  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)文章

最新評(píng)論