Vue3 Teleport 組件的實踐及原理

Vue3 的組合式 API 以及基於 Proxy 響應式原理已經有不少文章介紹過了,除了這些比較亮眼的更新,Vue3 還新增了一個內置組件:Teleport。這個組件的做用主要用來將模板內的 DOM 元素移動到其餘位置。css

使用場景

業務開發的過程當中,咱們常常會封裝一些經常使用的組件,例如 Modal 組件。相信你們在使用 Modal 組件的過程當中,常常會遇到一個問題,那就是 Modal 的定位問題。html

話很少說,咱們先寫一個簡單的 Modal 組件。vue

<!-- Modal.vue -->
<style lang="scss">
.modal {
  &__mask {
    position: fixed;
    top: 0;
    left: 0;
    width: 100vw;
    height: 100vh;
    background: rgba(0, 0, 0, 0.5);
  }
  &__main {
    margin: 0 auto;
    margin-bottom: 5%;
    margin-top: 20%;
    width: 500px;
    background: #fff;
    border-radius: 8px;
  }
  /* 省略部分樣式 */
}
</style>
<template>
  <div class="modal__mask">
    <div class="modal__main">
      <div class="modal__header">
        <h3 class="modal__title">彈窗標題</h3>
        <span class="modal__close">x</span>
      </div>
      <div class="modal__content">
        彈窗文本內容
      </div>
      <div class="modal__footer">
        <button>取消</button>
        <button>確認</button>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  setup() {
    return {};
  },
};
</script>

而後咱們在頁面中引入 Modal 組件。node

<!-- App.vue -->
<style lang="scss">
.container {
  height: 80vh;
  margin: 50px;
  overflow: hidden;
}
</style>
<template>
  <div class="container">
    <Modal />
  </div>
</template>

<script>
export default {
  components: {
    Modal,
  },
  setup() {
    return {};
  }
};
</script>

Modal

如上圖所示, div.container 下彈窗組件正常展現。使用 fixed 進行佈局的元素,在通常狀況下會相對於屏幕視窗來進行定位,可是若是父元素的 transform, perspectivefilter 屬性不爲 none 時,fixed 元素就會相對於父元素來進行定位。緩存

咱們只須要把 .container 類的 transform 稍做修改,彈窗組件的定位就會錯亂。ide

<style lang="scss">
.container {
  height: 80vh;
  margin: 50px;
  overflow: hidden;
  transform: translateZ(0);
}
</style>

Modal

這個時候,使用 Teleport 組件就能解決這個問題了。佈局

Teleport 提供了一種乾淨的方法,容許咱們控制在 DOM 中哪一個父節點下呈現 HTML,而沒必要求助於全局狀態或將其拆分爲兩個組件。 -- Vue 官方文檔

咱們只須要將彈窗內容放入 Teleport 內,並設置 to 屬性爲 body,表示彈窗組件每次渲染都會作爲 body 的子級,這樣以前的問題就能獲得解決。ui

<template>
  <teleport to="body">
    <div class="modal__mask">
      <div class="modal__main">
        ...
      </div>
    </div>
  </teleport>
</template>

能夠在 https://codesandbox.io/embed/vue-modal-h5g8y 查看代碼。spa

使用 Teleport 的 Modal

源碼解析

咱們能夠先寫一個簡單的模板,而後看看 Teleport 組件通過模板編譯後,生成的代碼。code

Vue.createApp({
  template: `
    <Teleport to="body">
      <div> teleport to body </div>  
    </Teleport>
  `
})

模板編譯後的代碼

簡化後代碼:

function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode, openBlock, createBlock, Teleport } = Vue
    return (openBlock(), createBlock(Teleport, { to: "body" }, [
      createVNode("div", null, " teleport to body ", -1 /* HOISTED */)
    ]))
  }
}

能夠看到 Teleport 組件經過 createBlock 進行建立。

// packages/runtime-core/src/renderer.ts
export function createBlock(
    type, props, children, patchFlag
) {
  const vnode = createVNode(
    type,
    props,
    children,
    patchFlag
  )
  // ... 省略部分邏輯
  return vnode
}

export function createVNode(
  type, props, children, patchFlag
) {
  // class & style normalization.
  if (props) {
    // ...
  }

  // encode the vnode type information into a bitmap
  const shapeFlag = isString(type)
    ? ShapeFlags.ELEMENT
    : __FEATURE_SUSPENSE__ && isSuspense(type)
      ? ShapeFlags.SUSPENSE
      : isTeleport(type)
        ? ShapeFlags.TELEPORT
        : isObject(type)
          ? ShapeFlags.STATEFUL_COMPONENT
          : isFunction(type)
            ? ShapeFlags.FUNCTIONAL_COMPONENT
            : 0

  const vnode: VNode = {
    type,
    props,
    shapeFlag,
    patchFlag,
    key: props && normalizeKey(props),
    ref: props && normalizeRef(props),
  }

  return vnode
}

// packages/runtime-core/src/components/Teleport.ts
export const isTeleport = type => type.__isTeleport
export const Teleport = {
  __isTeleport: true,
  process() {}
}

傳入 createBlock 的第一個參數爲 Teleport,最後獲得的 vnode 中會有一個 shapeFlag 屬性,該屬性用來表示 vnode 的類型。isTeleport(type) 獲得的結果爲 true,因此 shapeFlag 屬性最後的值爲 ShapeFlags.TELEPORT1 << 6)。

// packages/shared/src/shapeFlags.ts
export const enum ShapeFlags {
  ELEMENT = 1,
  FUNCTIONAL_COMPONENT = 1 << 1,
  STATEFUL_COMPONENT = 1 << 2,
  TEXT_CHILDREN = 1 << 3,
  ARRAY_CHILDREN = 1 << 4,
  SLOTS_CHILDREN = 1 << 5,
  TELEPORT = 1 << 6,
  SUSPENSE = 1 << 7,
  COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
  COMPONENT_KEPT_ALIVE = 1 << 9
}

在組件的 render 節點,會依據 typeshapeFlag 走不一樣的邏輯。

// packages/runtime-core/src/renderer.ts
const render = (vnode, container) => {
  if (vnode == null) {
    // 當前組件爲空,則將組件銷燬
    if (container._vnode) {
      unmount(container._vnode, null, null, true)
    }
  } else {
    // 新建或者更新組件
    // container._vnode 是以前已建立組件的緩存
    patch(container._vnode || null, vnode, container)
  }
  container._vnode = vnode
}

// patch 是表示補丁,用於 vnode 的建立、更新、銷燬
const patch = (n1, n2, container) => {
  // 若是新舊節點的類型不一致,則將舊節點銷燬
  if (n1 && !isSameVNodeType(n1, n2)) {
    unmount(n1)
  }
  const { type, ref, shapeFlag } = n2
  switch (type) {
    case Text:
      // 處理文本
      break
    case Comment:
      // 處理註釋
      break
    // case ...
    default:
      if (shapeFlag & ShapeFlags.ELEMENT) {
        // 處理 DOM 元素
      } else if (shapeFlag & ShapeFlags.COMPONENT) {
        // 處理自定義組件
      } else if (shapeFlag & ShapeFlags.TELEPORT) {
        // 處理 Teleport 組件
        // 調用 Teleport.process 方法
        type.process(n1, n2, container...);
      } // else if ...
  }
}

能夠看到,在處理 Teleport 時,最後會調用 Teleport.process 方法,Vue3 中不少地方都是經過 process 的方式來處理 vnode 相關邏輯的,下面咱們重點看看 Teleport.process 方法作了些什麼。

// packages/runtime-core/src/components/Teleport.ts
const isTeleportDisabled = props => props.disabled
export const Teleport = {
  __isTeleport: true,
  process(n1, n2, container) {
    const disabled = isTeleportDisabled(n2.props)
    const { shapeFlag, children } = n2
    if (n1 == null) {
      const target = (n2.target = querySelector(n2.prop.to))      
      const mount = (container) => {
        // compiler and vnode children normalization.
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(children, container)
        }
      }
      if (disabled) {
        // 開關關閉,掛載到原來的位置
        mount(container)
      } else if (target) {
        // 將子節點,掛載到屬性 `to` 對應的節點上
        mount(target)
      }
    }
    else {
      // n1不存在,更新節點便可
    }
  }
}

其實原理很簡單,就是將 Teleportchildren 掛載到屬性 to 對應的 DOM 元素中。爲了方便理解,這裏只是展現了源碼的九牛一毛,省略了不少其餘的操做。

總結

但願在閱讀文章的過程當中,你們可以掌握 Teleport 組件的用法,並使用到業務場景中。儘管原理十分簡單,可是咱們有了 Teleport 組件,就能輕鬆解決彈窗元素定位不許確的問題。

image

相關文章
相關標籤/搜索