Vue内置组件Teleport的使用

 更新时间:2023年05月22日 09:41:52   作者:Junior_FE_2022  
Teleport是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去,本文就来介绍一下如何使用,感兴趣的可以了解一下

背景

当我们想在 vue 中开发一个能够指定位置渲染的组件例如 tooltip、modal 时可能首先想到的是去引 ui 库中的组件,或者自己手写一个,但 vue3 中提供的内置组件 Teleport 能够帮我们轻松解决问题,下面就来介绍下它的用法以及实现原理

正文

官网对于它的介绍是: <Teleport> 是一个内置组件,它可以将一个组件内部的一部分模板“传送”到该组件的 DOM 结构外层的位置去

用法及属性

<Teleport to="body" :disabled="disabled">
  <div></div>
</Teleport>
//to: 传送的目标,dom对象/css选择器字符串 //disabled: 是否禁用
const disabled = ref<boolean>(false)

用法还是挺简单的,然后来看下需要注意的地方

Tip

  • <Teleport> 挂载时,传送的 to 目标必须已经存在于 DOM 中。如果目标元素也是由 Vue 渲染的,需要确保在挂载 <Teleport> 之前先挂载该元素
  • 可以搭配组件使用,只改变了渲染的 DOM 结构,它不会影响组件间的逻辑关系,<Teleport> 和内部组件始终保持父子关系,也就是说 props 和 provide 都可以正常使用
  • 多个 Teleport 会共享目标,多个 <Teleport> 组件可以将其内容挂载在同一个目标元素上,而顺序就是顺次追加
<Teleport to=".modal">
  <div>A</div>
</Teleport>
<Teleport to=".modal">
  <div>B</div>
</Teleport>

结果:

<div class=".modal">
  <div>A</div>
  <div>B</div>
</div>

实现一个简单的 tooltip

<div
  v-for="(item, index) in array"
  :key="index"
  @mousemove="handleMousemove($event, index)"
  @mouseleave="handleMouseleave"
></div>
<teleport to="body">
  <div
    class="max-w-60vw rounded-10px p-10px border-2px fixed border-indigo-300 bg-white"
    ref="toolTipRef"
    :style="{
            left: tooltipStyle.x,
            top: tooltipStyle.y,
            opacity: tooltipStyle.opacity,
        }"
  >
    <span>{ tooltipStyle.content }</span>
  </div>
</teleport>

实现的大概逻辑就是鼠标目标元素上划过时更改 teleport 内部元素的透明度,移除时将透明度改为 0

<script lang="ts" setup>
    const tooltipStyle = reactive({
        x: '0px',
        y: '0px',
        content: '',
        opacity: 0,
    });
    const array = ref<string[]>(['test'])
    const handleMousemove = (e: MouseEvent, index: number) => {
        tooltipStyle.opacity = 1;
        tooltipStyle.x = e.x + 10 + 'px';
        tooltipStyle.y = e.y + 10 + 'px';
        tooltipStyle.content = array.value[index]
    };
    const handleMouseleave = () => {
        tooltipStyle.opacity = 0;
    };
</script>

看完用法,接着就到本文的重点了,让我们来探究下核心源码是怎么实现的

原理

首先我们可以考虑的问题是将 teleport 的渲染和正常 vnode 的渲染分离开,这样做的优点是:

  • 渲染函数中保持整洁
  • 当我们没有使用 Teleport 时,因为将这个渲染逻辑单独抽出来,所以可以利用 tree-shaking 将相关的代码删除。所以 vue 的做法是针对 teleport 组件重新写了套渲染代码:

在 renderer 的 patch 函数中,如果遇到类型是 teleport 的,就使用自己的挂载方法,这里的 TeleportImpl 就是具体的实现对象

else if (shapeFlag & ShapeFlags.TELEPORT) {
    ;(type as typeof TeleportImpl).process(
        n1 as TeleportVNode,
        n2 as TeleportVNode,
        container,
        anchor,
        parentComponent,
        parentSuspense,
        isSVG,
        slotScopeIds,
        optimized,
        internals
    )
}

移动函数move

if (shapeFlag & ShapeFlags.TELEPORT) {
    ;(type as typeof TeleportImpl).move(vnode, container, anchor, internals)
    return
}

下面我们来看具体是怎么实现的(保留核心代码)

对应的位置在仓库的Teleport.ts文件中

const TeleportImpl = {
  process(
    n1: TeleportVNode | null,
    n2: TeleportVNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    internals: RendererInternals
  ) {
    const {
      mc: mountChildren,
      pc: patchChildren,
      pbc: patchBlockChildren,
      o: { insert, querySelector, createText, createComment },
    } = internals
    const disabled = isTeleportDisabled(n2.props)
    let { shapeFlag, children, dynamicChildren } = n2
    //如果是首次挂载
    if (n1 == null) {
      // insert anchors in the main view
      const placeholder = (n2.el = createText(''))
      const mainAnchor = (n2.anchor = createText(''))
      insert(placeholder, container, anchor)
      insert(mainAnchor, container, anchor)
      //resolveTarget处理传入的to属性
      const target = (n2.target = resolveTarget(n2.props, querySelector))
      const targetAnchor = (n2.targetAnchor = createText(''))
      if (target) {
        insert(targetAnchor, target)
      }
      //自己的挂载方法
      const mount = (container: RendererElement, anchor: RendererNode) => {
        // Teleport *always* has Array children. This is enforced in both the
        // compiler and vnode children normalization.
        if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
          mountChildren(children as VNodeArrayChildren, container, anchor, parentComponent)
        }
      }
      if (disabled) {
        mount(container, mainAnchor)
      } else if (target) {
        mount(target, targetAnchor)
      }
    } else {
      // 更新
      n2.el = n1.el
      const mainAnchor = (n2.anchor = n1.anchor)!
      const target = (n2.target = n1.target)!
      const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
      const wasDisabled = isTeleportDisabled(n1.props)
      const currentContainer = wasDisabled ? container : target
      const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
      if (dynamicChildren) {
        // fast path when the teleport happens to be a block root
        patchBlockChildren(n1.dynamicChildren!, dynamicChildren, currentContainer, parentComponent)
        // even in block tree mode we need to make sure all root-level nodes
        // in the teleport inherit previous DOM references so that they can
        // be moved in future patches.
        traverseStaticChildren(n1, n2, true)
      }
      if (disabled) {
        if (!wasDisabled) {
          // enabled -> disabled
          // move into main container
          moveTeleport(n2, container, mainAnchor, internals, TeleportMoveTypes.TOGGLE)
        }
      } else {
        // target changed
        if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
          const nextTarget = (n2.target = resolveTarget(n2.props, querySelector))
          if (nextTarget) {
            moveTeleport(n2, nextTarget, null, internals, TeleportMoveTypes.TARGET_CHANGE)
          }
        } else if (wasDisabled) {
          // disabled -> enabled
          // move into teleport target
          moveTeleport(n2, target, targetAnchor, internals, TeleportMoveTypes.TOGGLE)
        }
      }
    }
  },
}

然后我们看 process 里的重要函数

moveTarget

function moveTeleport(
  vnode: VNode,
  container: RendererElement,
  parentAnchor: RendererNode | null,
  { o: { insert }, m: move }: RendererInternals,
  moveType: TeleportMoveTypes = TeleportMoveTypes.REORDER
) {
  // move target anchor if this is a target change.
  if (moveType === TeleportMoveTypes.TARGET_CHANGE) {
    insert(vnode.targetAnchor!, container, parentAnchor)
  }
  const { el, anchor, shapeFlag, children, props } = vnode
  const isReorder = moveType === TeleportMoveTypes.REORDER
  // move main view anchor if this is a re-order.
  if (isReorder) {
    insert(el!, container, parentAnchor)
  }
  // if this is a re-order and teleport is enabled (content is in target)
  // do not move children. So the opposite is: only move children if this
  // is not a reorder, or the teleport is disabled
  if (!isReorder || isTeleportDisabled(props)) {
    // Teleport has either Array children or no children.
    if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
      for (let i = 0; i < (children as VNode[]).length; i++) {
        move((children as VNode[])[i], container, parentAnchor, MoveType.REORDER)
      }
    }
  }
  // move main view anchor if this is a re-order.
  if (isReorder) {
    insert(anchor!, container, parentAnchor)
  }
}

resolveTarget

const resolveTarget = <T = RendererElement>(
  props: TeleportProps | null,
  select: RendererOptions['querySelector']
): T | null => {
  const targetSelector = props && props.to
  if (isString(targetSelector)) {
    if (!select) {
      return null
    } else {
      const target = select(targetSelector)
      return target as any
    }
  } else {
    return targetSelector as any
  }
}

看起来有些复杂,不过让我们理一下思路:上面我们提到了要实现自己的渲染方法,所以我们可以先写基本的渲染函数,然后在内部需要区分是首次挂载还是组件更新,但如果是 teleport 的接收的参数更改了呢,所以这时候就要去主动实现一个 move 函数将内容移动到新的节点下。好了,有了思路后我们来尝试写出来

先来实现组件的挂载与更新

const Teleport = {
  __isTeleport: true,
  process(n1, n2, container, anchor, internals) {
    //通过internals拿到渲染器内部方法
    const { patch, patchChildren } = internals
    // 如果oldVnode不存在,就是全新挂载
    if (!n1) {
      //mount
      //获取挂载点
      const target =
        typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to
      //将newVnode挂载
      n2.children.forEach((child) => patch(null, child, target, anchor))
    } else {
      //更新
      patchChildren(n1, n2, container)
    }
  },
}

上面我们提到了更新,但如果是 to 属性更改了呢,所以需要有个分支来处理

//如果新旧 to 参数不同,需要对内容移动
if (n2.props.to !== n1.props.to) {
  //获取新容器
  const newTarget =
    typeof n2.props.to === 'string' ? document.querySelector(n2.props.to) : n2.props.to
  //移动到新的容器上
  n2.children.forEach((child) => move(child, newTarget))
}

传入 move 函数

else if (shapeFlag & ShapeFlags.TELEPORT) {
    type.process(n1, n2, container, anchor, internals) {
        patch,
        patchChildren,
        move(vnode, container, anchor) {
            //这里只处理了组件或者普通元素
            const el = vnode.component ? vnode.component.subTree.el : vnode.el
            const { insert } = internals
            insert(el, container, anchor)
        }
    }
}

这里我们省略了处理 disabled 的 remove 函数,不是本文研究的重点,具体可以看源码的 Teleport 文件

到此这篇关于Vue内置组件Teleport的使用的文章就介绍到这了,更多相关Vue Teleport内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Vue动态组件和异步组件原理详解

    Vue动态组件和异步组件原理详解

    这篇文章主要给大家介绍了关于Vue动态组件和异步组件原理的相关资料,文中通过示例代码介绍的非常详细,对大家学习或者使用Vue具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-05-05
  • Vue首屏白屏问题的原因和解决方法讲解

    Vue首屏白屏问题的原因和解决方法讲解

    这篇文章主要介绍了Vue首屏白屏问题的原因和解决方法讲解,Vue首屏白屏问题是指在页面初次加载时,部分或全部内容无法正常显示,出现空白的情况。其原因可能是因为页面渲染速度过慢,或者是因为网络请求等问题导致数据无法及时加载
    2023-05-05
  • vue获取参数的几种方式总结

    vue获取参数的几种方式总结

    这篇文章主要介绍了vue获取参数的几种方式总结,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-08-08
  • Vue 实例事件简单示例

    Vue 实例事件简单示例

    这篇文章主要介绍了Vue 实例事件,结合简单示例形势分析了vue.js事件响应与页面元素相关操作技巧,需要的朋友可以参考下
    2019-09-09
  • 如何用VUE和Canvas实现雷霆战机打字类小游戏

    如何用VUE和Canvas实现雷霆战机打字类小游戏

    这篇文章主要介绍了如何用VUE和Canvas实现雷霆战机打字类小游戏,麻雀虽小,五脏俱全,对游戏感兴趣的同学,可以参考下,研究里面的原理和实现方法
    2021-04-04
  • 关于在vscode使用webpack指令显示

    关于在vscode使用webpack指令显示"因为在此系统中禁止运行脚本"问题(完美解决)

    这篇文章主要介绍了解决在vscode使用webpack指令显示"因为在此系统中禁止运行脚本"问题,本文给大家分享完美解决方法,需要的朋友可以参考下
    2021-07-07
  • vue+jsplumb实现工作流程图的项目实践

    vue+jsplumb实现工作流程图的项目实践

    本文主要介绍了vue+jsplumb实现工作流程图的项目实践,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2022-04-04
  • solid.js响应式createSignal 源码解析

    solid.js响应式createSignal 源码解析

    这篇文章主要为大家介绍了solid.js响应式createSignal 源码解析,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-09-09
  • 基于vue3&element-plus的暗黑模式实例详解

    基于vue3&element-plus的暗黑模式实例详解

    实现暗黑主题的方式有很多种,也有很多成型的框架可以直接使用,下面这篇文章主要给大家介绍了关于基于vue3&element-plus的暗黑模式的相关资料,需要的朋友可以参考下
    2022-12-12
  • Vue多重文字描边组件实现示例详解

    Vue多重文字描边组件实现示例详解

    这篇文章主要为大家介绍了Vue多重文字描边组件实现示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-06-06

最新评论