解決vue3中內(nèi)存泄漏的問(wèn)題
vue3的內(nèi)存泄漏
在項(xiàng)目中會(huì)發(fā)現(xiàn)一個(gè)奇怪的現(xiàn)象,當(dāng)使用element-plus中的圖標(biāo)組件時(shí)會(huì)出現(xiàn)內(nèi)存泄漏。詳情查看
解決方案1:關(guān)閉靜態(tài)提升。詳情查看
解決方案2:參考本文
至于為什么靜態(tài)提升會(huì)導(dǎo)致內(nèi)存泄漏,本文將通過(guò)幾個(gè)案例的源碼分析詳細(xì)講解。
案例1
<div id="app"></div> <script type="module"> ?import { ? ?createApp, ? ?ref, } from '../packages/vue/dist/vue.esm-browser.js' ?const app = createApp({ ? ?setup() { ? ? ?const show = ref(false) ? ? ?return { ? ? ? ?show, ? ? } ? }, ? ? ?template: ` ? ? ? ?<div> ? ? ? ? ? ?<button @click="show=!show">show</button> ? ? ? ? ? ?<template v-if="show"> ? ? ? ? ? ? ? ?<template v-for="i in 3"> ? ? ? ? ? ? ? ? ? ? ?<div> ? ? ? ? ? ? ? ? ? ? ? ?<span>12</span> ? ? ? ? ? ? ? ? ? ? ? ?<span>34</span> </div> ?</template> ?</template> </div> ? ? ? ? ? ?` }) ?app.mount('#app') </script>
點(diǎn)擊按鈕前:游離節(jié)點(diǎn)只有一個(gè),這是熱更新導(dǎo)致的,不需要管。
點(diǎn)擊兩次按鈕后:
對(duì)比可以發(fā)現(xiàn)多出了兩個(gè)span和和一個(gè)div和兩個(gè)text的游離節(jié)點(diǎn),最下面的注釋節(jié)點(diǎn)不需要管。
先來(lái)看一下這個(gè)模板編譯后的結(jié)果:
const _Vue = Vue const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue ? const _hoisted_1 = ["onClick"] const _hoisted_2 = /*#__PURE__*/_createElementVNode("span", null, "12", -1 /* HOISTED */) const _hoisted_3 = /*#__PURE__*/_createElementVNode("span", null, "34", -1 /* HOISTED */) const _hoisted_4 = [ ?_hoisted_2, ?_hoisted_3 ] ? return function render(_ctx, _cache, $props, $setup, $data, $options) { ?with (_ctx) { ? ?const { createElementVNode: _createElementVNode, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue ? ? ?return (_openBlock(), _createElementBlock("div", null, [ ? ? ?_createElementVNode("button", { ? ? ? ?onClick: $event => (show=!show) ? ? }, "show", 8 /* PROPS */, _hoisted_1), ? ? ?show ? ? ? ?? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(3, (i) => { ? ? ? ? ? ?return (_openBlock(), _createElementBlock("div", null, _hoisted_4)) ? ? ? ? }), 256 /* UNKEYED_FRAGMENT */)) ? ? ? : _createCommentVNode("v-if", true) ? ])) } }
這里關(guān)注_hoisted_4
靜態(tài)節(jié)點(diǎn)。
掛載階段
掛載第一個(gè)div的子節(jié)點(diǎn)時(shí):
此時(shí)children
中兩個(gè)節(jié)點(diǎn)分別指向_hoisted_2
和 _hoisted_3
循環(huán)遍歷children時(shí),會(huì)走cloneIfMounted
。
export function cloneIfMounted(child: VNode): VNode { ?return (child.el === null && child.patchFlag !== PatchFlags.HOISTED) || ? ?child.memo ? ?? child ? : cloneVNode(child) }
而_hoisted_2
和 _hoisted_3
一開(kāi)始屬性el為null但patchFlag使HOISTED,所以會(huì)走cloneVnode
export function cloneVNode<T, U>( ?vnode: VNode<T, U>, ?extraProps?: (Data & VNodeProps) | null, ?mergeRef = false ): VNode<T, U> { ?// This is intentionally NOT using spread or extend to avoid the runtime ?// key enumeration cost. ?const { props, ref, patchFlag, children } = vnode ?const mergedProps = extraProps ? mergeProps(props || {}, extraProps) : props ?const cloned: VNode<T, U> = { ? ?__v_isVNode: true, ? ?__v_skip: true, ? ?type: vnode.type, ? ?props: mergedProps, ? ?key: mergedProps && normalizeKey(mergedProps), ? ?ref: ? ? ?extraProps && extraProps.ref ? ? ? ?? // #2078 in the case of <component :is="vnode" ref="extra"/> ? ? ? ? ?// if the vnode itself already has a ref, cloneVNode will need to merge ? ? ? ? ?// the refs so the single vnode can be set on multiple refs ? ? ? ? ?mergeRef && ref ? ? ? ? ?? isArray(ref) ? ? ? ? ? ?? ref.concat(normalizeRef(extraProps)!) ? ? ? ? ? : [ref, normalizeRef(extraProps)!] ? ? ? ? : normalizeRef(extraProps) ? ? ? : ref, ? ?scopeId: vnode.scopeId, ? ?slotScopeIds: vnode.slotScopeIds, ? ?children: ? ? ?__DEV__ && patchFlag === PatchFlags.HOISTED && isArray(children) ? ? ? ?? (children as VNode[]).map(deepCloneVNode) ? ? ? : children, ? ?target: vnode.target, ? ?targetAnchor: vnode.targetAnchor, ? ?staticCount: vnode.staticCount, ? ?shapeFlag: vnode.shapeFlag, ? ?// if the vnode is cloned with extra props, we can no longer assume its ? ?// existing patch flag to be reliable and need to add the FULL_PROPS flag. ? ?// note: preserve flag for fragments since they use the flag for children ? ?// fast paths only. ? ?patchFlag: ? ? ?extraProps && vnode.type !== Fragment ? ? ? ?? patchFlag === -1 // hoisted node ? ? ? ? ?? PatchFlags.FULL_PROPS ? ? ? ? : patchFlag | PatchFlags.FULL_PROPS ? ? ? : patchFlag, ? ?dynamicProps: vnode.dynamicProps, ? ?dynamicChildren: vnode.dynamicChildren, ? ?appContext: vnode.appContext, ? ?dirs: vnode.dirs, ? ?transition: vnode.transition, ? ? ?// These should technically only be non-null on mounted VNodes. However, ? ?// they *should* be copied for kept-alive vnodes. So we just always copy ? ?// them since them being non-null during a mount doesn't affect the logic as ? ?// they will simply be overwritten. ? ?component: vnode.component, ? ?suspense: vnode.suspense, ? ?ssContent: vnode.ssContent && cloneVNode(vnode.ssContent), ? ?ssFallback: vnode.ssFallback && cloneVNode(vnode.ssFallback), ? ?el: vnode.el, ? ?anchor: vnode.anchor, ? ?ctx: vnode.ctx, ? ?ce: vnode.ce } ?if (__COMPAT__) { ? ?defineLegacyVNodeProperties(cloned as VNode) } ?return cloned }
克隆時(shí)會(huì)將被克隆節(jié)點(diǎn)的el賦值給新的節(jié)點(diǎn)。
回到循環(huán)體,可以看到將克隆后的節(jié)點(diǎn)重新賦值給了children即_hoisted_4
[i],此時(shí)_hoisted_4
中的內(nèi)容不再指向_hoisted_2
和_hoisted_3
,而是克隆后的節(jié)點(diǎn)。_hoisted_2
和_hoisted_3
就此完全脫離了關(guān)系。這是一個(gè)疑點(diǎn),每次都需要克隆,不懂這樣靜態(tài)的提升的意義在哪里。
后續(xù)div子節(jié)點(diǎn)的掛載都會(huì)走這個(gè)循環(huán),每次循環(huán)都會(huì)克隆節(jié)點(diǎn)并重新賦值給children即_hoisted_4
[i]。
到此,掛載完成。
可想而知,掛載完成后,children即_hoisted_4
中的內(nèi)容是最后一個(gè)div的兩個(gè)虛擬子節(jié)點(diǎn)。
卸載階段
這里卸載的虛擬節(jié)點(diǎn)的type是Symbol(v-fgt)
這是vue處理<template v-if>
標(biāo)簽時(shí)創(chuàng)建的虛擬節(jié)點(diǎn),這里需要關(guān)注的是unmount
方法的第四個(gè)參數(shù)doRemove
,傳入了true。
type UnmountFn = ( ?vnode: VNode, ?parentComponent: ComponentInternalInstance | null, ?parentSuspense: SuspenseBoundary | null, ?doRemove?: boolean, ?optimized?: boolean ) => void const unmount: UnmountFn
進(jìn)入unmount函數(shù),會(huì)走到type===Fragment
的分支。
else if ( (type === Fragment && ? patchFlag & ? (PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) || (!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN) ) { ?unmountChildren(children as VNode[], parentComponent, parentSuspense) }
調(diào)用unmountChildren
方法。
type UnmountChildrenFn = ( ?children: VNode[], ?parentComponent: ComponentInternalInstance | null, ?parentSuspense: SuspenseBoundary | null, ?doRemove?: boolean, ?optimized?: boolean, ?start?: number ) => void const unmountChildren: UnmountChildrenFn = ( ? ?children, ? ?parentComponent, ? ?parentSuspense, ? ?doRemove = false, ? ?optimized = false, ? ?start = 0 ) => { for (let i = start; i < children.length; i++) { ?unmount(children[i], parentComponent, parentSuspense, doRemove, optimized) } }
調(diào)用時(shí)沒(méi)有傳入第四個(gè)參數(shù),默認(rèn)是false。然后會(huì)遞歸調(diào)用unmount
方法。
注意,此時(shí)傳入的doRemove
是false。
循環(huán)調(diào)用unmount
傳入div的虛擬節(jié)點(diǎn)
此時(shí)走到unmount
方法中的這個(gè)分支
else if ( ?dynamicChildren && ?// #1153: fast path should not be taken for non-stable (v-for) fragments (type !== Fragment || ? (patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT)) ) { ?// fast path for block nodes: only need to unmount dynamic children. ?unmountChildren( ? ?dynamicChildren, ? ?parentComponent, ? ?parentSuspense, ? ?false, ? ?true ) }
dynamicChildren
是空數(shù)組,所以unmountChildren
不會(huì)發(fā)生什么。
繼續(xù)往下走,unmount
方法中的最后有
if (doRemove) { ?remove(vnode) }
此時(shí)doRemove
為false,不會(huì)調(diào)用remove
方法。
處理完三個(gè)div的節(jié)點(diǎn)后,函數(shù)回到上一層。接著處理type是Symbol(v-fgt)
的虛擬節(jié)點(diǎn)。而此時(shí)doRemove
為true,調(diào)用remove
方法。
const remove: RemoveFn = vnode => { ?const { type, el, anchor, transition } = vnode ?if (type === Fragment) { ? ?if ( ? ? ?__DEV__ && ? ? ?vnode.patchFlag > 0 && ? ? ?vnode.patchFlag & PatchFlags.DEV_ROOT_FRAGMENT && ? ? ?transition && ? ? ?!transition.persisted ? ) { ? ? ;(vnode.children as VNode[]).forEach(child => { ? ? ? ?if (child.type === Comment) { ? ? ? ? ?hostRemove(child.el!) ? ? ? } else { ? ? ? ? ?remove(child) ? ? ? } ? ? }) ? } else { ? ? ?removeFragment(el!, anchor!) ? } ? ?return } ? ?if (type === Static) { ? ?removeStaticNode(vnode) ? ?return } ? ?const performRemove = () => { ? ?hostRemove(el!) ? ?if (transition && !transition.persisted && transition.afterLeave) { ? ? ?transition.afterLeave() ? } } ? ?if ( ? ?vnode.shapeFlag & ShapeFlags.ELEMENT && ? ?transition && ? ?!transition.persisted ) { ? ?const { leave, delayLeave } = transition ? ?const performLeave = () => leave(el!, performRemove) ? ?if (delayLeave) { ? ? ?delayLeave(vnode.el!, performRemove, performLeave) ? } else { ? ? ?performLeave() ? } } else { ? ?performRemove() } }
會(huì)走到removeFragment
方法。
const removeFragment = (cur: RendererNode, end: RendererNode) => { ?// For fragments, directly remove all contained DOM nodes. ?// (fragment child nodes cannot have transition) ?let next ?while (cur !== end) { ? ?next = hostNextSibling(cur)! ? ?hostRemove(cur) ? ?cur = next } ?hostRemove(end) }
從這里可以看到,會(huì)依次刪除掉3個(gè)div的真實(shí)dom。
到此,整個(gè)<template v-if>
卸載完成。
那到底內(nèi)存泄漏在哪里?
還記得_hoissted_4
保存的是最后一個(gè)虛擬div節(jié)點(diǎn)的兩個(gè)虛擬span節(jié)點(diǎn),而節(jié)點(diǎn)中的el屬性依然維持著真實(shí)節(jié)點(diǎn)的引用,不會(huì)被GC,
所以這就造成了內(nèi)存泄漏。這里就解釋了那兩個(gè)游離的span節(jié)點(diǎn)。
好奇的你一定會(huì)問(wèn):還有一個(gè)游離的div和兩個(gè)游離的text節(jié)點(diǎn)哪里來(lái)的呢?
不要忘記了,el中也會(huì)保持對(duì)父親和兒子的引用。詳情見(jiàn)下圖
每一個(gè)span都有一個(gè)text兒子,共用一個(gè)div父節(jié)點(diǎn),完美解釋了前面提到的所有游離節(jié)點(diǎn)。
案例2
將案例1的代碼稍稍做下改動(dòng)。
<div id="app"></div> <script type="module"> ?import { ? ?createApp, ? ?ref, } from '../packages/vue/dist/vue.esm-browser.js' ?const app = createApp({ ? ?setup() { ? ? ?const show = ref(false) ? ? ?return { ? ? ? ?show, ? ? } ? }, ? ? ?template: ` ? ? ? ?<div> ? ? ? ? ? ?<button @click="show=!show">show</button> ? ? ? ? ? ?<div v-if="show"> ? ? ? ? ? ? ? ?<template v-for="i in 3"> ? ? ? ? ? ? ? ? ? ? ?<div> ? ? ? ? ? ? ? ? ? ? ? ?<span>12</span> ? ? ? ? ? ? ? ? ? ? ? ?<span>34</span> </div> </template> </div> </div> ? ? ? ? ? ?` }) ?app.mount('#app') </script>
點(diǎn)擊按鈕前:游離節(jié)點(diǎn)只有一個(gè),這是熱更新導(dǎo)致的,不需要管。
點(diǎn)擊兩次按鈕后:
你會(huì)震驚地發(fā)現(xiàn),我就改變了一個(gè)標(biāo)簽,泄漏的節(jié)點(diǎn)竟然多了這么多。 對(duì)比可以發(fā)現(xiàn)多出了六個(gè)span和和四個(gè)div和八個(gè)text的游離節(jié)點(diǎn),最下面的注釋節(jié)點(diǎn)不需要管。
同樣查看編譯后的結(jié)果
const _Vue = Vue const { createElementVNode: _createElementVNode, createCommentVNode: _createCommentVNode } = _Vue ? const _hoisted_1 = ["onClick"] const _hoisted_2 = { key: 0 } const _hoisted_3 = /*#__PURE__*/_createElementVNode("span", null, "12", -1 /* HOISTED */) const _hoisted_4 = /*#__PURE__*/_createElementVNode("span", null, "34", -1 /* HOISTED */) const _hoisted_5 = [ ?_hoisted_3, ?_hoisted_4 ] ? return function render(_ctx, _cache, $props, $setup, $data, $options) { ?with (_ctx) { ? ?const { createElementVNode: _createElementVNode, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue ? ? ?return (_openBlock(), _createElementBlock("div", null, [ ? ? ?_createElementVNode("button", { ? ? ? ?onClick: $event => (show=!show) ? ? }, "show", 8 /* PROPS */, _hoisted_1), ? ? ?show ? ? ? ?? (_openBlock(), _createElementBlock("div", _hoisted_2, [ ? ? ? ? ? (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(3, (i) => { ? ? ? ? ? ? ?return (_openBlock(), _createElementBlock("div", null, _hoisted_5)) ? ? ? ? ? }), 256 /* UNKEYED_FRAGMENT */)) ? ? ? ? ])) ? ? ? : _createCommentVNode("v-if", true) ? ])) } }
這里關(guān)注的是_hoisted_5
節(jié)點(diǎn)
掛載階段
掛載和案例1的過(guò)程大差不差,只需要知道掛載完成后,children即_hoisted_5
中的內(nèi)容是最后一個(gè)div的兩個(gè)虛擬子節(jié)點(diǎn)。
卸載階段
這里卸載的虛擬節(jié)點(diǎn)的type是div
,這是<div v-if>
的虛擬節(jié)點(diǎn),這里需要關(guān)注的是unmount
方法的第四個(gè)參數(shù)doRemove
,傳入了true。
type UnmountFn = ( ?vnode: VNode, ?parentComponent: ComponentInternalInstance | null, ?parentSuspense: SuspenseBoundary | null, ?doRemove?: boolean, ?optimized?: boolean ) => void const unmount: UnmountFn
進(jìn)入unmount函數(shù),會(huì)走到這個(gè)分支。
else if ( ?dynamicChildren && ?// #1153: fast path should not be taken for non-stable (v-for) fragments (type !== Fragment || ? (patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT)) ) { ?// fast path for block nodes: only need to unmount dynamic children. ?unmountChildren( ? ?dynamicChildren, ? ?parentComponent, ? ?parentSuspense, ? ?false, ? ?true ) }
dynamicChildren
是長(zhǎng)度為1的數(shù)組,保存著:
注意,這里傳入的doRemove
參數(shù)是false,這是和案例一同樣是卸載Fragment的重大區(qū)別。
type UnmountChildrenFn = ( ?children: VNode[], ?parentComponent: ComponentInternalInstance | null, ?parentSuspense: SuspenseBoundary | null, ?doRemove?: boolean, ?optimized?: boolean, ?start?: number ) => void ?const unmountChildren: UnmountChildrenFn = ( ? ?children, ? ?parentComponent, ? ?parentSuspense, ? ?doRemove = false, ? ?optimized = false, ? ?start = 0 ) => { ? ?for (let i = start; i < children.length; i++) { ? ? ?unmount(children[i], parentComponent, parentSuspense, doRemove, optimized) ? } }
接著調(diào)用unmount
方法,傳入的doRemove
是false。
新的unmount
會(huì)走這個(gè)分支
else if ( (type === Fragment && ? patchFlag & ? (PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) || (!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN) ) { ?unmountChildren(children as VNode[], parentComponent, parentSuspense) }
調(diào)用時(shí)沒(méi)有傳入第四個(gè)參數(shù),默認(rèn)是false。然后會(huì)遞歸調(diào)用unmount
方法。
處理3個(gè)div時(shí)與案例一一樣,接著回到處理Framgent。
繼續(xù)往下走,unmount
方法中的最后有
if (doRemove) { ?remove(vnode) }
此時(shí)doRemove
為false,不會(huì)調(diào)用remove
方法。所以到這里,依舊沒(méi)有到案例一的循環(huán)刪除真實(shí)節(jié)點(diǎn)的環(huán)節(jié)。接著往下看。
處理完Framgent
,再回到<div v-if>
對(duì)應(yīng)的虛擬節(jié)點(diǎn)的那一層。
if (doRemove) { ?remove(vnode) }
此時(shí)doRemove為true,進(jìn)入remove函數(shù)。
const remove: RemoveFn = vnode => { ?const { type, el, anchor, transition } = vnode ?... ?... ?... ?const performRemove = () => { ? ?hostRemove(el!) ? ? ? ? ? ? ? if (transition && !transition.persisted && transition.afterLeave) { ? ? ?transition.afterLeave() ? } } ? ?if ( ? ?vnode.shapeFlag & ShapeFlags.ELEMENT && ? ?transition && ? ?!transition.persisted ) { ? ?const { leave, delayLeave } = transition ? ?const performLeave = () => leave(el!, performRemove) ? ?if (delayLeave) { ? ? ?delayLeave(vnode.el!, performRemove, performLeave) ? } else { ? ? ?performLeave() ? } } else { ? ?//走到這 ? ?performRemove() } }
調(diào)用performRemove
,此時(shí)<div v-if>
這個(gè)節(jié)點(diǎn)從文檔中移除,那么它所包含的所有子節(jié)點(diǎn)也移除了。
此時(shí)卸載階段完成。
現(xiàn)在來(lái)思考一下,游離的節(jié)點(diǎn)是從哪里來(lái)的?
同樣的,_hoisted_5
中的兩個(gè)虛擬節(jié)點(diǎn)中還各自保存著相應(yīng)的el 即span。那么根據(jù)其引用父親和兒子將全部不能被GC,所以變成了游離的節(jié)點(diǎn)。
這時(shí)的你會(huì)有所疑惑,為什么同樣是維持著父子關(guān)系,案例一種游離的只有2個(gè)span,2個(gè)text和一個(gè)div,而這卻多了這么多。
進(jìn)入解答環(huán)節(jié):
對(duì)比可以發(fā)現(xiàn),關(guān)鍵點(diǎn)在于方案二中處理Fragment
時(shí)沒(méi)有進(jìn)入到
if (doRemove) { ?remove(vnode) }
也就沒(méi)有到
const removeFragment = (cur: RendererNode, end: RendererNode) => { ?// For fragments, directly remove all contained DOM nodes. ?// (fragment child nodes cannot have transition) ?let next ?while (cur !== end) { ? ?next = hostNextSibling(cur)! ? ?hostRemove(cur) ? ?cur = next } ?hostRemove(end) }
取而代之的是直接將最頂層的div從文檔刪除。那么_hoisted_5
中的兩個(gè)虛擬節(jié)點(diǎn)中保存的el,其parentNode是div,而div中又保持著兄弟節(jié)點(diǎn)(因?yàn)闆](méi)有顯示地刪除,所以會(huì)繼續(xù)存在),即剩余的兩個(gè)div以及它的父節(jié)點(diǎn)即<div v-if>
,而各自又保存著各自的兒子。
離的個(gè)數(shù):
Span: 3 * 2 =6
Div: 3 + 1 = 4
Text: 3*2 = 6
等等,text節(jié)點(diǎn)不是有8個(gè)嗎,還有兩個(gè)在哪里?
這就要提到vue處理Fragment
的時(shí)候做的處理了。
可以看到,處理Fragment
時(shí)會(huì)創(chuàng)建兩個(gè)空節(jié)點(diǎn),作為子節(jié)點(diǎn)插入的錨點(diǎn)。所以上下會(huì)多了兩個(gè)文本節(jié)點(diǎn)。如下圖。
所以最終的游離個(gè)數(shù):
Span: 3 * 2 =6
Div: 3 + 1 = 4
Text: 3*2+2 = 8
到此,完美解釋了所有的游離節(jié)點(diǎn)。
通過(guò)案例引出解決方案
可以看到一個(gè)標(biāo)簽的改變,直接改變了游離節(jié)點(diǎn)的個(gè)數(shù),設(shè)想一下,這是一個(gè)表格,而且里面包含靜態(tài)提升的節(jié)點(diǎn),那么整個(gè)表格將會(huì)變成游離節(jié)點(diǎn),發(fā)生內(nèi)存泄漏,這是我在項(xiàng)目中的親身經(jīng)歷,才會(huì)引發(fā)我寫(xiě)出這篇文章。
好了,為什么使用element的圖標(biāo)庫(kù)會(huì)造成內(nèi)存泄漏?
看看源碼:
// src/components/add-location.vue var _hoisted_1 = { ?viewBox: "0 0 1024 1024", ?xmlns: "http://www.w3.org/2000/svg" }, _hoisted_2 = /* @__PURE__ */ _createElementVNode("path", { ?fill: "currentColor", ?d: "M288 896h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32z" }, null, -1), _hoisted_3 = /* @__PURE__ */ _createElementVNode("path", { ?fill: "currentColor", ?d: "M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416zM512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544z" }, null, -1), _hoisted_4 = /* @__PURE__ */ _createElementVNode("path", { ?fill: "currentColor", ?d: "M544 384h96a32 32 0 1 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64h96v-96a32 32 0 0 1 64 0v96z" }, null, -1), _hoisted_5 = [ ?_hoisted_2, ?_hoisted_3, ?_hoisted_4 ]; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { ?return _openBlock(), _createElementBlock("svg", _hoisted_1, _hoisted_5); }
這是什么?天,靜態(tài)提升!所以只要使用了就會(huì)造成內(nèi)存泄漏!
解決方案一在開(kāi)頭就有,就是關(guān)閉靜態(tài)提升。沒(méi)錯(cuò)。關(guān)閉后就不會(huì)出現(xiàn)靜態(tài)提升的數(shù)組,就不會(huì)有數(shù)組中的虛擬節(jié)點(diǎn)一直引用著el。
一開(kāi)始以為是element的鍋,經(jīng)過(guò)深層次分析,其實(shí)不是。這里只要換成任意靜態(tài)節(jié)點(diǎn)并開(kāi)啟靜態(tài)提升都會(huì)有這個(gè)問(wèn)題。
解決方案2
先來(lái)看看關(guān)閉靜態(tài)提升后,案例一和案例二編譯后的結(jié)果: 案例一:
const _Vue = Vue ? return function render(_ctx, _cache, $props, $setup, $data, $options) { ?with (_ctx) { ? ?const { createElementVNode: _createElementVNode, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue ? ? ?return (_openBlock(), _createElementBlock("div", null, [ ? ? ?_createElementVNode("button", { ? ? ? ?onClick: $event => (show=!show) ? ? }, "show", 8 /* PROPS */, ["onClick"]), ? ? ?show ? ? ? ?? (_openBlock(true), _createElementBlock(_Fragment, { key: 0 }, _renderList(3, (i) => { ? ? ? ? ? ?return (_openBlock(), _createElementBlock("div", null, [ ? ? ? ? ? ? ?_createElementVNode("span", null, "12"), ? ? ? ? ? ? ?_createElementVNode("span", null, "34") ? ? ? ? ? ])) ? ? ? ? }), 256 /* UNKEYED_FRAGMENT */)) ? ? ? : _createCommentVNode("v-if", true) ? ])) } }
案例二:
const _Vue = Vue ? return function render(_ctx, _cache, $props, $setup, $data, $options) { ?with (_ctx) { ? ?const { createElementVNode: _createElementVNode, renderList: _renderList, Fragment: _Fragment, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode } = _Vue ? ? ?return (_openBlock(), _createElementBlock("div", null, [ ? ? ?_createElementVNode("button", { ? ? ? ?onClick: $event => (show=!show) ? ? }, "show", 8 /* PROPS */, ["onClick"]), ? ? ?show ? ? ? ?? (_openBlock(), _createElementBlock("div", { key: 0 }, [ ? ? ? ? ? (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(3, (i) => { ? ? ? ? ? ? ?return (_openBlock(), _createElementBlock("div", null, [ ? ? ? ? ? ? ? ?_createElementVNode("span", null, "12"), ? ? ? ? ? ? ? ?_createElementVNode("span", null, "34") ? ? ? ? ? ? ])) ? ? ? ? ? }), 256 /* UNKEYED_FRAGMENT */)) ? ? ? ? ])) ? ? ? : _createCommentVNode("v-if", true) ? ])) } }
可以看到關(guān)鍵在于每次都會(huì)創(chuàng)建一個(gè)新的數(shù)組,這樣卸載之后,這個(gè)數(shù)組能被GC,自然就不會(huì)存在對(duì)el的引用,不會(huì)產(chǎn)生游離的節(jié)點(diǎn),自然就不會(huì)發(fā)生內(nèi)存泄漏。
所以,編寫(xiě)一個(gè)插件,對(duì)element的圖標(biāo)組件庫(kù)進(jìn)行改造。這里以vite為例。
import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; import fixHoistStatic from "./plugins/fix-hoist-static"; export default defineConfig(({ command, mode }) => { ?return { ? ?optimizeDeps: { ? ? ?exclude: ["@element-plus/icons-vue"], ? }, ? ?plugins: [ ? ? ?vue(), ? ? ?fixHoistStatic(), ? ], }; });
// plugins/fix-hoist-static const reg = /(_createElementBlock\d*("svg", _hoisted_\d+, )(_hoisted_\d+)/g; export default () => ({ ?name: "fix_hoistStatic", ?transform(code, id) { ? ?if (id.includes("@element-plus/icons-vue/dist/index.js")) { ? ? ?code = code.replace(reg, "$1[...$2]"); ? ? ?return code; ? } }, });
這里利用正則將數(shù)組替換成解構(gòu)的數(shù)組。
因?yàn)関ite會(huì)進(jìn)行依賴預(yù)構(gòu)建,所以開(kāi)發(fā)階段需要添加配置排除。詳情查看
編譯后
// src/components/add-location.vue var _hoisted_1 = { ?viewBox: "0 0 1024 1024", ?xmlns: "http://www.w3.org/2000/svg" }, _hoisted_2 = /* @__PURE__ */ _createElementVNode("path", { ?fill: "currentColor", ?d: "M288 896h448q32 0 32 32t-32 32H288q-32 0-32-32t32-32z" }, null, -1), _hoisted_3 = /* @__PURE__ */ _createElementVNode("path", { ?fill: "currentColor", ?d: "M800 416a288 288 0 1 0-576 0c0 118.144 94.528 272.128 288 456.576C705.472 688.128 800 534.144 800 416zM512 960C277.312 746.688 160 565.312 160 416a352 352 0 0 1 704 0c0 149.312-117.312 330.688-352 544z" }, null, -1), _hoisted_4 = /* @__PURE__ */ _createElementVNode("path", { ?fill: "currentColor", ?d: "M544 384h96a32 32 0 1 1 0 64h-96v96a32 32 0 0 1-64 0v-96h-96a32 32 0 0 1 0-64h96v-96a32 32 0 0 1 64 0v96z" }, null, -1), _hoisted_5 = [ ?_hoisted_2, ?_hoisted_3, ?_hoisted_4 ]; function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { ?return _openBlock(), _createElementBlock("svg", _hoisted_1, [..._hoisted_5]); }
打開(kāi)控制臺(tái)進(jìn)行測(cè)試,完美解決內(nèi)存泄漏!
以上就是解決vue3中內(nèi)存泄漏的問(wèn)題的詳細(xì)內(nèi)容,更多關(guān)于vue3內(nèi)存泄漏的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JS 實(shí)現(xiàn)獲取對(duì)象屬性個(gè)數(shù)的方法小結(jié)
這篇文章主要介紹了JS 實(shí)現(xiàn)獲取對(duì)象屬性個(gè)數(shù)的方法,結(jié)合實(shí)例形式總結(jié)分析了JS 獲取對(duì)象屬性個(gè)數(shù)的三種常用方法,需要的朋友可以參考下2023-05-05Vue Router之router.push和router.resolve頁(yè)面跳轉(zhuǎn)方式
這篇文章主要介紹了Vue Router之router.push和router.resolve頁(yè)面跳轉(zhuǎn)方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-04-04vue+jsplumb實(shí)現(xiàn)工作流程圖的項(xiàng)目實(shí)踐
本文主要介紹了vue+jsplumb實(shí)現(xiàn)工作流程圖的項(xiàng)目實(shí)踐,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04vue中axios防止多次觸發(fā)終止多次請(qǐng)求的示例代碼(防抖)
這篇文章主要介紹了vue中axios防止多次觸發(fā)終止多次請(qǐng)求的實(shí)現(xiàn)方法(防抖),本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2020-02-02Vue項(xiàng)目部署在Spring Boot出現(xiàn)頁(yè)面空白問(wèn)題的解決方案
這篇文章主要介紹了Vue項(xiàng)目部署在Spring Boot出現(xiàn)頁(yè)面空白問(wèn)題的解決方案,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-11-11Vue中v-form標(biāo)簽里的:rules作用及定義方法
文章介紹了在Vue項(xiàng)目中使用ElementUI或ElementPlus組件庫(kù)時(shí),如何通過(guò)`el-form`標(biāo)簽的`:rules`屬性進(jìn)行表單驗(yàn)證,`:rules`屬性用于定義表單項(xiàng)的驗(yàn)證規(guī)則,包括必填項(xiàng)、格式校驗(yàn)、長(zhǎng)度限制等,文章還展示了如何定義基本驗(yàn)證規(guī)則和自定義驗(yàn)證函數(shù),感興趣的朋友一起看看吧2025-03-03VUE使用router.push實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)和傳參方式
這篇文章主要介紹了VUE使用router.push實(shí)現(xiàn)頁(yè)面跳轉(zhuǎn)和傳參方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01vue3?+?antv/x6實(shí)現(xiàn)流程圖的全過(guò)程
隨著互聯(lián)網(wǎng)的發(fā)展,越來(lái)越多的應(yīng)用需要實(shí)現(xiàn)流程圖的制作,如工作流程圖、電路圖等,文中通過(guò)代碼以及圖文將實(shí)現(xiàn)的過(guò)程介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2024-06-06