引言

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中的状态保持。

作者:京东零售 陈震

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

Iframe在Vue中的状态保持技术的更多相关文章

  1. Vue中的状态管理器 - Vuex

    我们知道vue是组件式开发的,当你的项目越来越大后,每个组件背后的数据也会变得越来越难以理顺, 这个时候你就可以考虑使用vuex了. 备注: 官方建议小项目不要使用,引入vuex会带来新的概念和模式, ...

  2. [译]面向初学者的Asp.Net状态管理技术

    介绍 本文主要讲解Asp.Net应用程序中的状态管理技术(Asp.Net中有多种状态管理技术),并批判性地分析所有状态管理技术的优缺点. 背景 HTTP是无状态的协议.客户端发起一个请求,服务器响应完 ...

  3. vue中嵌套页面 iframe 标签

    vue中嵌套iframe,将要嵌套的文件放在static下面: <iframe src="../../../static/bear.html" width="300 ...

  4. vue中嵌套页面(iframe)

    vue中嵌套iframe,将要嵌套的文件放在static下面.(要将打包文件整体放在statici里,我的文件名是canvas) src可以使用相对路径,也可使用服务器根路径http:localhos ...

  5. Vuex内容解析和vue cli项目中使用状态管理模式Vuex

    中文文档:vuex官方中文网站 一.vuex里面都有些什么内容? const store = new Vuex.Store({ state: { name: 'weish', age: }, gett ...

  6. Vue中iframe和组件的通信

    最近的项目开发中用到了Vue组件中嵌套iframe,相应的碰到了组件和HTML的通信问题,场景如下:demo.vue中嵌入 test.html 由于一般的iframe嵌套是用于HTML文件的,在vue ...

  7. vue中嵌套的iframe中控制路由的跳转及传参

    在iframe引入的页面中,通过事件触发的方式进行传递参数,其中data是一个标识符的作用,在main.js中通过data进行判断,params是要传递的参数 //iframe向vue传递跳转路由的参 ...

  8. Vue中使用Switch开关用来控制商品的上架与下架情况、同时根据数据库商品的状态反应到前台、前台修改商品状态保存到数据库

    一般后台对商品的信息管理.包含商品的上架与下架.为了提高用户的体验.将商品上下架的操作做成开关的形式.同时后台数据库中保存的商品状态能够根据开关状态改变. 1.效果展示 这种效果:== 当开关是开启状 ...

  9. Vue中结合Flask与Node.JS的异步加载功能实现文章的分页效果

    你好!欢迎阅读我的博文,你可以跳转到我的个人博客网站,会有更好的排版效果和功能. 此外,本篇博文为本人Pushy原创,如需转载请注明出处:http://blog.pushy.site/posts/15 ...

  10. vue中动态加载组件+开发者模式+JS参数值传递和引用传递

    今天写vue里面通过接口反参动态加载组件时候 跟着同学...学习到了 一.先说说vue 内置组件 component 的用法 component组件可以来专门用来进行组件的切换,使用is来绑定你的组件 ...

随机推荐

  1. el与data的两种写法

    el的两种写法 Vue初始化(被创建)后会判断有无el 有el:创建Vue实例对象的时候配置el属性 无el:通过vm.$mount('#root')指定el的值 data的两种写法 对象式:data ...

  2. 宏任务&微处理

    事件循环 JavaScript 语言的一大特点就是单线程,同一个时间只能做一件事.为了协调事件.用户交互.脚本.UI 渲染和网络处理等行为,防止主线程的不阻塞,Event Loop 的方案应用而生. ...

  3. 使用python爬虫爬取链家潍坊市二手房项目

    使用python爬虫爬取链家潍坊市二手房项目 需求分析 需要将潍坊市各县市区页面所展示的二手房信息按要求爬取下来,同时保存到本地. 流程设计 明确目标网站URL( https://wf.lianjia ...

  4. linux环境下部署mysql环境

    一.部署步骤 1.将安装包上传到Linux服务器上(目录随意),然后解压缩 2.进入到解压后的目录下,分别执行以下命令安装四个包(严格按照顺序执行) rpm -ivh mysql-community- ...

  5. 王树森Transformer学习笔记

    目录 Transformer Attention结构 Self-Attention结构 Multi-head Self-Attention BERT:Bidirectional Encoder Rep ...

  6. Collection单列集合总结

    这篇文章记录了Collection集合,List集合,Set集合 在文章第七点总结了两大系列集合的五种实现类的区别,有需要的小伙伴可以直接去查看 一.什么是集合 集合是Java中存储对象数据的一种容器 ...

  7. vue中 computed和watch的一些简单理解(区别)

    今天看到一个问题,就是vue的computed和watch要在哪些场景下使用,其实也就是在问他们的区别.computed也就是计算属性,它可以帮助我们将在模板中的一些稍微复杂的逻辑计算放回到js代码中 ...

  8. 使用vSphere Update Manager 升级 ESXi 主机

    使用vSphere Update Manager 升级 ESXi 主机 vSphere Update Manager  vSphere Update Manager 是用于升级.迁移.更新和修补群集主 ...

  9. MySQL相关优质文章推荐

    MySQL相关优质文章推荐 文章推荐 文章链接地址 MySQL高性能优化系列 MySQL字符集及校对规则的理解 MySQL InnoDB锁机制全面解析分享 MySQL事务隔离级别和MVCC,MVCC文 ...

  10. 学习httprunner遇到的问题记录

    今天研究httprunner遇到了几个问题 1.问题1: 最新的版本4.3.0已经舍弃了locusts,har2case这两个东西 导致运行的时候,出现har2case不是内部命令 一开始以为是没有配 ...