Iframe在Vue中的狀態保持技術

来源:https://www.cnblogs.com/Jcloud/archive/2023/05/25/17430626.html
-Advertisement-
Play Games

Iframe是一個歷史悠久的HTML元素,根據MDN WEB DOCS官方介紹,Iframe定義為HTML內聯框架元素,表示嵌套的Browsing Context,它能夠將另一個HTML頁面嵌入到當前頁面中。Iframe可以廉價實現跨應用級的頁面共用,並且具有使用簡單、高相容性、內容隔離等優點,因此... ...


引言

Iframe是一個歷史悠久的HTML元素,根據MDN WEB DOCS官方介紹,Iframe定義為HTML內聯框架元素,表示嵌套的Browsing Context,它能夠將另一個HTML頁面嵌入到當前頁面中。Iframe可以廉價實現跨應用級的頁面共用,並且具有使用簡單、高相容性、內容隔離等優點,因此以Iframe為核心形成了前端平臺架構領域第1代技術。

眾所周知,當Iframe在DOM中初始渲染時,會自動載入其指向的資源鏈接Url,並重置內部的狀態。在一個典型的平臺應用中,一個父應用主頁面要掛載多個視窗(每一個視窗對應一個Iframe),那麼如何在切換視窗時,實現每一個視窗中的狀態(包括輸入狀態、錨點信息等)不丟失,也即“狀態保持”呢?

如果採用父子應用通信來記錄視窗狀態,那麼改造成本是非常巨大的。答案是利用Iframe的CSS Display特性,切換視窗時,非激活狀態的視窗並不消失,僅是Display狀態變更為none,激活狀態視窗的Display狀態變更為非none。在Display狀態切換時,Iframe不會重新載入。在Vue應用中,一行v-show指令即可替我們實現這一需求。

競爭機制

上述的狀態保持模型存在一個性能缺陷,即父應用主頁面實際上要提前擺放多個Iframe視窗。即使是這些不可見的視窗,也會發出資源request請求。大量的併發請求,會導致頁面性能下降。(值得一提的是,Chrome最新版本已經支持了Iframe的滾動懶載入策略,但是在此場景下,並不能改善併發請求的問題。)因此,我們需要引入資源池和競爭機制來管理多個Iframe。

引入一個容量為N的Iframe資源池來管理多開視窗,當資源池未滿時,新激活的視窗可以直接插入至資源池中;當資源池已滿時,資源池按照競爭策略,淘汰若幹池中的視窗並丟棄,然後插入新激活的視窗至資源池中。通過調整容量N,可以限制父應用主頁面上多開視窗的數量,從而限制併發請求數量,實現資源管控的目的。

Vue Patch原理探索

日前遇到了一個基於Vue應用的Iframe狀態保持問題,在上述模型下,資源池不僅保存視窗對象,而且記錄了每個視窗的點擊激活時間。資源池使用以下競爭淘汰策略:對視窗激活時間進行先後次序排序,激活時間排序次序較前的視窗優先被淘汰。當資源池滿時,會偶發池中視窗狀態不能保持的問題。

在Vue中,組件是一個可復用的Vue實例,Vue 會儘可能高效地渲染元素,通常會復用已有元素而不是從頭開始渲染。組件狀態是否正確保持,依賴關鍵屬性key。基於此,首先排查了Iframe組件的key屬性。事實上,Iframe組件已經正確分配了唯一的Uid,此種情況可以排除。

既然不是組件復用的問題,那麼在Vue內部的Diff Patch機制到底是如何運行的呢?讓我們看一下Vue 2.0的源代碼:

/**
 * 頁面首次渲染和後續更新的入口位置,也是 patch 的入口位置 
 */
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  if (!prevVnode) {
    // 老 VNode 不存在,表示首次渲染,即初始化頁面時走這裡
    ……
  } else {
    // 響應式數據更新時,即更新頁面時走這裡
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}

(1)在update生命周期下,主要執行了vm.__patch__方法。

/** 
* vm.__patch__ 
* 1、新節點不存在,老節點存在,調用 destroy,銷毀老節點 
* 2、如果 oldVnode 是真實元素,則表示首次渲染,創建新節點,並插入 body,然後移除老節點 
* 3、如果 oldVnode 不是真實元素,則表示更新階段,執行 patchVnode 
*/
function patch(oldVnode, vnode, hydrating, removeOnly) {
  …… // 1、新節點不存在,老節點存在,調用 destroy,銷毀老節點
  if (isUndef(oldVnode)) {
    …… // 2、老節點不存在,執行創建新節點
  } else {
    // 判斷 oldVnode 是否為真實元素
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // 3、不是真實元素,但是老節點和新節點是同一個節點,則是更新階段,執行 patch 更新節點
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      ……// 是真實元素,則表示初次渲染
    }
  }
  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}

(2)在__patch__方法內部,觸發patchVnode方法。

function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  ……
  if (isUndef(vnode.text)) {// 新節點不為文本節點
    if (isDef(oldCh) && isDef(ch)) {// 新舊節點的子節點都存在,執行diff遞歸
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else {
      ……
    }
  } else {
    ……
  }
}

(3)在patchVnode方法內部,觸發updateChildren方法。

/**
 * diff 過程:
 *   diff 優化:做了四種假設,假設新老節點開頭結尾有相同節點的情況,一旦命中假設,就避免了一次迴圈,以提高執行效率
 *   如果不幸沒有命中假設,則執行遍歷,從老節點中找到新開始節點
 *   找到相同節點,則執行 patchVnode,然後將老節點移動到正確的位置
 *   如果老節點先於新節點遍歷結束,則剩餘的新節點執行新增節點操作
 *   如果新節點先於老節點遍歷結束,則剩餘的老節點執行刪除操作,移除這些老節點
 */
function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  // 老節點的開始索引
  let oldStartIdx = 0
  // 新節點的開始索引
  let newStartIdx = 0
  // 老節點的結束索引
  let oldEndIdx = oldCh.length - 1
  // 第一個老節點
  let oldStartVnode = oldCh[0]
  // 最後一個老節點
  let oldEndVnode = oldCh[oldEndIdx]
  // 新節點的結束索引
  let newEndIdx = newCh.length - 1
  // 第一個新節點
  let newStartVnode = newCh[0]
  // 最後一個新節點
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // 遍歷新老兩組節點,只要有一組遍歷完(開始索引超過結束索引)則跳出迴圈
  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      // 如果節點被移動,在當前索引上可能不存在,檢測這種情況,如果節點不存在則調整索引
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      // 老開始節點和新開始節點是同一個節點,執行 patch
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
      // patch 結束後老開始和新開始的索引分別加 1
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      // 老結束和新結束是同一個節點,執行 patch
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
      // patch 結束後老結束和新結束的索引分別減 1
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      // 老開始和新結束是同一個節點,執行 patch
      ……
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      // 老結束和新開始是同一個節點,執行 patch
      ……
    } else {
      // 如果上面的四種假設都不成立,則通過遍歷找到新開始節點在老節點中的位置索引
      ……
        // 在老節點中找到新開始節點了
        if (sameVnode(vnodeToMove, newStartVnode)) {
          // 如果這兩個節點是同一個,則執行 patch
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          // patch 結束後將該老節點置為 undefined
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // 最後這種情況是,找到節點了,但是發現兩個節點不是同一個節點,則視為新元素,執行創建
          ……
        }
      // 老節點向後移動一個
      newStartVnode = newCh[++newStartIdx]
    }
  }
  // 走到這裡,說明老姐節點或者新節點被遍歷完了,執行剩餘節點的處理
  ……
}

(4)咱們終於來到了主角updateChildren。在updateChildren內部實現中,使用了2套指針分別指向新舊Vnode頭尾,並向中間聚攏遞歸,以實現新舊數據對比刷新。

在前述資源池模型下,當查找到新舊Iframe組件時,會執行如下邏輯:

if (sameVnode(vnodeToMove, newStartVnode)) {
          // 如果這兩個節點是同一個,則執行 patch
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          // patch 結束後將該老節點置為 undefined
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
}

看來出現問題的罪魁禍首是執行了nodeOps.insertBefore。在WEB的運行環境下實際上執行的是DOM的insertBefore API。那麼我們移步來看看在DOM環境下,Iframe究竟是採取了何種刷新策略。

Iframe的狀態刷新機制

為了更清晰地看到DOM節點的變化情況,我們可以引入MutationObserver在最新版Chrome中來觀測DOM根節點。
首先設置容器節點下有兩個子節點:<span/><iframe/>,分別執行以下方案並記錄結果:
對比方案A:使用insertBefore在iframe節點前再插入一個新的span節點
對比方案B:使用insertBefore在iframe節點後再插入一個新的span節點
對比方案C:使用insertBefore交換span和iframe節點
對比方案D:使用insertBefore原地操作iframe自身
其結果如下:

方案名稱 Iframe是否刷新 DOM節點變化
A 新增一個子節點span
B 新增一個子節點span
C 先移除一個iframe,再插入一個iframe
D 先移除一個iframe,再插入一個iframe

實驗結果顯示,對Iframe執行insertBefore時,實際上DOM會依次執行移除、新增節點操作,導致Iframe狀態刷新。

在Vuejs Issues #9473中提到了類似的問題,一種解決方案是在Vue Patch時優先對非Iframe類型元素進行DOM操作,但是目前這個優化策略尚未被採用,在Vue 3.0版本中也依然存在這個問題。

那麼在資源池模型下,如何才能保證Iframe不執行insertBefore呢?重新回到Vue Patch機制下,我們發現,只有新舊Iframe在新舊Vnode列表中的相對位置保持不變時,才會只執行patchVnode方法,而不會觸發insertBefore方法。

因此,採取的最終解決方案是,更改淘汰機制,將排序操作改為搜索操作,保證了多開視窗在Vue中的狀態保持。

作者:京東零售 陳震

內容來源:京東雲開發者社區


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 雖然已經有《7千多兒童故事網ACCESS\EXCEL資料庫》這種記錄數的童話故事類數據,但是遇到了好採集的就總想採集下來,後續有時間或有需求可以再做合併等操作。 分類情況統計為: 兒童故事:兒童小故事(1895)、睡前故事(1229)、益智故事(233)、哲理故事(177)。民間故事:世界上下五千年 ...
  • 原本我以為《3萬5千英語句子英語例句大全ACCESS資料庫》例句已經夠多了,沒想到今天遇到一個10萬條英語單詞例句的數據,非常適合與單詞詞典進行關聯學習,例句多了單詞的用法以及句子的掌握都更有效率,例句多了單詞的用法以及句子的掌握都更有效率,例句多了單詞的用法以及句子的掌握都更有效率,例句多了單詞的 ...
  • CSS中,*的作用是通配表示“全部”。遺憾的是,並沒有一種通配元素名的方法。 例如,我有好幾個東西class都標記為了my-element-序號,就像這樣: ```html ... ``` 我現在希望讓所有這些class的東西都應用同一個css規則。可惜,css並不支持這麼一種寫法: ```css ...
  • 近日在編寫一個小程式,將日記功能移植到小程式中,雖然在手機端寫日記一般用不到Markdown,但是仍想在小程式中查看在電腦端寫的Markdown格式的內容,如代碼塊等。 經過查詢,找到一個被廣泛使用的解決方案是towxml 現記錄如下: > 首先將代碼克隆下來 ```js git clone htt ...
  • > 效果預覽 ![調節頁面.gif](https://wansherry.com/api/fc01e2c58219126e20367e856ebad24c.gif) > 關鍵代碼 ```javascript //調節視窗大小 useEffect(() => { if (conref.current) ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 在業務中,有這麼一種場景,表格下的某一列 ID 值,文本超長了,正常而言會是這樣: 通常,這種情況都需要超長省略溢出打點,那麼,就會變成這樣: 但是,這種展示有個缺點,3 個 ID 看上去就完全一致了,因此,PM 希望能夠實現頭部省略打點 ...
  • 隨著項目的發展,前端SPA應用的規模不斷加大、業務代碼耦合、編譯慢,導致日常的維護難度日益增加。同時前端技術的發展迅猛,導致功能擴展吃力,重構成本高,穩定性低。因此前端微服務應運而生。 ...
  • 1. 動畫 動畫(animation)是CSS3中具有顛覆性的特征之一,可以通過設置都各節點來精確控制一個或一組動畫,常用來實現複雜的動畫效果。 相比較過度,動畫可以實現更多變化,更多控制,連續自動播放等效果。 1.1 動畫的基本使用 製作動畫分為兩步: (1)先定義動畫。 (2)再使用(調用)動畫 ...
一周排行
    -Advertisement-
    Play Games
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...