乐趣区

关于vue3:深度解读-Vue3-源码-内置组件-teleport-是什么来头

前言

上一篇文章,咱们讲了「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 组件中的内容,到底是如何 走进了我的心 ?这,说来话长,长话短说, 咱们间接上图

通过流程图,咱们能够晓得整体 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。

往期文章回顾

深度解读 Vue 3 源码 | compile 和 runtime 联合的 patch 过程

深度解读 Vue 3 源码 | 从编译过程,了解动态节点晋升

❤️ 爱心三连击

通过浏览,如果你感觉有播种的话,能够爱心三连击!!!

前端问路人 —— 五柳 ( 微信公众号:Code center)

退出移动版