欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

 更新時間:2022年07月01日 15:53:39   作者:黃軼  
這篇文章主要為大家介紹了Template?ref在Vue3中的實現(xiàn)原理示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪

背景

最近我的 Vue3 音樂課程后臺問答區(qū)頻繁出現(xiàn)一個關(guān)于 Template ref 在 Composition API 中使用的問題,于是我就想寫一篇文章詳細(xì)解答這個問題。

先來看一個簡單的例子:

<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>

首先我們在模板中給 div 添加了 ref 屬性,并且它的值是 root 字符串,接下來我們在 setup 函數(shù)中使用 ref API 定義了一個 root 的響應(yīng)式對象,并把它放到 setup 函數(shù)的返回對象中。

那么有小伙伴就問了,setup 函數(shù)中使用 ref API 定義的 root 和在模板中定義的 ref 是同一個東西嗎?如果不是,那為什么需要同名呢,不同名可以嗎?

帶著這些疑問,我們來分析一下 Template ref 的實現(xiàn)原理。

模板的編譯

我們還是結(jié)合示例來分析,首先借助于模板導(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ù)來創(chuàng)建對應(yīng)的元素 vnode,來看它實現(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)邏輯,重點看 vnode 的創(chuàng)建過程。

createElementBlock 函數(shù)內(nèi)部通過 createBaseVNode 函數(shù)來創(chuàng)建生成 vnode 對象,其中第二個參數(shù) props 就是用來描述 vnode 的一些屬性,在這個例子中 props 的值是 {ref: "root"}。

生成的 vnode 對象中,有一個 ref 屬性,在 props 存在的情況下,會經(jīng)過一層 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)換成一個元素 vnode,它對應(yīng)的 type 是 div,props 值是 {ref: "root"}、ref 的值是 {i: currentRenderingInstance, r: "root"}。

setup 函數(shù)返回值的處理

接下來,我們順著組件的掛載過程,來分析 setup 函數(shù)的返回值是如何處理的。一個組件的掛載,首先會執(zhí)行 mountComponent 函數(shù):

const mountComponent = (initialVNode, container, anchor, parentComponent) => {
  // 創(chuàng)建組件實例
  const instance = initialVNode.component = createComponentInstance(initialVNode, parentComponent)
  // 設(shè)置組件實例
  setupComponent(instance)
  // 設(shè)置并運行帶副作用的渲染函數(shù)
  setupRenderEffect(instance, initialVNode, container, anchor)
}

可以看到,mountComponent 主要做了三件事情:創(chuàng)建組件實例、設(shè)置組件實例和設(shè)置并運行帶副作用的渲染函數(shù)。

其中 setup 函數(shù)的處理邏輯在 setupComponent 函數(shù)內(nèi)部:

function setupComponent (instance, isSSR = false) {
  const { props, children, shapeFlag } = instance.vnode
  // 判斷是否是一個有狀態(tài)的組件
  const isStateful = shapeFlag & 4
  // 初始化 props
  initProps(instance, props, isStateful, isSSR)
  // 初始化 插槽
  initSlots(instance, children)
  // 設(shè)置有狀態(tài)的組件實例
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  return setupResult
}

setupComponent 內(nèi)部會根據(jù) shapeFlag 的值判斷這是不是一個有狀態(tài)組件,如果是則要進(jìn)一步去設(shè)置有狀態(tài)組件的實例。

通常我們寫的組件就是一個有狀態(tài)的組件,所謂有狀態(tài),指的就是組件在渲染過程中,會把它的一些狀態(tài)掛載到組件實例對應(yīng)的屬性上。

接下來看 setupStatefulComponent 函數(shù):

function setupStatefulComponent (instance, isSSR) {
  const Component = instance.type
  // 創(chuàng)建渲染代理的屬性訪問緩存
  instance.accessCache = {}
  // 創(chuàng)建渲染上下文代理
  instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers))
  // 判斷處理 setup 函數(shù)
  const { setup } = Component
  if (setup) {
    // 如果 setup 函數(shù)帶參數(shù),則創(chuàng)建一個 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è)置
    finishComponentSetup(instance)
  }
}

setupStatefulComponent 函數(shù)主要做了三件事:創(chuàng)建渲染上下文代理、判斷處理 setup 函數(shù)和完成組件實例設(shè)置。

這里我們重點關(guān)注判斷處理 setup 函數(shù)部分,首先它會通過 callWithErrorHandling 的方式來執(zhí)行組件定義的 setup 函數(shù),并把它的返回值放到 setupResult 中,然后執(zhí)行 handleSetupResult 函數(shù)來處理 setup 執(zhí)行結(jié)果,來看它的實現(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ù)返回值是一個函數(shù),那么該函數(shù)就作為組件的 render 函數(shù);如果 setup 返回值是一個對象,那么則把它的值做一層代理,賦值給 instance.setupState。

組件的渲染

setupComponent 函數(shù)執(zhí)行完,就要執(zhí)行 setupRenderEffect 設(shè)置并運行帶副作用的渲染函數(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é)點
      initialVNode.el = subTree.el
      instance.isMounted = true
    }
    else {
      // 更新組件
    }
  }
  // 創(chuàng)建組件渲染的副作用響應(yīng)式對象
  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)式庫的 ReactiveEffect 函數(shù)創(chuàng)建了一個副作用實例 effect,并且把 instance.update 函數(shù)指向了 effect.run。

當(dāng)首次執(zhí)行 instance.update 時,內(nèi)部就會執(zhí)行 componentUpdateFn 函數(shù)觸發(fā)組件的首次渲染。

當(dāng)組件的數(shù)據(jù)發(fā)生變化時,組件渲染函數(shù) componentUpdateFn 會重新執(zhí)行一遍,從而達(dá)到重新渲染組件的目的。

componentUpdateFn 函數(shù)內(nèi)部會判斷這是一次初始渲染還是組件的更新渲染,目前我們只需關(guān)注初始渲染流程。

初始渲染主要做兩件事情:執(zhí)行 renderComponentRoot 函數(shù)渲染組件生成 subTree 子樹 vnode,執(zhí)行 patch 函數(shù)把 subTree 掛載到 container 中。

renderComponentRoot 內(nèi)部會執(zhí)行組件的 render 函數(shù)渲染生成一棵 vnode 樹,然后在 patch 過程中把 vnode 樹渲染生成真正的 DOM 樹。

接下來看 patch 函數(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é)點
      break
    case Comment:
      // 處理注釋節(jié)點
      break
    case Static:
      // 處理靜態(tài)節(jié)點
      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)部會根據(jù) vnode 節(jié)點類型的不同,執(zhí)行不同的處理邏輯,最終目的就是構(gòu)造出一棵 DOM 樹。

Template Ref 的注冊

遍歷渲染 DOM 樹的過程,實際上就是一個遞歸執(zhí)行 patch 函數(shù)的過程,在 patch 函數(shù)的最后,也就是當(dāng)前節(jié)點掛載后,會判斷如果新的 vnode 存在 ref 屬性,則執(zhí)行 setRef 完成 Template Ref 的注冊邏輯。

顯然,對于我們示例來說,div 標(biāo)簽生成的元素 vnode,它對應(yīng)的 ref 的值是 {i: currentRenderingInstance, r: "root"},滿足條件,則會執(zhí)行 setRef 函數(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 指向組件的實例,否則指向元素的 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 動態(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ù)的目的就是對當(dāng)前節(jié)點的引用求值,并保存下來。節(jié)點的引用值 refValue 會根據(jù) vnode 的類型有所區(qū)別,如果 vnode 是組件 vnode,refValue 指向組件的實例,否則指向元素的 DOM。

這就是平時我們給普通元素節(jié)點設(shè)置 ref 能拿到對應(yīng)的 DOM,而對組件節(jié)點設(shè)置 ref 能拿到組件實例的原因。

從傳遞的參數(shù) rawRef 中,可以獲取到當(dāng)前組件實例 owner,以及對應(yīng)的 ref 值,對于我們的示例,rawRef 的值是 {i: currentRenderingInstance, r: "root"},那么對應(yīng)的 ref 就是 root 字符串。

如果 ref 是字符串類型,且 owner.setupState 中包含了這個字符串屬性,那么則把這個 refValue 保留到 owner.setupStatep[ref] 中。

前面說到,在 handleSetupResult 的時候,我們已經(jīng)把 setup 函數(shù)的返回值保留到 instance.setupState 中了:

instance.setupState = proxyRefs(setupResult)

這里要注意,使用了 proxyRefs 函數(shù)對 setupResult 做了響應(yīng)式處理,來看它的實現(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)式對象,那么會使用 Proxy 對它做一層代理,它有什么作用呢?接下來結(jié)合前面的示例進(jìn)行分析。

示例中,在 setup 函數(shù)內(nèi)部,我們利用了 ref API 定義了響應(yīng)式對象 root:

const root = ref(null) 

然后把這個響應(yīng)式對象作為 setupResult 返回:

const root = ref(null)
return {
  root
}

在 handleSetupResult 的時候,相當(dāng)于執(zhí)行:

instance.setupState = proxyRefs({ root: root})

經(jīng)過 setRef 的處理,會執(zhí)行:

instance.setupState['root'] = refValue // DOM

執(zhí)行這個操作的時候,會觸發(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 對象。那么 oldValue 指向的就是響應(yīng)式對象 root,并且滿足 isRef(oldValue) && !isRef(value),因此會執(zhí)行:

oldValue.value = value

這樣響應(yīng)式對象 root 的值就指向了 DOM 對象,所以在 onMounted 后就可以通過 root.value 訪問到對應(yīng)的 DOM 對象了。

總結(jié)

ref API 定義的 root 和在模板中定義的 ref=root 是并不是一個東西,它們之所以能關(guān)聯(lián)起來,是返回的 setupResult 中的屬性名和 Template ref 中指向的字符串同名。我們對示例稍加修改:

<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 后通過 rootRef.value 訪問到對應(yīng)的 DOM 對象的。

因此局部定義的 ref 響應(yīng)式變量并不需要和 Template ref 指向的字符串同名,只需要 setupResult 中保存 ref 響應(yīng)式變量的屬性名和 Template ref 指向的字符串同名即可,因為內(nèi)部是通過該字符串檢索的。

以上就是Template ref在Vue3中的實現(xiàn)示例詳解的詳細(xì)內(nèi)容,更多關(guān)于Vue3實現(xiàn)Template ref的資料請關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 詳解Vue2.0里過濾器容易踩到的坑

    詳解Vue2.0里過濾器容易踩到的坑

    本篇文章主要介紹了Vue2.0里過濾器容易踩到的坑,vue2.0需要自己定義過濾器。有興趣的可以了解一下
    2017-06-06
  • 如何巧用Vue.extend繼承組件實現(xiàn)el-table雙擊可編輯(不使用v-if、v-else)

    如何巧用Vue.extend繼承組件實現(xiàn)el-table雙擊可編輯(不使用v-if、v-else)

    這篇文章主要給大家介紹了關(guān)于如何巧用Vue.extend繼承組件實現(xiàn)el-table雙擊可編輯的相關(guān)資料,不使用v-if、v-else,文中通過實例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-06-06
  • Vue實現(xiàn)點擊時間獲取時間段查詢功能

    Vue實現(xiàn)點擊時間獲取時間段查詢功能

    這篇文章主要為大家詳細(xì)介紹了Vue實現(xiàn)點擊時間獲取時間段查詢功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下
    2018-04-04
  • Vuex 快速入門(簡單易懂)

    Vuex 快速入門(簡單易懂)

    這篇文章主要介紹了Vuex 快速入門(簡單易懂),小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2018-09-09
  • vue組件中使用iframe元素的示例代碼

    vue組件中使用iframe元素的示例代碼

    本篇文章主要介紹了vue組件中使用iframe元素的示例代碼,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧
    2017-12-12
  • vue框架制作購物車小球動畫效果實例代碼

    vue框架制作購物車小球動畫效果實例代碼

    最近在學(xué)習(xí)前端制作了一個購物車小球的動畫效果,本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友參考下吧
    2019-09-09
  • vue恢復(fù)初始數(shù)據(jù)this.$data,this.$options.data()解析

    vue恢復(fù)初始數(shù)據(jù)this.$data,this.$options.data()解析

    這篇文章主要介紹了vue恢復(fù)初始數(shù)據(jù)this.$data,this.$options.data()解析,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教
    2022-03-03
  • Vue的v-if和v-show的區(qū)別圖文介紹

    Vue的v-if和v-show的區(qū)別圖文介紹

    這篇文章主要介紹了Vue的v-if和v-show的區(qū)別圖文介紹,本文通過示例代碼給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下
    2023-03-03
  • vue實現(xiàn)tab切換的3種方式及切換保持?jǐn)?shù)據(jù)狀態(tài)

    vue實現(xiàn)tab切換的3種方式及切換保持?jǐn)?shù)據(jù)狀態(tài)

    這篇文章主要給大家介紹了關(guān)于vue實現(xiàn)tab切換的3種方式及切換保持?jǐn)?shù)據(jù)狀態(tài)的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2021-05-05
  • vuex actions異步修改狀態(tài)的實例詳解

    vuex actions異步修改狀態(tài)的實例詳解

    今天小編就為大家分享一篇vuex actions異步修改狀態(tài)的實例詳解,具有很好的參考價值,希望對大家有所幫助。一起跟隨小編過來看看吧
    2019-11-11

最新評論