乐趣区

关于iframe:Iframe在Vue中的状态保持技术-京东云技术团队

引言

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 中的状态放弃。

作者:京东批发 陈震

内容起源:京东云开发者社区

退出移动版