Iframe在Vue中的狀態(tài)保持技術(shù)
引言
Iframe是一個(gè)歷史悠久的HTML元素,根據(jù)MDN WEB DOCS官方介紹,Iframe定義為HTML內(nèi)聯(lián)框架元素,表示嵌套的Browsing Context,它能夠?qū)⒘硪粋€(gè)HTML頁面嵌入到當(dāng)前頁面中。Iframe可以廉價(jià)實(shí)現(xiàn)跨應(yīng)用級(jí)的頁面共享,并且具有使用簡(jiǎn)單、高兼容性、內(nèi)容隔離等優(yōu)點(diǎn),因此以Iframe為核心形成了前端平臺(tái)架構(gòu)領(lǐng)域第1代技術(shù)。
眾所周知,當(dāng)Iframe在DOM中初始渲染時(shí),會(huì)自動(dòng)加載其指向的資源鏈接Url,并重置內(nèi)部的狀態(tài)。在一個(gè)典型的平臺(tái)應(yīng)用中,一個(gè)父應(yīng)用主頁面要掛載多個(gè)窗口(每一個(gè)窗口對(duì)應(yīng)一個(gè)Iframe),那么如何在切換窗口時(shí),實(shí)現(xiàn)每一個(gè)窗口中的狀態(tài)(包括輸入狀態(tài)、錨點(diǎn)信息等)不丟失,也即“狀態(tài)保持”呢?
如果采用父子應(yīng)用通信來記錄窗口狀態(tài),那么改造成本是非常巨大的。答案是利用Iframe的CSS Display特性,切換窗口時(shí),非激活狀態(tài)的窗口并不消失,僅是Display狀態(tài)變更為none,激活狀態(tài)窗口的Display狀態(tài)變更為非none。在Display狀態(tài)切換時(shí),Iframe不會(huì)重新加載。在Vue應(yīng)用中,一行v-show指令即可替我們實(shí)現(xiàn)這一需求。
競(jìng)爭(zhēng)機(jī)制
上述的狀態(tài)保持模型存在一個(gè)性能缺陷,即父應(yīng)用主頁面實(shí)際上要提前擺放多個(gè)Iframe窗口。即使是這些不可見的窗口,也會(huì)發(fā)出資源request請(qǐng)求。大量的并發(fā)請(qǐng)求,會(huì)導(dǎo)致頁面性能下降。(值得一提的是,Chrome最新版本已經(jīng)支持了Iframe的滾動(dòng)懶加載策略,但是在此場(chǎng)景下,并不能改善并發(fā)請(qǐng)求的問題。)因此,我們需要引入資源池和競(jìng)爭(zhēng)機(jī)制來管理多個(gè)Iframe。
引入一個(gè)容量為N的Iframe資源池來管理多開窗口,當(dāng)資源池未滿時(shí),新激活的窗口可以直接插入至資源池中;當(dāng)資源池已滿時(shí),資源池按照競(jìng)爭(zhēng)策略,淘汰若干池中的窗口并丟棄,然后插入新激活的窗口至資源池中。通過調(diào)整容量N,可以限制父應(yīng)用主頁面上多開窗口的數(shù)量,從而限制并發(fā)請(qǐng)求數(shù)量,實(shí)現(xiàn)資源管控的目的。
Vue Patch原理探索
日前遇到了一個(gè)基于Vue應(yīng)用的Iframe狀態(tài)保持問題,在上述模型下,資源池不僅保存窗口對(duì)象,而且記錄了每個(gè)窗口的點(diǎn)擊激活時(shí)間。資源池使用以下競(jìng)爭(zhēng)淘汰策略:對(duì)窗口激活時(shí)間進(jìn)行先后次序排序,激活時(shí)間排序次序較前的窗口優(yōu)先被淘汰。當(dāng)資源池滿時(shí),會(huì)偶發(fā)池中窗口狀態(tài)不能保持的問題。
在Vue中,組件是一個(gè)可復(fù)用的Vue實(shí)例,Vue 會(huì)盡可能高效地渲染元素,通常會(huì)復(fù)用已有元素而不是從頭開始渲染。組件狀態(tài)是否正確保持,依賴關(guān)鍵屬性key?;诖?,首先排查了Iframe組件的key屬性。事實(shí)上,Iframe組件已經(jīng)正確分配了唯一的Uid,此種情況可以排除。
既然不是組件復(fù)用的問題,那么在Vue內(nèi)部的Diff Patch機(jī)制到底是如何運(yùn)行的呢?讓我們看一下Vue 2.0的源代碼:
/** * 頁面首次渲染和后續(xù)更新的入口位置,也是 patch 的入口位置 */ Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { if (!prevVnode) { // 老 VNode 不存在,表示首次渲染,即初始化頁面時(shí)走這里 …… } else { // 響應(yīng)式數(shù)據(jù)更新時(shí),即更新頁面時(shí)走這里 vm.$el = vm.__patch__(prevVnode, vnode) } }
(1)在update生命周期下,主要執(zhí)行了vm.__patch__
方法。
/** * vm.__patch__ * 1、新節(jié)點(diǎn)不存在,老節(jié)點(diǎn)存在,調(diào)用 destroy,銷毀老節(jié)點(diǎn) * 2、如果 oldVnode 是真實(shí)元素,則表示首次渲染,創(chuàng)建新節(jié)點(diǎn),并插入 body,然后移除老節(jié)點(diǎn) * 3、如果 oldVnode 不是真實(shí)元素,則表示更新階段,執(zhí)行 patchVnode */ function patch(oldVnode, vnode, hydrating, removeOnly) { …… // 1、新節(jié)點(diǎn)不存在,老節(jié)點(diǎn)存在,調(diào)用 destroy,銷毀老節(jié)點(diǎn) if (isUndef(oldVnode)) { …… // 2、老節(jié)點(diǎn)不存在,執(zhí)行創(chuàng)建新節(jié)點(diǎn) } else { // 判斷 oldVnode 是否為真實(shí)元素 const isRealElement = isDef(oldVnode.nodeType) if (!isRealElement && sameVnode(oldVnode, vnode)) { // 3、不是真實(shí)元素,但是老節(jié)點(diǎn)和新節(jié)點(diǎn)是同一個(gè)節(jié)點(diǎn),則是更新階段,執(zhí)行 patch 更新節(jié)點(diǎn) patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { ……// 是真實(shí)元素,則表示初次渲染 } } invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
(2)在__patch__
方法內(nèi)部,觸發(fā)patchVnode
方法。
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { …… if (isUndef(vnode.text)) {// 新節(jié)點(diǎn)不為文本節(jié)點(diǎn) if (isDef(oldCh) && isDef(ch)) {// 新舊節(jié)點(diǎn)的子節(jié)點(diǎn)都存在,執(zhí)行diff遞歸 if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) } else { …… } } else { …… } }
(3)在patchVnode
方法內(nèi)部,觸發(fā)updateChildren
方法。
/** * diff 過程: * diff 優(yōu)化:做了四種假設(shè),假設(shè)新老節(jié)點(diǎn)開頭結(jié)尾有相同節(jié)點(diǎn)的情況,一旦命中假設(shè),就避免了一次循環(huán),以提高執(zhí)行效率 * 如果不幸沒有命中假設(shè),則執(zhí)行遍歷,從老節(jié)點(diǎn)中找到新開始節(jié)點(diǎn) * 找到相同節(jié)點(diǎn),則執(zhí)行 patchVnode,然后將老節(jié)點(diǎn)移動(dòng)到正確的位置 * 如果老節(jié)點(diǎn)先于新節(jié)點(diǎn)遍歷結(jié)束,則剩余的新節(jié)點(diǎn)執(zhí)行新增節(jié)點(diǎn)操作 * 如果新節(jié)點(diǎn)先于老節(jié)點(diǎn)遍歷結(jié)束,則剩余的老節(jié)點(diǎn)執(zhí)行刪除操作,移除這些老節(jié)點(diǎn) */ function updateChildren(parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { // 老節(jié)點(diǎn)的開始索引 let oldStartIdx = 0 // 新節(jié)點(diǎn)的開始索引 let newStartIdx = 0 // 老節(jié)點(diǎn)的結(jié)束索引 let oldEndIdx = oldCh.length - 1 // 第一個(gè)老節(jié)點(diǎn) let oldStartVnode = oldCh[0] // 最后一個(gè)老節(jié)點(diǎn) let oldEndVnode = oldCh[oldEndIdx] // 新節(jié)點(diǎn)的結(jié)束索引 let newEndIdx = newCh.length - 1 // 第一個(gè)新節(jié)點(diǎn) let newStartVnode = newCh[0] // 最后一個(gè)新節(jié)點(diǎn) let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, vnodeToMove, refElm // 遍歷新老兩組節(jié)點(diǎn),只要有一組遍歷完(開始索引超過結(jié)束索引)則跳出循環(huán) while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { // 如果節(jié)點(diǎn)被移動(dòng),在當(dāng)前索引上可能不存在,檢測(cè)這種情況,如果節(jié)點(diǎn)不存在則調(diào)整索引 oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] } else if (sameVnode(oldStartVnode, newStartVnode)) { // 老開始節(jié)點(diǎn)和新開始節(jié)點(diǎn)是同一個(gè)節(jié)點(diǎn),執(zhí)行 patch patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // patch 結(jié)束后老開始和新開始的索引分別加 1 oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] } else if (sameVnode(oldEndVnode, newEndVnode)) { // 老結(jié)束和新結(jié)束是同一個(gè)節(jié)點(diǎn),執(zhí)行 patch patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) // patch 結(jié)束后老結(jié)束和新結(jié)束的索引分別減 1 oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right // 老開始和新結(jié)束是同一個(gè)節(jié)點(diǎn),執(zhí)行 patch …… } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left // 老結(jié)束和新開始是同一個(gè)節(jié)點(diǎn),執(zhí)行 patch …… } else { // 如果上面的四種假設(shè)都不成立,則通過遍歷找到新開始節(jié)點(diǎn)在老節(jié)點(diǎn)中的位置索引 …… // 在老節(jié)點(diǎn)中找到新開始節(jié)點(diǎn)了 if (sameVnode(vnodeToMove, newStartVnode)) { // 如果這兩個(gè)節(jié)點(diǎn)是同一個(gè),則執(zhí)行 patch patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // patch 結(jié)束后將該老節(jié)點(diǎn)置為 undefined oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 最后這種情況是,找到節(jié)點(diǎn)了,但是發(fā)現(xiàn)兩個(gè)節(jié)點(diǎn)不是同一個(gè)節(jié)點(diǎn),則視為新元素,執(zhí)行創(chuàng)建 …… } // 老節(jié)點(diǎn)向后移動(dòng)一個(gè) newStartVnode = newCh[++newStartIdx] } } // 走到這里,說明老姐節(jié)點(diǎn)或者新節(jié)點(diǎn)被遍歷完了,執(zhí)行剩余節(jié)點(diǎn)的處理 …… }
(4)咱們終于來到了主角updateChildren
。在updateChildren
內(nèi)部實(shí)現(xiàn)中,使用了2套指針分別指向新舊Vnode頭尾,并向中間聚攏遞歸,以實(shí)現(xiàn)新舊數(shù)據(jù)對(duì)比刷新。
在前述資源池模型下,當(dāng)查找到新舊Iframe組件時(shí),會(huì)執(zhí)行如下邏輯:
if (sameVnode(vnodeToMove, newStartVnode)) { // 如果這兩個(gè)節(jié)點(diǎn)是同一個(gè),則執(zhí)行 patch patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // patch 結(jié)束后將該老節(jié)點(diǎn)置為 undefined oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) }
看來出現(xiàn)問題的罪魁禍?zhǔn)资菆?zhí)行了nodeOps.insertBefore
。在WEB的運(yùn)行環(huán)境下實(shí)際上執(zhí)行的是DOM的insertBefore API。那么我們移步來看看在DOM環(huán)境下,Iframe究竟是采取了何種刷新策略。
Iframe的狀態(tài)刷新機(jī)制
為了更清晰地看到DOM節(jié)點(diǎn)的變化情況,我們可以引入MutationObserver在最新版Chrome中來觀測(cè)DOM根節(jié)點(diǎn)。\
首先設(shè)置容器節(jié)點(diǎn)下有兩個(gè)子節(jié)點(diǎn):<span/>
和<iframe/>
,分別執(zhí)行以下方案并記錄結(jié)果:\
對(duì)比方案A:使用insertBefore在iframe節(jié)點(diǎn)前再插入一個(gè)新的span節(jié)點(diǎn)\
對(duì)比方案B:使用insertBefore在iframe節(jié)點(diǎn)后再插入一個(gè)新的span節(jié)點(diǎn)\
對(duì)比方案C:使用insertBefore交換span和iframe節(jié)點(diǎn)\
對(duì)比方案D:使用insertBefore原地操作iframe自身\
其結(jié)果如下:
方案名稱 | Iframe是否刷新 | DOM節(jié)點(diǎn)變化 |
---|---|---|
A | 否 | 新增一個(gè)子節(jié)點(diǎn)span |
B | 否 | 新增一個(gè)子節(jié)點(diǎn)span |
C | 是 | 先移除一個(gè)iframe,再插入一個(gè)iframe |
D | 是 | 先移除一個(gè)iframe,再插入一個(gè)iframe |
實(shí)驗(yàn)結(jié)果顯示,對(duì)Iframe執(zhí)行insertBefore時(shí),實(shí)際上DOM會(huì)依次執(zhí)行移除、新增節(jié)點(diǎn)操作,導(dǎo)致Iframe狀態(tài)刷新。
在Vuejs Issues #9473中提到了類似的問題,一種解決方案是在Vue Patch時(shí)優(yōu)先對(duì)非Iframe類型元素進(jìn)行DOM操作,但是目前這個(gè)優(yōu)化策略尚未被采用,在Vue 3.0版本中也依然存在這個(gè)問題。
那么在資源池模型下,如何才能保證Iframe不執(zhí)行insertBefore呢?重新回到Vue Patch機(jī)制下,我們發(fā)現(xiàn),只有新舊Iframe在新舊Vnode列表中的相對(duì)位置保持不變時(shí),才會(huì)只執(zhí)行patchVnode方法,而不會(huì)觸發(fā)insertBefore方法。
因此,采取的最終解決方案是,更改淘汰機(jī)制,將排序操作改為搜索操作,保證了多開窗口在Vue中的狀態(tài)保持。
以上就是Iframe在Vue中的狀態(tài)保持技術(shù)的詳細(xì)內(nèi)容,更多關(guān)于Iframe Vue狀態(tài)保持的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
vue?codemirror實(shí)現(xiàn)在線代碼編譯器效果
這篇文章主要介紹了vue-codemirror實(shí)現(xiàn)在線代碼編譯器?,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-12-12Vue+Vant實(shí)現(xiàn)7天日歷展示并在切換日期時(shí)實(shí)時(shí)變換功能
本文介紹了如何利用Vue和Vant框架結(jié)合moment.js插件來實(shí)現(xiàn)一個(gè)7天日歷展示功能,在這個(gè)功能中,用戶可以在切換日期時(shí)看到界面的實(shí)時(shí)變化,此外,文章還提供了代碼實(shí)現(xiàn)和效果測(cè)試的詳細(xì)步驟,幫助開發(fā)者能夠順利完成類似的項(xiàng)目開發(fā)2024-10-10關(guān)于ElementUI的el-upload組件二次封裝的問題
這篇文章主要介紹了關(guān)于ElementUI的el-upload組件二次封裝的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10Vue.js基礎(chǔ)之監(jiān)聽子組件事件v-on及綁定數(shù)據(jù)v-model學(xué)習(xí)
這篇文章主要為大家介紹了Vue.js基礎(chǔ)之監(jiān)聽子組件事件v-on及綁定數(shù)據(jù)v-model學(xué)習(xí),有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-06-06在Vue.js應(yīng)用中實(shí)現(xiàn)分布式搜索和全文搜索
分布式搜索和全文搜索在現(xiàn)代應(yīng)用程序中變得越來越重要,因?yàn)樗鼈兛梢詭椭脩艨焖俨檎液蜋z索大量數(shù)據(jù),Elasticsearch是一種強(qiáng)大的分布式搜索引擎,本文將介紹如何在Vue.js應(yīng)用程序中實(shí)現(xiàn)分布式搜索和全文搜索,以及如何與Elasticsearch集成,需要的朋友可以參考下2023-11-11