介绍iframe是一个历史悠久的HTML元素。根据MDNWEBDOCS的官方介绍,Iframe被定义为一个HTML内联框架元素,代表一个嵌套的BrowsingContext,可以将另一个HTML页面嵌入到当前页面中。iframe可以低成本实现跨应用级的页面共享,具有使用简单、兼容性高、内容隔离等优点。因此,以iframe为核心,形成了前端平台架构领域的第一代技术。众所周知,当Iframe最初在DOM中渲染时,它指向的资源链接Url会被自动加载,内部状态会被重置。在一个典型的平台应用中,一个父应用的主页面需要挂载多个窗口(每个窗口对应一个iframe),那么切换窗口时如何实现每个窗口中的状态(包括输入状态、锚点信息)等。)不丢失,即“状态保存”?如果使用父子应用程序通信来记录窗口状态,改造成本是非常巨大的。答案是使用Iframe的CSS显示功能。切换窗口时,非活动窗口不会消失,只是Display状态变为none,活动窗口的Display状态变为非none。切换显示状态时,Iframe不会重新加载。在Vue应用中,一行v-show指令就可以帮我们完成这个需求。竞争机制上面提到的状态维护模型存在一个性能缺陷,即父应用的主页面实际上需要提前放置多个Iframe窗口。甚至这些看不见的窗口也会发出资源请求。大量并发请求会导致页面性能下降。(值得一提的是,最新版本的Chrome已经支持iframe的滚动懒加载策略,但是在这种场景下,并不能改善并发请求的问题。)因此,我们需要引入资源池和竞争机制来管理多个内嵌框架。引入一个容量为N的iframe资源池来管理多个打开的窗口。当资源池未满时,新激活的窗口可以直接插入到资源池中;当资源池满时,资源池会根据竞争策略淘汰若干个池中的窗口,将资源池中的窗口丢弃,将新激活的窗口插入到资源池中。通过调整容量N,可以限制父应用主页面的打开窗口数,从而限制并发请求数,达到资源管控的目的。探索VuePatch的原理最近遇到一个基于Vue应用的iframe状态维护问题。在上述模型下,资源池中不仅保存了窗口对象,还记录了每个窗口的点击激活时间。资源池采用如下竞争淘汰策略:窗口激活时间按顺序排序,激活时间靠前的窗口优先淘汰。当资源池满时,偶尔会出现池中窗口状态无法维护的问题。在Vue中,组件是一个可重用的Vue实例,Vue会尽可能高效地渲染元素,通常是重用现有元素而不是从头开始渲染。组件状态是否正确维护取决于键属性key。基于此,首先查看iframe组件的key属性。其实Iframe组件已经正确分配了唯一的Uid,可以排除这种情况。既然不是组件复用的问题,那么Vue内部的DiffPatch机制是如何工作的呢?我们看一下Vue2.0的源码:/***页面第一次渲染和后续更新的入口位置,也是patch*/Vue.prototype._update=function(vnode:VNode,hydrating?:boolean){if(!prevVnode){//旧的VNode不存在,表示第一次渲染,也就是初始化页面的时候去这里...}else{//当响应数据为updated,也就是更新页面的时候去这里vm.$el=vm.__patch__(prevVnode,vnode)}}(1)update生命周期下,主要执行vm.__patch__方法。/***vm.__patch__*1.新节点不存在,旧节点存在,调用destroy,销毁旧节点*2.如果oldVnode是真实元素,表示第一次渲染,创建新节点,插入body,然后removeOldnode*3.如果oldVnode不是真正的元素,则表示更新阶段,执行patchVnode*/functionpatch(oldVnode,vnode,hydrating,removeOnly){...//1.new节点不存在,旧节点存在,调用destroy,销毁旧节点if(isUndef(oldVnode)){...//2.旧节点不存在,执行创建新节点}else{//判断oldVnode是否是真实元素constisRealElement=isDef(oldVnode.nodeType)if(!isRealElement&&sameVnode(oldVnode,vnode)){//3.不是真实元素,但是旧节点和新节点是同一个节点,是更新阶段,执行patch更新节点patchVnode(oldVnode,vnode,insertedVnodeQueue,null,null,removeOnly)}else{...//是一个真正的元素,表示第一次渲染}}invokeInsertHook(vnode,insertedVnodeQueue,isInitialPatch)returnvnode.elm}(2)在__patch__方法内部,触发patchVnode方法。functionpatchVnode(oldVnode,vnode,insertedVnodeQueue,removeOnly){...if(isUndef(vnode.text)){//新节点不是文本节点if(isDef(oldCh)&&isDef(ch)){//新老节点所有子节点都存在,执行diff递归if(oldCh!==ch)updateChildren(elm,oldCh,ch,insertedVnodeQueue,removeOnly)}else{...}}}(3)patchVnode方法内部,触发updateChildren方法。/***diff流程:*diff优化:做了四个假设,假设新老节点的首尾节点相同,一旦满足假设,避免循环,提高执行效率*如果不幸假设没有命中,执行遍历,从旧节点找到新的起始节点*找到相同的节点,然后执行patchVnode,然后将旧节点移动到正确的位置*如果旧节点先于新节点遍历,剩余的新节点会执行新节点操作*如果新节点先于旧节点遍历,则删除剩余的旧节点,移除这些旧节点*/functionupdateChildren(parentElm,oldCh,newCh,insertedVnodeQueue,removeOnly){//旧节点的起始索引letoldStartIdx=0//新节点的起始索引letnewStartIdx=0//旧节点的结束索引letoldEndIdx=oldCh.length-1//第一个旧节点letoldStartVnode=oldCh[0]//lastoldnodeletoldEndVnode=oldCh[oldEndIdx]//新节点的结束索引letnewEndIdx=newCh.length-1//第一个新节点letnewStartVnode=newCh[0]//最后一个新节点letnewEndVnode=newCh[newEndIdx]letoldKeyToIdx,idxInOld,vnodeToMove,refElm//遍历新旧节点组,只要遍历一组(开始索引超过结束索引),就会跳出循环while(oldStartIdx<=oldEndIdx&&newStartIdx<=newEndIdx){if(isUndef(oldStartVnode)){//如果节点被移动,当前索引上可能不存在,检测这种情况,如果节点不存在则调整索引oldStartVnode=oldCh[++oldStartIdx]//Vnode已经movedleft}elseif(isUndef(oldEndVnode)){oldEndVnode=oldCh[--oldEndIdx]}elseif(sameVnode(oldStartVnode,newStartVnode)){//旧起始节点和新起始节点是同一个节点,执行patchpatchVnode(oldStartVnode,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)//补丁结束后旧开始和新开始的索引加1oldStartVnode=oldCh[++oldStartIdx]newStartVnode=newCh[++newStartIdx]}elseif(sameVnode(oldEndVnode,newEndVnode)){//旧端和新端是同一个节点,执行补丁patchVnode(oldEndVnode,newEndVnode,insertedVnodeQueue,newCh,newEndIdx)//补丁结束后,旧端的索引和新端分别减1oldEndVnode=oldCh[--oldEndIdx]newEndVnode=newCh[--newEndIdx]}elseif(sameVnode(oldStartVnode,newEndVnode)){//Vnode右移//旧的开始和新的end是同一个节点,执行patch...}elseif(sameVnode(oldEndVnode,newStartVnode)){//Vnode向左移动//旧的结束和新的开始是同一个节点,执行patch...}else{//在旧节点中找到新的开始节点if(sameVnode(vnodeToMove,newStartvnode)){//如果两个节点相同,则执行补丁patchVnode(vnodeToMove,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)//补丁后,将旧节点设置为未定义oldCh[idxInOld]=undefinedcanMove&&nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm)}else{...}//旧节点向后移动一个newStartVnode=newCh[++newStartIdx]}}//来这里,说明老姐妹节点还是new节点已经遍历完了...}(4)终于来到了主角updateChildren。在updateChildren内部实现中,使用两组指针分别指向新旧Vnode的头尾,中间聚集递归,实现新旧数据的对比刷新。在上述资源池模型下,当找到新旧Iframe组件时,会执行如下逻辑:,newStartVnode,insertedVnodeQueue,newCh,newStartIdx)//打完补丁后将旧节点设置为undefinedoldCh[idxInOld]=undefinedcanMove&&nodeOps.insertBefore(parentElm,vnodeToMove.elm,oldStartVnode.elm)}看来是罪魁祸首问题是执行添加了nodeOps.insertBefore。在WEB的运行环境中,真正执行的是DOM的insertBeforeAPI。那么我们就一步步来看看Iframe在DOM环境下采用了什么样的刷新策略。Iframe的状态刷新机制为了更清楚的看到DOM节点的变化,我们可以引入MutationObserver来观察最新版Chrome中的DOM根节点。首先在容器节点下设置两个子节点:和,??分别执行如下方案并记录结果:比较方案A:使用insertBefore在iframe节点前插入一个新的span节点比较方案B:使用InsertBefore在iframe节点后插入一个新的span节点比较方案C:使用insertBefore交换span和iframe节点比较方案D:使用insertBefore对iframe本身进行原地操作结果如下:实验结果表明,当insertBefore在iframe上执行,其实DOM会依次执行移除和添加节点的操作,从而导致iframe状态的刷新。VuejsIssues#9473中提到了类似的问题。一种解决方案是在VuePatch中先对非iframe类型的元素进行DOM操作,但这种优化策略目前还没有被采用,这个问题在Vue3.0中仍然存在。那么在资源池模型下,如何保证iframe不执行insertBefore呢?回到Vue的Patch机制,我们发现只有当新旧Iframe在新旧Vnode列表中的相对位置不变时,才会执行patchVnode方法,不会触发insertBefore方法。因此,最终采用的解决方案是改变淘汰机制,将排序操作改为查找操作,从而保证Vue中多个窗口的状态保持不变。
