深度解讀 Vue3 源碼 | 內置組件 teleport 是什麼「來頭」?

前言

image

上一篇文章,咱們講了「Vue3」 runtimecompile 結合的 patch 過程。彷佛,因爲文章內容太過晦澀的緣由,並無收到不少同窗的反饋。可是,其實這裏我想說的是源碼就是這樣,初見時如陌生人通常,再見時如初戀,既熟悉又懷念javascript

因此,這一篇文章,我打算講個「Vue3」中輕鬆愉快的設計點——內置組件 teleport。那麼,此次咱們將會從使用角度和源碼角度去深刻了解 teleport 組件是如何實現的?前端

什麼是 teleport 組件

固然,若是已經懂得怎麼使用 teleport 組件的同窗能夠跳過這個小節。

咱們從使用性的角度思考,很現實的一點,就是 teleport 組件能帶給咱們什麼價值java

最經典的回答就是開發中使用 Modal 模態框的場景。一般,咱們會在中後臺的業務開發中頻繁地使用到模態框。可能對於中臺還好,它們會搞一些 low code減小開發成本,但這也是通常大公司或者技術較強的公司才能實現的。segmentfault

而實際狀況下,咱們傳統的後臺開發,就是會存在頻繁地手動使用 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 組件

假設,此時咱們有一個這樣的栗子:post

<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)
相關文章
相關標籤/搜索