19

深度解读 Vue3 源码 | 内置组件 teleport 是什么“来头”?

 3 years ago
source link: https://segmentfault.com/a/1190000025134313
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

前言

NfQbu2I.png!mobile

上一篇文章,我们讲了「Vue3」 runtimecompile 结合的 patch 过程。似乎,由于文章内容太过晦涩的原因,并没有收到很多同学的反馈。但是,其实这里我想说的是源码就是这样, 初见时如陌生人一般,再见时如初恋,既熟悉又怀念

所以,这一篇文章,我打算讲个「Vue3」中 轻松愉快 的设计点——内置组件 teleport 。那么,这次我们将会从 使用角度和源码角度 去深入了解 teleport 组件是如何实现的?

什么是 teleport 组件

当然,如果已经懂得怎么使用 teleport 组件的同学可以跳过这个小节。

我们从使用性的角度思考,很现实的一点,就是 teleport 组件 能带给我们什么价值

最经典的回答就是开发中使用 Modal 模态框的场景。通常,我们会在中后台的业务开发中频繁地使用到模态框。可能对于中台还好,它们会搞一些 low code减少开发成本 ,但这也是一般大公司或者技术较强的公司才能实现的。

而实际情况下,我们传统的后台开发,就是会存在频繁地 手动使用 Modal 的情况,它看起来会是这样:

<div class="page">
  <div class="header">我希望点击我出现弹窗</div>
  <!--假设此处有 100 行代码-->
  ....
  <Modal>
    <div>
      我是 header 希望出的弹窗
    </div>
  </Modal>
</div>

这样的代码,凸显出来的问题,就是 脱离了所见即所得 的理念,即我头部希望出现的弹窗, 由于样式的问题 ,我需要将 Modal 写在最下面。

teleport 组件的出现, 首当其冲 的就是解决这个问题,仍然还是上面那个栗子,通过 teleport 组件我们可以这么写:

<div class="page">
  <div class="header">我希望点击我出现弹窗</div>
  <!--弹窗内容-->
  <teleport to="#modal-header">
    <div>
      我是 header 希望出的弹窗
    </div>
  </teleport>
  <!--假设此处有 100 行代码-->
  ....
  <Modal id="modal-header">
  </Modal>
</div>

结合 teleport 组件使用 modal ,一方面,我们的弹窗内容,就可以符合我们的正常的思考逻辑。并且,另一方面,也可以充分地提高 Modal 组件的 可复用性 ,即页面中一个 Modal 负责展示不同内容。

从源码角度认识 teleport 组件

假设,此时我们有一个这样的栗子:

<div id="my-heart">
  i love you 
</div>
<teleport to="#my-heart" >
  honey
</teleport>

通过上面的介绍,我们很容易就知道,它最终渲染到页面上的 DOM 会是这样:

<div id="my-heart">
  i love you honey
</div>

那么,这个时候我们就会想, teleport 组件中的内容,究竟是如何 走进了我的心 ?这,说来话长,长话短说, 我们直接上图

zuEziiJ.png!mobile

通过流程图,我们可以知道整体 teleport 的工作流并不复杂。那么,接下来,我们再从 源码设计 的角度认识 teleport 组件的运行机制。

这里,我们仍然会分为 compileruntime 两个阶段去介绍。

compile 编译生成的 render 函数

仍然是我们上面的那个栗子,它经过 compile 编译处理后生成的 可执行代码 会是这样:

const _Vue = Vue
const { createVNode: _createVNode, createTextVNode: _createTextVNode } = _Vue

const _hoisted_1 = _createVNode("div", { id: "my-heart" }, "i love you ", -1 /* HOISTED */)
const _hoisted_2 = _createTextVNode("honey")

return function render(_ctx, _cache) {
  with (_ctx) {
    const { createVNode: _createVNode, createTextVNode: _createTextVNode, Teleport: _Teleport, openBlock: _openBlock, createBlock: _createBlock, Fragment: _Fragment } = _Vue

    return (_openBlock(), _createBlock(_Fragment, null, [
      _hoisted_1,
      (_openBlock(), _createBlock(_Teleport, { to: "#my-heart" }, [
        _hoisted_2
      ]))
  ], 64))
}

由于, teleport 组件并不属于静态节点需要提升的范围,所以它会在 render 函数内部创建,即这一部分:

_createBlock(_Teleport, { to: "#my-heart" }, [
  _hoisted_2
]))

需要注意的是,此时 teleport 的内容 honey 是属于静态节点,所以它会被提升。

并且,这里有一处细节, teleport 组件的内部元素永远是 以数组的形式 处理,这在之后的 patch 处理中也会提及。

runtime 运行时的 patch 处理

相比较 compile 编译时生成 teleport 组件的可执行代码, runtime 运行时的 patch 处理可以说是整个 teleport 组件 实现的核心

在上一篇文章 深度解读 Vue 3 源码 | compile 和 runtime 结合的 patch 过程 中,我们说了 patch 会根据不同的 shapeFlag 处理不同的逻辑,而 teleport 则会命中 shapeFlagTELEPORT 的逻辑:

function patch(...) {
  ...
  switch(type) {
    ...
    default:
      if (shapeFlag & ShapeFlags.TELEPORT) {
        ;(type as typeof TeleportImpl).process(
          n1 as TeleportVNode,
          n2 as TeleportVNode,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized,
          internals
        )
      }
  }
}

这里会调用 TeleportImpl 上的 process 方法来实现 teleportpatch 过程,并且它也是 teleport 组件实现的 核心代码 。而 TeleportImpl.process 函数的逻辑可以分为这四个步骤:

创建并挂载注释节点

首先,创建两个注释 VNode ,插入此时 teleport 组件在页面中的对应位置,即插入到 teleport 的父节点 container 中:

// 创建注释节点
const placeholder = (n2.el = __DEV__
        ? createComment('teleport start')
        : createText(''))
const mainAnchor = (n2.anchor = __DEV__
  ? createComment('teleport end')
  : createText(''))
// 插入注释节点
insert(placeholder, container, anchor)
insert(mainAnchor, container, anchor)

挂载 target 节点和占位节点

其次,判断 teleport 组件对应 targetDOM 节点是否存在,存在则插入一个 空的文本节点 ,也可以称为 占位节点

const target = (n2.target = resolveTarget(n2.props, querySelector))
const targetAnchor = (n2.targetAnchor = createText(''))
if (target) {
  insert(targetAnchor, target)
} else if (__DEV__) {
  warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
}

定义挂载函数 mount

然后,定义 mount 方法来为 teleport 组件进行特定的挂载操作,它的本质是基于 mountChildren 挂载子元素方法的封装:

const mount = (container: RendererElement, anchor: RendererNode) => {
  if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
    mountChildren(
      children as VNodeArrayChildren,
      container,
      anchor,
      parentComponent,
      parentSuspense,
      isSVG,
      optimized
    )
  }
}

可以看到,这里也对是否 ShpeFlagsARRAY_CHILDREN即数组 ,进行了判断,因为 teleport子元素必须为数组 。并且, mount 方法的两个形参的意义分别是:

  • container 代表要挂载的父节点。
  • anchor 调用 insertBefore 插入时的 referenceNode ,即占位 VNode

根据 disabled 处理不同逻辑

由于, teleport 组件提供了一个 props 属性 disabled 来控制是否将内容显示在目标 target 中。所以,最后会根据 disabled 来进行不同逻辑的处理:

  • disabledtrue 时, mainAnchor 作为 referenceNode ,即 注释节点 ,挂载到此时 teleport 的父级节点中。
  • disabledfalse 时, targetAnchor 作为 refereneceNode ,即 target 中的空文本节点,挂载到此时 teleporttarget 节点中。
if (disabled) {
  mount(container, mainAnchor)
} else if (target) {
  mount(target, targetAnchor)
}

mount 方法最终会调用原始的 DOM API insertBefore 来实现 teleport 内容的挂载。我们来回忆一下 insertBefore 的语法:

var insertedNode = parentNode.insertBefore(newNode, referenceNode);

由于 insertBefore 的第二个参数 referenceNode 是必选的, 如果不提供节点或者传入无效值,在不同的浏览器中会有不同的表现(摘自 MDN) 。所以,当 disabledfalse 时,我们的 referenceNode 就是一个已插入 target 中的 空文本节点 ,从而确保在不同浏览器上都能 表现一致

小结

今天介绍的是属于 teleport 组件创建的逻辑。同样地, teleport 组件也有自己特殊的 patch 逻辑,这里有兴趣的同学可以自行去了解。虽说, teleport 组件的实现并不复杂,但是,其中的 细节处理仍然是值得学习一番 ,例如注释节点来标记 teleport 组件位置、空文本节点作为占位节点确保 insertBefore 在不同浏览器上表现一致等。

写在最后

相比较前两篇讲解「Vue3」源码的文章来说,这篇应该算是 通俗易懂 。在写完前两篇后,我自己也思考了一段时间,如何降低文章的阅读门槛?我想应该会在后期重新翻新它们,因为源码的本质是复杂的,要想低门槛地实现阅读,这需要一定时间和抽象表达。最后,如果文章中存在不足的地方,欢迎各位同学提 Issue。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK