引言

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的源代码:

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

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

  1. /**
  2. * vm.__patch__
  3. * 1、新节点不存在,老节点存在,调用 destroy,销毁老节点
  4. * 2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
  5. * 3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
  6. */
  7. function patch(oldVnode, vnode, hydrating, removeOnly) {
  8. …… // 1、新节点不存在,老节点存在,调用 destroy,销毁老节点
  9. if (isUndef(oldVnode)) {
  10. …… // 2、老节点不存在,执行创建新节点
  11. } else {
  12. // 判断 oldVnode 是否为真实元素
  13. const isRealElement = isDef(oldVnode.nodeType)
  14. if (!isRealElement && sameVnode(oldVnode, vnode)) {
  15. // 3、不是真实元素,但是老节点和新节点是同一个节点,则是更新阶段,执行 patch 更新节点
  16. patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
  17. } else {
  18. ……// 是真实元素,则表示初次渲染
  19. }
  20. }
  21. invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  22. return vnode.elm
  23. }

(2)在__patch__方法内部,触发patchVnode方法。

  1. function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  2. ……
  3. if (isUndef(vnode.text)) {// 新节点不为文本节点
  4. if (isDef(oldCh) && isDef(ch)) {// 新旧节点的子节点都存在,执行diff递归
  5. if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
  6. } else {
  7. ……
  8. }
  9. } else {
  10. ……
  11. }
  12. }

(3)在patchVnode方法内部,触发updateChildren方法。

  1. /**
  2. * diff 过程:
  3. * diff 优化:做了四种假设,假设新老节点开头结尾有相同节点的情况,一旦命中假设,就避免了一次循环,以提高执行效率
  4. * 如果不幸没有命中假设,则执行遍历,从老节点中找到新开始节点
  5. * 找到相同节点,则执行 patchVnode,然后将老节点移动到正确的位置
  6. * 如果老节点先于新节点遍历结束,则剩余的新节点执行新增节点操作
  7. * 如果新节点先于老节点遍历结束,则剩余的老节点执行删除操作,移除这些老节点
  8. */
  9. function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  10. // 老节点的开始索引
  11. let oldStartIdx = 0
  12. // 新节点的开始索引
  13. let newStartIdx = 0
  14. // 老节点的结束索引
  15. let oldEndIdx = oldCh.length - 1
  16. // 第一个老节点
  17. let oldStartVnode = oldCh[0]
  18. // 最后一个老节点
  19. let oldEndVnode = oldCh[oldEndIdx]
  20. // 新节点的结束索引
  21. let newEndIdx = newCh.length - 1
  22. // 第一个新节点
  23. let newStartVnode = newCh[0]
  24. // 最后一个新节点
  25. let newEndVnode = newCh[newEndIdx]
  26. let oldKeyToIdx, idxInOld, vnodeToMove, refElm
  27. // 遍历新老两组节点,只要有一组遍历完(开始索引超过结束索引)则跳出循环
  28. while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
  29. if (isUndef(oldStartVnode)) {
  30. // 如果节点被移动,在当前索引上可能不存在,检测这种情况,如果节点不存在则调整索引
  31. oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
  32. } else if (isUndef(oldEndVnode)) {
  33. oldEndVnode = oldCh[--oldEndIdx]
  34. } else if (sameVnode(oldStartVnode, newStartVnode)) {
  35. // 老开始节点和新开始节点是同一个节点,执行 patch
  36. patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  37. // patch 结束后老开始和新开始的索引分别加 1
  38. oldStartVnode = oldCh[++oldStartIdx]
  39. newStartVnode = newCh[++newStartIdx]
  40. } else if (sameVnode(oldEndVnode, newEndVnode)) {
  41. // 老结束和新结束是同一个节点,执行 patch
  42. patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
  43. // patch 结束后老结束和新结束的索引分别减 1
  44. oldEndVnode = oldCh[--oldEndIdx]
  45. newEndVnode = newCh[--newEndIdx]
  46. } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
  47. // 老开始和新结束是同一个节点,执行 patch
  48. ……
  49. } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
  50. // 老结束和新开始是同一个节点,执行 patch
  51. ……
  52. } else {
  53. // 如果上面的四种假设都不成立,则通过遍历找到新开始节点在老节点中的位置索引
  54. ……
  55. // 在老节点中找到新开始节点了
  56. if (sameVnode(vnodeToMove, newStartVnode)) {
  57. // 如果这两个节点是同一个,则执行 patch
  58. patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
  59. // patch 结束后将该老节点置为 undefined
  60. oldCh[idxInOld] = undefined
  61. canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
  62. } else {
  63. // 最后这种情况是,找到节点了,但是发现两个节点不是同一个节点,则视为新元素,执行创建
  64. ……
  65. }
  66. // 老节点向后移动一个
  67. newStartVnode = newCh[++newStartIdx]
  68. }
  69. }
  70. // 走到这里,说明老姐节点或者新节点被遍历完了,执行剩余节点的处理
  71. ……
  72. }

(4)咱们终于来到了主角updateChildren。在updateChildren内部实现中,使用了2套指针分别指向新旧Vnode头尾,并向中间聚拢递归,以实现新旧数据对比刷新。

在前述资源池模型下,当查找到新旧Iframe组件时,会执行如下逻辑:

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

看来出现问题的罪魁祸首是执行了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. ROS话题通信C++(附launch启动方式)

    ROS话题通信C++(附launch启动方式) 创建工作空间 mkdir -p topic_ws/src cd topic_ws catkin_make 设置环境变量 source ./devel/s ...

  2. C++/Qt网络通讯模块设计与实现(三)

    上一节给大家从源码级别分析了SNetClient网络客户端的实现过程,详见​C++/Qt网络通讯模块设计与实现(二),并给大家留了一个疑问,即引入SNetClientRunning类是为了解决什么问题 ...

  3. react商品详情页、购物车逻辑、首页上拉加载、下滑刷新

    1.回顾 2.点击列表进入产品的详情页面 设计向页面的布局结构,设计详情页面,入口导入布局文件 // index.js 入口文件 import Detail from '@/Detail'; < ...

  4. 保持唯一性,请停止使用【python3 内置hash() 函数】

    问题: 如图,用hash() 筛重时竟然出现了重复. 如下图: hash字符串时,同一窗口的是一致的,不同窗口结果竟然不同. 原因: python的字符串hash算法并不是直接遍历字符串每个字符去计算 ...

  5. DevOps|研发效能价值如何衡量

    现在很多公司都在做或者计划做研发效能,也知道研发效能工作很重要,能提高产研运同学的协同效率,提高员工的工作效率和质量,提高业务交付效率和交付质量,但是价值有多大?效率又有多高呢?因为不容易说清楚,所以 ...

  6. WPF 界面布局、常用控件入门教程实例 WPF入门学习控件快速教程例子 WPF上位机、工控串口通信经典入门

    WPF(Windows Presentation Foundation)是一种用于创建 Windows 桌面应用程序的框架,它提供了丰富的控件库和灵活的界面布局,可以创建现代化的用户界面.下面是 WP ...

  7. 长达 1.7 万字的 explain 关键字指南!

    当你的数据里只有几千几万,那么 SQL 优化并不会发挥太大价值,但当你的数据里去到了几百上千万,SQL 优化的价值就体现出来了!因此稍微有些经验的同学都知道,怎么让 MySQL 查询语句又快又好是一件 ...

  8. Redis系列12:Redis 的事务机制

    Redis系列1:深刻理解高性能Redis的本质 Redis系列2:数据持久化提高可用性 Redis系列3:高可用之主从架构 Redis系列4:高可用之Sentinel(哨兵模式) Redis系列5: ...

  9. 解决flex布局中justify-content设置成space-between后因数据问题导致最后一行布局错乱

    在常用的flex布局中,当页面展示商品时,因为数据的不确定,导致justify-content设置成space-between,最后一行布局错乱 1 <!DOCTYPE html> 2 & ...

  10. java中的装箱 拆箱 以及 字符串与基本数据类型的转化

    java中的装箱 拆箱 装箱就是 自动将基本数据类型转换为包装器类型:拆箱就是 自动将包装器类型转换为基本数据类型 ; Integer i =5;//装箱 int j=i;//拆箱 在装箱的时候自动调 ...