State preservation technology of Iframe in Vue | JD Cloud technical team

introduction

Iframe is an HTML element with a long history. According to the official introduction of MDN WEB DOCS, Iframe is defined as an HTML inline frame element, which means a nested Browsing Context, which can embed another HTML page into the current page. Iframe can achieve cross-application-level page sharing at low cost, and has the advantages of simple use, high compatibility, and content isolation. Therefore, with Iframe as the core, it forms the first generation technology in the field of front-end platform architecture.

As we all know, when the Iframe is initially rendered in the DOM, the resource link Url it points to will be automatically loaded and the internal state will be reset. In a typical platform application, the main page of a parent application needs to mount multiple windows (each window corresponds to an Iframe), so how to realize the state in each window (including input state, anchor point information) when switching windows etc.) is not lost, that is, "state preservation"?

If the parent-child application communication is used to record the window state, the transformation cost is very huge. The answer is to use the CSS Display feature of Iframe. When switching windows, the inactive window does not disappear, only the Display state changes to none, and the Display state of the active window changes to non-none. The Iframe does not reload when the Display state is switched. In a Vue application, a single v-show command can fulfill this requirement for us.

Competitive mechanism

There is a performance defect in the above state maintenance model, that is, the main page of the parent application actually needs to place multiple Iframe windows in advance. Even these invisible windows will issue resource requests. A large number of concurrent requests will lead to a decrease in page performance. (It is worth mentioning that the latest version of Chrome already supports the scrolling lazy loading strategy of Iframe, but in this scenario, it cannot improve the problem of concurrent requests.) Therefore, we need to introduce resource pools and competition mechanisms to manage multiple Iframes .

Introduce an Iframe resource pool with a capacity of N to manage multiple open windows. When the resource pool is not full, newly activated windows can be directly inserted into the resource pool; when the resource pool is full, the resource pool will eliminate several pools according to the competition strategy The window in the resource pool is discarded and the newly activated window is inserted into the resource pool. By adjusting the capacity N, you can limit the number of open windows on the main page of the parent application, thereby limiting the number of concurrent requests and achieving the purpose of resource management and control.

Exploration of the principle of Vue Patch

A few days ago, I encountered an Iframe state maintenance problem based on a Vue application. Under the above model, the resource pool not only saves the window object, but also records the click activation time of each window. The resource pool uses the following competitive elimination strategy: the window activation time is sorted in sequence, and the window whose activation time is earlier in the sequence is prioritized to be eliminated. When the resource pool is full, there will be occasional problems that the window state in the pool cannot be maintained.

In Vue, a component is a reusable Vue instance, and Vue will render elements as efficiently as possible, usually reusing existing elements instead of rendering from scratch. Whether the component state is maintained correctly depends on the key attribute key. Based on this, first check the key attribute of the Iframe component. In fact, the Iframe component has been correctly assigned a unique Uid, and this situation can be ruled out.

Since it is not a matter of component reuse, how does the Diff Patch mechanism inside Vue work? Let's take a look at the source code of Vue 2.0:

/**
 * 页面首次渲染和后续更新的入口位置,也是 patch 的入口位置 
 */
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  if (!prevVnode) {
    // 老 VNode 不存在,表示首次渲染,即初始化页面时走这里
    ……
  } else {
    // 响应式数据更新时,即更新页面时走这里
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
}

(1) Under the update life cycle, vm.__patch__the method is mainly executed.

/** 
* 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__Inside the method, trigger patchVnodethe method.

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) patchVnodeInside the method, trigger updateChildrenthe method.

/**
 * 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) We finally come to the protagonist updateChildren. In updateChildrenthe internal implementation, two sets of pointers are used to point to the head and tail of the old and new Vnodes respectively, and the recursion is gathered in the middle to realize the comparison and refresh of the old and new data.

Under the aforementioned resource pool model, when the old and new Iframe components are found, the following logic will be executed:

if (sameVnode(vnodeToMove, newStartVnode)) {
          // 如果这两个节点是同一个,则执行 patch
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
          // patch 结束后将该老节点置为 undefined
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
}

It appears that the culprit of the problem is the execution nodeOps.insertBefore. In the running environment of WEB, the insertBefore API of DOM is actually executed. So let's take a step to see what kind of refresh strategy Iframe adopts in the DOM environment.

Iframe state refresh mechanism

In order to see the changes of DOM nodes more clearly, we can introduce MutationObserver to observe the DOM root node in the latest version of Chrome.
First set up two child nodes under the container node: <span/>and <iframe/>, respectively execute the following schemes and record the results:
Comparison scheme A: use insertBefore to insert a new span node before the iframe node
Comparison scheme B: use insertBefore to insert a new span node after the iframe node New span node
Comparison scheme C: use insertBefore to exchange span and iframe nodes
Comparison scheme D: use insertBefore to operate iframe itself in place
The results are as follows:

Program name Whether the Iframe is refreshed DOM node changes
A no Add a new child node span
B no Add a new child node span
C yes First remove an iframe, then insert an iframe
D yes First remove an iframe, then insert an iframe

The experimental results show that when insertBefore is executed on the Iframe, the DOM will actually perform the operations of removing and adding nodes in sequence, resulting in a refresh of the Iframe state.

A similar problem was mentioned in Vuejs Issues #9473. One solution is to perform DOM operations on non-Iframe type elements first in Vue Patch, but this optimization strategy has not been adopted yet, and this problem still exists in Vue 3.0 .

So under the resource pool model, how can I ensure that Iframe does not execute insertBefore? Returning to the Vue Patch mechanism, we found that only when the relative positions of the old and new Iframes in the old and new Vnode lists remain unchanged, only the patchVnode method will be executed, and the insertBefore method will not be triggered.

Therefore, the final solution adopted is to change the elimination mechanism and change the sorting operation to a search operation, so as to ensure that the status of multiple windows in Vue is maintained.

Author: Jingdong Retail Chen Zhen

Content source: JD Cloud developer community

{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/u/4090830/blog/9008159