Vue3 的组合式 API 以及基于 Proxy 响应式原理曾经有很多文章介绍过了,除了这些比拟亮眼的更新,Vue3 还新增了一个内置组件:Teleport。这个组件的作用次要用来将模板内的 DOM 元素挪动到其余地位。

应用场景

业务开发的过程中,咱们常常会封装一些罕用的组件,例如 Modal 组件。置信大家在应用 Modal 组件的过程中,常常会遇到一个问题,那就是 Modal 的定位问题。

话不多说,咱们先写一个简略的 Modal 组件。

<!-- 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 组件。

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

如上图所示, div.container 下弹窗组件失常展现。应用 fixed 进行布局的元素,在个别状况下会绝对于屏幕视窗来进行定位,然而如果父元素的 transform, perspectivefilter 属性不为 none 时,fixed 元素就会绝对于父元素来进行定位。

咱们只须要把 .container 类的 transform 稍作批改,弹窗组件的定位就会错乱。

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

这个时候,应用 Teleport 组件就能解决这个问题了。

Teleport 提供了一种洁净的办法,容许咱们管制在 DOM 中哪个父节点下出现 HTML,而不用求助于全局状态或将其拆分为两个组件。 -- Vue 官网文档

咱们只须要将弹窗内容放入 Teleport 内,并设置 to 属性为 body,示意弹窗组件每次渲染都会做为 body 的子级,这样之前的问题就能失去解决。

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

能够在 https://codesandbox.io/embed/vue-modal-h5g8y 查看代码。

源码解析

咱们能够先写一个简略的模板,而后看看 Teleport 组件通过模板编译后,生成的代码。

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.tsexport 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.tsexport const isTeleport = type => type.__isTeleportexport const Teleport = {  __isTeleport: true,  process() {}}

传入 createBlock 的第一个参数为 Teleport,最初失去的 vnode 中会有一个 shapeFlag 属性,该属性用来示意 vnode 的类型。isTeleport(type) 失去的后果为 true,所以 shapeFlag 属性最初的值为 ShapeFlags.TELEPORT1 << 6)。

// packages/shared/src/shapeFlags.tsexport 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.tsconst 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.tsconst isTeleportDisabled = props => props.disabledexport 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 组件,就能轻松解决弹窗元素定位不精确的问题。