解決vue3中內(nèi)存泄漏的問題
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)存泄漏,本文將通過幾個(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)不需要管。
先來看一下這個(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一開始屬性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í)沒有傳入第四個(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ì)問:還有一個(gè)游離的div和兩個(gè)游離的text節(jié)點(diǎn)哪里來的呢?
不要忘記了,el中也會(huì)保持對(duì)父親和兒子的引用。詳情見下圖


每一個(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的過程大差不差,只需要知道掛載完成后,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í)沒有傳入第四個(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方法。所以到這里,依舊沒有到案例一的循環(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)在來思考一下,游離的節(jié)點(diǎn)是從哪里來的?
同樣的,_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í)沒有進(jìn)入到
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中的兩個(gè)虛擬節(jié)點(diǎn)中保存的el,其parentNode是div,而div中又保持著兄弟節(jié)點(diǎn)(因?yàn)闆]有顯示地刪除,所以會(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)。
通過案例引出解決方案
可以看到一個(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ā)我寫出這篇文章。
好了,為什么使用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)存泄漏!
解決方案一在開頭就有,就是關(guān)閉靜態(tài)提升。沒錯(cuò)。關(guān)閉后就不會(huì)出現(xiàn)靜態(tài)提升的數(shù)組,就不會(huì)有數(shù)組中的虛擬節(jié)點(diǎn)一直引用著el。
一開始以為是element的鍋,經(jīng)過深層次分析,其實(shí)不是。這里只要換成任意靜態(tài)節(jié)點(diǎn)并開啟靜態(tài)提升都會(huì)有這個(gè)問題。
解決方案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)鍵在于每次都會(huì)創(chuàng)建一個(gè)新的數(shù)組,這樣卸載之后,這個(gè)數(shù)組能被GC,自然就不會(huì)存在對(duì)el的引用,不會(huì)產(chǎn)生游離的節(jié)點(diǎn),自然就不會(huì)發(fā)生內(nèi)存泄漏。
所以,編寫一個(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)建,所以開發(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]);
}打開控制臺(tái)進(jìn)行測(cè)試,完美解決內(nèi)存泄漏!
以上就是解決vue3中內(nèi)存泄漏的問題的詳細(xì)內(nèi)容,更多關(guān)于vue3內(nèi)存泄漏的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- 一文詳解Vue中內(nèi)存泄漏的場(chǎng)景與預(yù)防技巧
- Vue內(nèi)存泄漏的識(shí)別和解決方案
- 在Vue開發(fā)過程中解決和預(yù)防內(nèi)存泄漏問題的方法詳解
- Vue實(shí)現(xiàn)定位并解決內(nèi)存泄漏
- 解決Vue使用百度地圖BMapGL內(nèi)存泄漏問題?Out?of?Memory
- vue中的eventBus會(huì)不會(huì)產(chǎn)生內(nèi)存泄漏你知道嗎
- Vue優(yōu)化:常見會(huì)導(dǎo)致內(nèi)存泄漏問題及優(yōu)化詳解
- 解決vue自定義指令導(dǎo)致的內(nèi)存泄漏問題
- 詳解Vue如何避免內(nèi)存泄漏
相關(guān)文章
vue 中使用 watch 出現(xiàn)了如下的報(bào)錯(cuò)的原因分析
這篇文章主要介紹了vue 中使用 watch 出現(xiàn)了如下的報(bào)錯(cuò)信息的原因分析及解決方法,本文附有代碼解決方案,非常不錯(cuò),需要的朋友可以參考下2019-05-05
vite.config.ts如何加載.env環(huán)境變量
這篇文章主要介紹了vite.config.ts加載.env環(huán)境變量方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-10-10
關(guān)于Element-ui中el-table出現(xiàn)的表格錯(cuò)位問題解決
使用ElementUI的el-table后,偶然發(fā)現(xiàn)出現(xiàn)行列錯(cuò)位、對(duì)不齊問題,下面這篇文章主要給大家介紹了關(guān)于Element-ui中el-table出現(xiàn)的表格錯(cuò)位問題解決的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2022-07-07
解決VUE項(xiàng)目在IIS部署出現(xiàn):Uncaught SyntaxError: Unexpected&n
這篇文章介紹了解決VUE項(xiàng)目在IIS部署出現(xiàn):Uncaught SyntaxError: Unexpected token < 報(bào)錯(cuò)的方法,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
Vue.js實(shí)現(xiàn)數(shù)據(jù)響應(yīng)的方法
這篇文章主要介紹了Vue.js實(shí)現(xiàn)數(shù)據(jù)響應(yīng)的方法,本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2018-08-08
解決Vue的文本編輯器 vue-quill-editor 小圖標(biāo)樣式排布錯(cuò)亂問題
這篇文章主要介紹了解決Vue的文本編輯器 vue-quill-editor 小圖標(biāo)樣式排布錯(cuò)亂問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2020-08-08
Vue實(shí)現(xiàn)簡(jiǎn)易購(gòu)物車案例
這篇文章主要為大家詳細(xì)介紹了Vue實(shí)現(xiàn)簡(jiǎn)易購(gòu)物車案例,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-06-06
關(guān)于vue設(shè)置環(huán)境變量全流程
這篇文章主要介紹了關(guān)于vue設(shè)置環(huán)境變量全流程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-03-03

