解決vue3中內(nèi)存泄漏的問題
vue3的內(nèi)存泄漏
在項目中會發(fā)現(xiàn)一個奇怪的現(xiàn)象,當(dāng)使用element-plus中的圖標(biāo)組件時會出現(xiàn)內(nèi)存泄漏。詳情查看
解決方案1:關(guān)閉靜態(tài)提升。詳情查看
解決方案2:參考本文
至于為什么靜態(tài)提升會導(dǎo)致內(nèi)存泄漏,本文將通過幾個案例的源碼分析詳細(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>
點擊按鈕前:游離節(jié)點只有一個,這是熱更新導(dǎo)致的,不需要管。
點擊兩次按鈕后:
對比可以發(fā)現(xiàn)多出了兩個span和和一個div和兩個text的游離節(jié)點,最下面的注釋節(jié)點不需要管。
先來看一下這個模板編譯后的結(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é)點。
掛載階段
掛載第一個div的子節(jié)點時:
此時children
中兩個節(jié)點分別指向_hoisted_2
和 _hoisted_3
循環(huán)遍歷children時,會走cloneIfMounted
。
export function cloneIfMounted(child: VNode): VNode { ?return (child.el === null && child.patchFlag !== PatchFlags.HOISTED) || ? ?child.memo ? ?? child ? : cloneVNode(child) }
而_hoisted_2
和 _hoisted_3
一開始屬性el為null但patchFlag使HOISTED,所以會走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 }
克隆時會將被克隆節(jié)點的el賦值給新的節(jié)點。
回到循環(huán)體,可以看到將克隆后的節(jié)點重新賦值給了children即_hoisted_4
[i],此時_hoisted_4
中的內(nèi)容不再指向_hoisted_2
和_hoisted_3
,而是克隆后的節(jié)點。_hoisted_2
和_hoisted_3
就此完全脫離了關(guān)系。這是一個疑點,每次都需要克隆,不懂這樣靜態(tài)的提升的意義在哪里。
后續(xù)div子節(jié)點的掛載都會走這個循環(huán),每次循環(huán)都會克隆節(jié)點并重新賦值給children即_hoisted_4
[i]。
到此,掛載完成。
可想而知,掛載完成后,children即_hoisted_4
中的內(nèi)容是最后一個div的兩個虛擬子節(jié)點。
卸載階段
這里卸載的虛擬節(jié)點的type是Symbol(v-fgt)
這是vue處理<template v-if>
標(biāo)簽時創(chuàng)建的虛擬節(jié)點,這里需要關(guān)注的是unmount
方法的第四個參數(shù)doRemove
,傳入了true。
type UnmountFn = ( ?vnode: VNode, ?parentComponent: ComponentInternalInstance | null, ?parentSuspense: SuspenseBoundary | null, ?doRemove?: boolean, ?optimized?: boolean ) => void const unmount: UnmountFn
進入unmount函數(shù),會走到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ù),默認(rèn)是false。然后會遞歸調(diào)用unmount
方法。
注意,此時傳入的doRemove
是false。
循環(huán)調(diào)用unmount
傳入div的虛擬節(jié)點
此時走到unmount
方法中的這個分支
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
不會發(fā)生什么。
繼續(xù)往下走,unmount
方法中的最后有
if (doRemove) { ?remove(vnode) }
此時doRemove
為false,不會調(diào)用remove
方法。
處理完三個div的節(jié)點后,函數(shù)回到上一層。接著處理type是Symbol(v-fgt)
的虛擬節(jié)點。而此時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() } }
會走到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) }
從這里可以看到,會依次刪除掉3個div的真實dom。
到此,整個<template v-if>
卸載完成。
那到底內(nèi)存泄漏在哪里?
還記得_hoissted_4
保存的是最后一個虛擬div節(jié)點的兩個虛擬span節(jié)點,而節(jié)點中的el屬性依然維持著真實節(jié)點的引用,不會被GC,
所以這就造成了內(nèi)存泄漏。這里就解釋了那兩個游離的span節(jié)點。
好奇的你一定會問:還有一個游離的div和兩個游離的text節(jié)點哪里來的呢?
不要忘記了,el中也會保持對父親和兒子的引用。詳情見下圖
每一個span都有一個text兒子,共用一個div父節(jié)點,完美解釋了前面提到的所有游離節(jié)點。
案例2
將案例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> ? ? ? ? ? ?<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>
點擊按鈕前:游離節(jié)點只有一個,這是熱更新導(dǎo)致的,不需要管。
點擊兩次按鈕后:
你會震驚地發(fā)現(xiàn),我就改變了一個標(biāo)簽,泄漏的節(jié)點竟然多了這么多。 對比可以發(fā)現(xiàn)多出了六個span和和四個div和八個text的游離節(jié)點,最下面的注釋節(jié)點不需要管。
同樣查看編譯后的結(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é)點
掛載階段
掛載和案例1的過程大差不差,只需要知道掛載完成后,children即_hoisted_5
中的內(nèi)容是最后一個div的兩個虛擬子節(jié)點。
卸載階段
這里卸載的虛擬節(jié)點的type是div
,這是<div v-if>
的虛擬節(jié)點,這里需要關(guān)注的是unmount
方法的第四個參數(shù)doRemove
,傳入了true。
type UnmountFn = ( ?vnode: VNode, ?parentComponent: ComponentInternalInstance | null, ?parentSuspense: SuspenseBoundary | null, ?doRemove?: boolean, ?optimized?: boolean ) => void const unmount: UnmountFn
進入unmount函數(shù),會走到這個分支。
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
是長度為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
會走這個分支
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ù),默認(rèn)是false。然后會遞歸調(diào)用unmount
方法。
處理3個div時與案例一一樣,接著回到處理Framgent。
繼續(xù)往下走,unmount
方法中的最后有
if (doRemove) { ?remove(vnode) }
此時doRemove
為false,不會調(diào)用remove
方法。所以到這里,依舊沒有到案例一的循環(huán)刪除真實節(jié)點的環(huán)節(jié)。接著往下看。
處理完Framgent
,再回到<div v-if>
對應(yīng)的虛擬節(jié)點的那一層。
if (doRemove) { ?remove(vnode) }
此時doRemove為true,進入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
,此時<div v-if>
這個節(jié)點從文檔中移除,那么它所包含的所有子節(jié)點也移除了。
此時卸載階段完成。
現(xiàn)在來思考一下,游離的節(jié)點是從哪里來的?
同樣的,_hoisted_5
中的兩個虛擬節(jié)點中還各自保存著相應(yīng)的el 即span。那么根據(jù)其引用父親和兒子將全部不能被GC,所以變成了游離的節(jié)點。
這時的你會有所疑惑,為什么同樣是維持著父子關(guān)系,案例一種游離的只有2個span,2個text和一個div,而這卻多了這么多。
進入解答環(huán)節(jié):
對比可以發(fā)現(xiàn),關(guān)鍵點在于方案二中處理Fragment
時沒有進入到
if (doRemove) { ?remove(vnode) }
也就沒有到
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
中的兩個虛擬節(jié)點中保存的el,其parentNode是div,而div中又保持著兄弟節(jié)點(因為沒有顯示地刪除,所以會繼續(xù)存在),即剩余的兩個div以及它的父節(jié)點即<div v-if>
,而各自又保存著各自的兒子。
離的個數(shù):
Span: 3 * 2 =6
Div: 3 + 1 = 4
Text: 3*2 = 6
等等,text節(jié)點不是有8個嗎,還有兩個在哪里?
這就要提到vue處理Fragment
的時候做的處理了。
可以看到,處理Fragment
時會創(chuàng)建兩個空節(jié)點,作為子節(jié)點插入的錨點。所以上下會多了兩個文本節(jié)點。如下圖。
所以最終的游離個數(shù):
Span: 3 * 2 =6
Div: 3 + 1 = 4
Text: 3*2+2 = 8
到此,完美解釋了所有的游離節(jié)點。
通過案例引出解決方案
可以看到一個標(biāo)簽的改變,直接改變了游離節(jié)點的個數(shù),設(shè)想一下,這是一個表格,而且里面包含靜態(tài)提升的節(jié)點,那么整個表格將會變成游離節(jié)點,發(fā)生內(nèi)存泄漏,這是我在項目中的親身經(jīng)歷,才會引發(fā)我寫出這篇文章。
好了,為什么使用element的圖標(biāo)庫會造成內(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)提升!所以只要使用了就會造成內(nèi)存泄漏!
解決方案一在開頭就有,就是關(guān)閉靜態(tài)提升。沒錯。關(guān)閉后就不會出現(xiàn)靜態(tài)提升的數(shù)組,就不會有數(shù)組中的虛擬節(jié)點一直引用著el。
一開始以為是element的鍋,經(jīng)過深層次分析,其實不是。這里只要換成任意靜態(tài)節(jié)點并開啟靜態(tài)提升都會有這個問題。
解決方案2
先來看看關(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)鍵在于每次都會創(chuàng)建一個新的數(shù)組,這樣卸載之后,這個數(shù)組能被GC,自然就不會存在對el的引用,不會產(chǎn)生游離的節(jié)點,自然就不會發(fā)生內(nèi)存泄漏。
所以,編寫一個插件,對element的圖標(biāo)組件庫進行改造。這里以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ù)組。
因為vite會進行依賴預(yù)構(gòu)建,所以開發(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]); }
打開控制臺進行測試,完美解決內(nèi)存泄漏!
以上就是解決vue3中內(nèi)存泄漏的問題的詳細(xì)內(nèi)容,更多關(guān)于vue3內(nèi)存泄漏的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JS 實現(xiàn)獲取對象屬性個數(shù)的方法小結(jié)
這篇文章主要介紹了JS 實現(xiàn)獲取對象屬性個數(shù)的方法,結(jié)合實例形式總結(jié)分析了JS 獲取對象屬性個數(shù)的三種常用方法,需要的朋友可以參考下2023-05-05Vue Router之router.push和router.resolve頁面跳轉(zhuǎn)方式
這篇文章主要介紹了Vue Router之router.push和router.resolve頁面跳轉(zhuǎn)方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2025-04-04vue中axios防止多次觸發(fā)終止多次請求的示例代碼(防抖)
這篇文章主要介紹了vue中axios防止多次觸發(fā)終止多次請求的實現(xiàn)方法(防抖),本文通過實例代碼給大家介紹的非常詳細(xì),具有一定的參考借鑒價值,需要的朋友可以參考下2020-02-02Vue項目部署在Spring Boot出現(xiàn)頁面空白問題的解決方案
這篇文章主要介紹了Vue項目部署在Spring Boot出現(xiàn)頁面空白問題的解決方案,非常不錯,具有一定的參考借鑒價值,需要的朋友可以參考下2018-11-11Vue中v-form標(biāo)簽里的:rules作用及定義方法
文章介紹了在Vue項目中使用ElementUI或ElementPlus組件庫時,如何通過`el-form`標(biāo)簽的`:rules`屬性進行表單驗證,`:rules`屬性用于定義表單項的驗證規(guī)則,包括必填項、格式校驗、長度限制等,文章還展示了如何定義基本驗證規(guī)則和自定義驗證函數(shù),感興趣的朋友一起看看吧2025-03-03VUE使用router.push實現(xiàn)頁面跳轉(zhuǎn)和傳參方式
這篇文章主要介紹了VUE使用router.push實現(xiàn)頁面跳轉(zhuǎn)和傳參方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-01-01