VueにおけるIframeの状態保存技術 | JD Cloud技術チーム

序章

Iframe は長い歴史を持つ HTML 要素で、MDN WEB DOCS の公式紹介によると、Iframe は HTML インライン フレーム要素として定義されており、これは現在のページに別の HTML ページを埋め込むことができる入れ子になったブラウジング コンテキストを意味します。iframeは、アプリケーションレベルを越えたページ共有を低コストで実現でき、使いやすさ、互換性の高さ、コンテンツの分離などの利点を持ち、iframeを核としてフロントエンド分野における第一世代の技術を形成します。プラットフォームのアーキテクチャ。

ご存知のとおり、Iframe が最初に DOM でレンダリングされるとき、それが指すリソース リンク URL が自動的にロードされ、内部状態がリセットされます。一般的なプラットフォームアプリケーションでは、親アプリケーションのメインページに複数のウィンドウ(各ウィンドウがIframeに対応)をマウントする必要があるため、ウィンドウを切り替える際の各ウィンドウ内の状態(入力状態、アンカーポイント情報を含む)をどのように実現するかなど。 )は失われない、つまり「状態保存」?

親子アプリケーション通信を使用してウィンドウの状態を記録する場合、変換コストは非常に膨大になります。答えは、Iframe の CSS 表示機能を使用することで、ウィンドウを切り替えると、非アクティブなウィンドウは消えず、表示状態が none に変更されるだけで、アクティブなウィンドウの表示状態が非 none に変わります。表示状態が切り替わっても、Iframe はリロードされません。Vue アプリケーションでは、1 つの v-show コマンドでこの要件を満たすことができます。

競争メカニズム

上記の状態維持モデルにはパフォーマンス上の欠陥があります。つまり、親アプリケーションのメイン ページには、実際には事前に複数の Iframe ウィンドウを配置する必要があります。これらの目に見えないウィンドウでもリソース要求が発行されます。同時リクエストが多数あると、ページのパフォーマンスが低下します。(最新バージョンの Chrome はすでに Iframe のスクロール遅延読み込み戦略をサポートしていることに注意してください。ただし、このシナリオでは同時リクエストの問題を改善することはできません。) したがって、複数のリソースを管理するには、リソース プールと競合メカニズムを導入する必要があります。 iframe 。

複数の開いているウィンドウを管理するために、容量 N の Iframe リソース プールを導入します。リソース プールがいっぱいでない場合は、新しくアクティブ化されたウィンドウをリソース プールに直接挿入できます。リソース プールがいっぱいになると、リソース プールはいくつかのプールを削除します。競争戦略に従って、リソース プール内のウィンドウは破棄され、新しくアクティブ化されたウィンドウがリソース プールに挿入されます。容量 N を調整することで、親アプリケーションのメイン ページで開いているウィンドウの数を制限できるため、同時リクエストの数が制限され、リソースの管理と制御の目的が達成されます。

Vue Patch の原理の探求

数日前、Vue アプリケーションに基づく Iframe 状態維持の問題が発生しました。上記のモデルでは、リソース プールはウィンドウ オブジェクトを保存するだけでなく、各ウィンドウのクリック アクティブ化時間も記録します。リソース プールは、次の競合排除戦略を使用します。つまり、ウィンドウのアクティブ化時刻が順番に並べ替えられ、シーケンス内でアクティブ化時刻が早いウィンドウが優先的に削除されます。リソースプールがいっぱいになると、プール内のウィンドウ状態を維持できなくなる問題が発生することがあります。

Vue では、コンポーネントは再利用可能な Vue インスタンスであり、Vue は要素を可能な限り効率的にレンダリングします。通常、最初からレンダリングするのではなく、既存の要素を再利用します。コンポーネントの状態が正しく維持されるかどうかは、キー属性キーに依存します。これを踏まえて、まず 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) 更新ライフサイクルでは、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 と新しい 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.insertBeforeWEBの実行環境では、DOMのinsertBefore APIが実際に実行されます。それでは、DOM 環境で Iframe がどのような更新戦略を採用しているかを見てみましょう。

Iframe 状態更新メカニズム

DOM ノードの変更をより明確に確認するために、最新バージョンの Chrome で DOM ルート ノードを観察する MutationObserver を導入できます。
まず、コンテナ ノードの下に 2 つの子ノードを設定します:<span/><iframe/>、それぞれ次のスキームを実行し、結果を記録します。
比較スキーム A: insertBefore を使用して、iframe ノードの前に新しいスパン ノードを挿入します。
比較スキーム B: insertBefore を使用して、新しいスパンを挿入します。 iframe ノードの後のノード 新しい Span ノード
比較スキーム C: insertBefore を使用して、span ノードと iframe ノードを交換します。
比較スキーム D: insertBefore を使用して、iframe 自体をその場で操作します
。 結果は次のとおりです。

番組名 Iframe が更新されるかどうか DOM ノードの変更
いいえ 新しい子ノード スパンを追加する
B いいえ 新しい子ノード スパンを追加する
C はい まず iframe を削除してから、iframe を挿入します
D はい まず iframe を削除してから、iframe を挿入します

実験結果は、Iframe で insertBefore が実行されると、DOM が実際にノードの削除と追加の操作を順番に実行し、その結果 Iframe 状態が更新されることを示しています。

同様の問題が Vuejs Issues #9473 で言及されています。解決策の 1 つは、Vue Patch で最初に非 Iframe タイプの要素に対して DOM 操作を実行することですが、この最適化戦略はまだ採用されておらず、この問題は Vue 3.0 でも依然として存在しています。

では、リソース プール モデルでは、Iframe が insertBefore を実行しないようにするにはどうすればよいでしょうか? Vue Patch メカニズムに戻ると、新旧の Vnode リスト内の新旧の Iframe の相対位置が変更されていない場合にのみ、patchVnode メソッドのみが実行され、insertBefore メソッドはトリガーされないことがわかりました。

したがって、採用される最終的な解決策は、Vue の複数のウィンドウのステータスが確実に維持されるように、削除メカニズムを変更し、ソート操作を検索操作に変更することです。

著者: Jingdong Retail Chen Zhen

コンテンツソース: JD Cloud 開発者コミュニティ

{{名前}}
{{名前}}

おすすめ

転載: my.oschina.net/u/4090830/blog/9008159