上一篇文章,咱們講了「Vue3」 runtime
和 compile
結合的 patch
過程。彷佛,因爲文章內容太過晦澀的緣由,並無收到不少同窗的反饋。可是,其實這裏我想說的是源碼就是這樣,初見時如陌生人通常,再見時如初戀,既熟悉又懷念。javascript
因此,這一篇文章,我打算講個「Vue3」中輕鬆愉快的設計點——內置組件 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
負責展現不一樣內容。函數
假設,此時咱們有一個這樣的栗子: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
組件的運行機制。
這裏,咱們仍然會分爲compile
和runtime
兩個階段去介紹。
仍然是咱們上面的那個栗子,它通過 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
處理中也會說起。
相比較 compile
編譯時生成 teleport
組件的可執行代碼,runtime
運行時的 patch
處理能夠說是整個 teleport
組件實現的核心。
在上一篇文章 深度解讀 Vue 3 源碼 | compile 和 runtime 結合的 patch 過程 中,咱們說了 patch
會根據不一樣的 shapeFlag
處理不一樣的邏輯,而 teleport
則會命中 shapeFlag
爲 TELEPORT
的邏輯:
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
方法來實現 teleport
的 patch
過程,而且它也是 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)
其次,判斷 teleport
組件對應 target
的 DOM
節點是否存在,存在則插入一個空的文本節點,也能夠稱爲佔位節點:
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
方法來爲 teleport
組件進行特定的掛載操做,它的本質是基於 mountChildren
掛載子元素方法的封裝:
const mount = (container: RendererElement, anchor: RendererNode) => { if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { mountChildren( children as VNodeArrayChildren, container, anchor, parentComponent, parentSuspense, isSVG, optimized ) } }
能夠看到,這裏也對是否 ShpeFlags
爲 ARRAY_CHILDREN
,即數組,進行了判斷,由於 teleport
的子元素必須爲數組。而且,mount
方法的兩個形參的意義分別是:
container
表明要掛載的父節點。anchor
調用 insertBefore
插入時的 referenceNode
,即佔位 VNode
。因爲,teleport
組件提供了一個 props
屬性 disabled
來控制是否將內容顯示在目標 target
中。因此,最後會根據 disabled
來進行不一樣邏輯的處理:
disabled
爲 true
時,mainAnchor
做爲 referenceNode
,即註釋節點,掛載到此時 teleport
的父級節點中。disabled
爲 false
時,targetAnchor
做爲 refereneceNode
,即 target
中的空文本節點,掛載到此時 teleport
的 target
節點中。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)。因此,當 disabled
爲 false
時,咱們的 referenceNode
就是一個已插入 target
中的空文本節點,從而確保在不一樣瀏覽器上都能表現一致。
今天介紹的是屬於 teleport
組件建立的邏輯。一樣地,teleport
組件也有本身特殊的 patch
邏輯,這裏有興趣的同窗能夠自行去了解。雖然說,teleport
組件的實現並不複雜,可是,其中的細節處理仍然是值得學習一番,例如註釋節點來標記 teleport
組件位置、空文本節點做爲佔位節點確保 insertBefore
在不一樣瀏覽器上表現一致等。
相比較前兩篇講解「Vue3」源碼的文章來講,這篇應該算是通俗易懂。在寫完前兩篇後,我本身也思考了一段時間,如何下降文章的閱讀門檻?我想應該會在後期從新翻新它們,由於源碼的本質是複雜的,要想低門檻地實現閱讀,這須要必定時間和抽象表達。最後,若是文章中存在不足的地方,歡迎各位同窗提 Issue。
深度解讀 Vue 3 源碼 | compile 和 runtime 結合的 patch 過程
深度解讀 Vue 3 源碼 | 從編譯過程,理解靜態節點提高
經過閱讀,若是你以爲有收穫的話,能夠愛心三連擊!!!
前端問路人 —— 五柳( 微信公衆號: Code center)