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

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

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

背景

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

先來看一個簡單的例子:

<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 的響應式對象,并把它放到 setup 函數(shù)的返回對象中。

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

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

模板的編譯

我們還是結合示例來分析,首先借助于模板導出工具,可以看到它編譯后的 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ù)內部使用了 createElementBlock 函數(shù)來創(chuà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 的相關邏輯,重點看 vnode 的創(chuàng)建過程。

createElementBlock 函數(shù)內部通過 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 標簽在 render 函數(shù)執(zhí)行之后轉換成一個元素 vnode,它對應的 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)
  // 設置組件實例
  setupComponent(instance)
  // 設置并運行帶副作用的渲染函數(shù)
  setupRenderEffect(instance, initialVNode, container, anchor)
}

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

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

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)
  // 設置有狀態(tài)的組件實例
  const setupResult = isStateful
    ? setupStatefulComponent(instance, isSSR)
    : undefined
  return setupResult
}

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

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

接下來看 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 {
    // 完成組件實例設置
    finishComponentSetup(instance)
  }
}

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

這里我們重點關注判斷處理 setup 函數(shù)部分,首先它會通過 callWithErrorHandling 的方式來執(zhí)行組件定義的 setup 函數(shù),并把它的返回值放到 setupResult 中,然后執(zhí)行 handleSetupResult 函數(shù)來處理 setup 執(zhí)行結果,來看它的實現(xiàn):

function handleSetupResult(instance, setupResult) {
  if (isFunction(setupResult)) {
    // setup 返回渲染函數(shù)
    instance.render = setupResult
  }
  else if (isObject(setupResult)) {
    // 把 setup 返回結果做一層代理
    instance.setupState = proxyRefs(setupResult)
  }
  finishComponentSetup(instance)
}

可以看到,如果 setup 函數(shù)返回值是一個函數(shù),那么該函數(shù)就作為組件的 render 函數(shù);如果 setup 返回值是一個對象,那么則把它的值做一層代理,賦值給 instance.setupState。

組件的渲染

setupComponent 函數(shù)執(zhí)行完,就要執(zhí)行 setupRenderEffect 設置并運行帶副作用的渲染函數(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)建組件渲染的副作用響應式對象
  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ù)內部利用響應式庫的 ReactiveEffect 函數(shù)創(chuàng)建了一個副作用實例 effect,并且把 instance.update 函數(shù)指向了 effect.run。

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

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

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

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

renderComponentRoot 內部會執(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
      }
  }
  // 設置 ref
  if (ref != null && parentComponent) {
    setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
  }
}

在組件的首次渲染階段,patch 函數(shù)內部會根據(jù) vnode 節(jié)點類型的不同,執(zhí)行不同的處理邏輯,最終目的就是構造出一棵 DOM 樹。

Template Ref 的注冊

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

顯然,對于我們示例來說,div 標簽生成的元素 vnode,它對應的 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ù)的目的就是對當前節(jié)點的引用求值,并保存下來。節(jié)點的引用值 refValue 會根據(jù) vnode 的類型有所區(qū)別,如果 vnode 是組件 vnode,refValue 指向組件的實例,否則指向元素的 DOM。

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

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

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

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

instance.setupState = proxyRefs(setupResult)

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

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

const root = ref(null) 

然后把這個響應式對象作為 setupResult 返回:

const root = ref(null)
return {
  root
}

在 handleSetupResult 的時候,相當于執(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 指向的就是響應式對象 root,并且滿足 isRef(oldValue) && !isRef(value),因此會執(zhí)行:

oldValue.value = value

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

總結

ref API 定義的 root 和在模板中定義的 ref=root 是并不是一個東西,它們之所以能關聯(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 訪問到對應的 DOM 對象的。

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

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

相關文章

最新評論