结语
Iframe是两个闻名遐迩的HTML原素,根据MDN WEB DOCS非官方介绍,Iframe表述为HTMLH55N架构原素,表示冗余的Browsing Context,它能够将另两个HTML网页内嵌到当前网页中。Iframe能低生产成本同时实现跨应用领域级的网页共享资源,并且具有使用简单、高相容性、内容隔绝等缺点,因而以Iframe为核心理念形成了后端网络平台构架领域第1代控制技术。
不可否认,当Iframe在DOM中如上所述图形时,会手动读取其对准的天然资源镜像Url,并抹除外部的状况。在两个众所周知的网络平台应用领域中,两个父应用领域主网页要装载数个询问处(每两个询问处相关联两个Iframe),所以如何在转换询问处时,同时实现每两个询问处中的状况(包括输出状况、模版信息等)不遗失,也即“状况维持”呢?
如果采用兄弟二人应用领域通讯来历史记录询问处状况,所以改建生产成本是非常巨大的。标准答案是利用Iframe的CSS Display优点,转换询问处时,非转化成状况的询问处并不消亡,仅是Display状况更改为none,转化成状况询问处的Display状况更改为非none。在Display状况转换时,Iframe不会重新读取。在Vue应用领域中,带队v-show命令方可替他们同时实现这一需求。
市场机制
上述的状况维持数学模型存在两个操控性瑕疵,即父应用领域主网页实际上要提早放置数个Iframe询问处。即使是这些不由此可见的询问处,也会收到天然资源request允诺。大量的mammalian允诺,会导致网页操控性下降。(值得一提的是,Chrome新一代版已经支持了Iframe的慢速懒读取思路,但是在此情景下,并不能明显改善mammalian允诺的问题。)因而,他们需要导入天然资源池和市场机制来管理工作数个Iframe。
导入两个耗电量为N的Iframe天然资源池来管理工作多开询问处,当天然资源池年满时,新转化成的询问处能内嵌至天然资源池内;当天然资源池已满时,天然资源池按照市场竞争思路,出局若干个池内的询问处并弃置,然后填入新转化成的询问处至天然资源池内。通过调整耗电量N,能管制父应用领域主网页上多开询问处的数量,从而管制mammalian允诺数量,同时实现天然资源管控的目的。
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 不是真实原素,则表示更新阶段,执行 patchVnodefunction 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 {
(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)) {// 老开始节点和新开始节点是同一个节点,执行 patchpatchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)// patch 结束后老开始和新开始的索引分别加 1oldStartVnode = oldCh[++oldStartIdx]newStartVnode = newCh[++newStartIdx]} else if (sameVnode(oldEndVnode, newEndVnode)) {// 老结束和新结束是同两个节点,执行 patchpatchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)// patch 结束后老结束和新结束的索引分别减 1oldEndVnode = 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)) {// 如果这两个节点是同两个,则执行 patchpatchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)// patch 结束后将该老节点置为 undefinedoldCh[idxInOld] = undefinedcanMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)} else {// 老节点向后移动两个newStartVnode = newCh[++newStartIdx]// 走到这里,说明老姐节点或者新节点被遍历完了(4)咱们终于来到了主角updateChildren。在updateChildren外部同时实现中,使用了2套指针分别对准新旧Vnode头尾,并向中间聚拢递归,以同时实现新旧数据对比刷新。
在前述天然资源池数学模型下,当查找到新旧Iframe组件时,会执行如下逻辑:
if (sameVnode(vnodeToMove, newStartVnode)) {// 如果这两个节点是同两个,则执行 patchpatchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)// patch 结束后将该老节点置为 undefinedoldCh[idxInOld] = undefinedcanMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
看来出现问题的罪魁祸首是执行了nodeOps.insertBefore。在WEB的运行环境下实际上执行的是DOM的insertBefore API。所以他们移步来看看在DOM环境下,Iframe究竟是采取了何种刷新思路。
Iframe的状况刷新机制
为了更清晰地看到DOM节点的变化情况,他们能导入MutationObserver在新一代版Chrome中来观测DOM根节点。
首先设置容器节点下有两个子节点:和,分别执行以下方案并历史记录结果:
对比方案A:使用insertBefore在iframe节点前再填入两个新的span节点
对比方案B:使用insertBefore在iframe节点后再填入两个新的span节点
对比方案C:使用insertBefore交换span和iframe节点
对比方案D:使用insertBefore原地操作iframe自身
其结果如下:
实验结果显示,对Iframe执行insertBefore时,实际上DOM会依次执行移除、新增节点操作,导致Iframe状况刷新。
在Vuejs Issues #9473中提到了类似的问题,一种解决方案是在Vue Patch时优先对非Iframe类型原素进行DOM操作,但是目前这个优化思路尚未被采用,在Vue 3.0版中也依然存在这个问题。
所以在天然资源池数学模型下,如何才能保证Iframe不执行insertBefore呢?重新回到Vue Patch机制下,他们发现,只有新旧Iframe在新旧Vnode列表中的相对位置维持不变时,才会只执行patchVnode方法,而不会触发insertBefore方法。
因而,采取的最终解决方案是,更改出局机制,将排序操作改为搜索操作,保证了多开询问处在Vue中的状况维持。