如何解决vue3中的内存泄漏?

vue3的内存泄漏

在项目中会发现一个奇怪的现象,当使用element-plus中的图标组件时会出现内存泄漏。详情查看

解决方案1:关闭静态提升。详情查看

解决方案2:文末附上

至于为什么静态提升会导致内存泄漏,本文将通过几个案例的源码分析详细讲解。

案例1

html 复制代码
 <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>

点击按钮前:游离节点只有一个,这是热更新导致的,不需要管。

点击两次按钮后:

对比可以发现多出了两个span和和一个div和两个text的游离节点,最下面的注释节点不需要管。

先来看一下这个模板编译后的结果:

ts 复制代码
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)
        ]))
      }
    }

这里关注_hoisted_4静态节点。

挂载阶段

挂载第一个div的子节点时:

此时children中两个节点分别指向_hoisted_2_hoisted_3

循环遍历children时,会走cloneIfMounted

ts 复制代码
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

ts 复制代码
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
    }

克隆时会将被克隆节点的el赋值给新的节点。

回到循环体,可以看到将克隆后的节点重新赋值给了children即_hoisted_4[i],此时_hoisted_4 中的内容不再指向_hoisted_2_hoisted_3,而是克隆后的节点。_hoisted_2_hoisted_3就此完全脱离了关系。这是一个疑点,每次都需要克隆,不懂这样静态的提升的意义在哪里。

后续div子节点的挂载都会走这个循环,每次循环都会克隆节点并重新赋值给children即_hoisted_4[i]。

到此,挂载完成。

可想而知,挂载完成后,children即_hoisted_4中的内容是最后一个div的两个虚拟子节点。

卸载阶段

这里卸载的虚拟节点的type是Symbol(v-fgt)这是vue处理<template v-if>标签时创建的虚拟节点,这里需要关注的是unmount方法的第四个参数doRemove,传入了true。

ts 复制代码
 type UnmountFn = (
      vnode: VNode,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      doRemove?: boolean,
      optimized?: boolean
    ) => void
    const unmount: UnmountFn

进入unmount函数,会走到type===Fragment的分支。

ts 复制代码
else if (
      (type === Fragment &&
       patchFlag &
       (PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) ||
      (!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN)
    ) {
      unmountChildren(children as VNode[], parentComponent, parentSuspense)
    }

调用unmountChildren方法。

ts 复制代码
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)
    }
}

调用时没有传入第四个参数,默认是false。然后会递归调用unmount方法。

注意,此时传入的doRemove是false。

循环调用unmount传入div的虚拟节点

此时走到unmount方法中的这个分支

ts 复制代码
 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是空数组,所以unmountChildren不会发生什么。

继续往下走,unmount方法中的最后有

ts 复制代码
if (doRemove) {
     remove(vnode)
}

此时doRemove为false,不会调用remove方法。

处理完三个div的节点后,函数回到上一层。接着处理type是Symbol(v-fgt)的虚拟节点。而此时doRemove为true,调用remove方法。

ts 复制代码
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方法。

ts 复制代码
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>卸载完成。

那到底内存泄漏在哪里?

还记得_hoissted_4保存的是最后一个虚拟div节点的两个虚拟span节点,而节点中的el属性依然维持着真实节点的引用,不会被GC,

所以这就造成了内存泄漏。这里就解释了那两个游离的span节点。

好奇的你一定会问:还有一个游离的div和两个游离的text节点哪里来的呢?

不要忘记了,el中也会保持对父亲和儿子的引用。详情见下图

每一个span都有一个text儿子,共用一个div父节点,完美解释了前面提到的所有游离节点。

案例2

将案例1的代码稍稍做下改动。

ts 复制代码
<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>

点击按钮前:游离节点只有一个,这是热更新导致的,不需要管。

点击两次按钮后:

你会震惊地发现,我就改变了一个标签,泄漏的节点竟然多了这么多。 对比可以发现多出了六个span和和四个div和八个text的游离节点,最下面的注释节点不需要管。

同样查看编译后的结果

ts 复制代码
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)
        ]))
      }
    }

这里关注的是_hoisted_5节点

挂载阶段

挂载和案例1的过程大差不差,只需要知道挂载完成后,children即_hoisted_5中的内容是最后一个div的两个虚拟子节点。

卸载阶段

这里卸载的虚拟节点的type是div,这是<div v-if>的虚拟节点,这里需要关注的是unmount方法的第四个参数doRemove,传入了true。

ts 复制代码
 type UnmountFn = (
      vnode: VNode,
      parentComponent: ComponentInternalInstance | null,
      parentSuspense: SuspenseBoundary | null,
      doRemove?: boolean,
      optimized?: boolean
    ) => void
    const unmount: UnmountFn

进入unmount函数,会走到这个分支。

go 复制代码
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的数组,保存着:

注意,这里传入的 doRemove参数是false,这是和案例一同样是卸载Fragment的重大区别。

ts 复制代码
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)
        }
      }

接着调用unmount方法,传入的doRemove是false。

新的unmount会走这个分支

ts 复制代码
else if (
      (type === Fragment &&
       patchFlag &
       (PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT)) ||
      (!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN)
    ) {
      unmountChildren(children as VNode[], parentComponent, parentSuspense)
    }

调用时没有传入第四个参数,默认是false。然后会递归调用unmount方法。

处理3个div时与案例一一样,接着回到处理Framgent。

继续往下走,unmount方法中的最后有

ts 复制代码
 if (doRemove) {
      remove(vnode)
    }

此时doRemove为false,不会调用remove方法。所以到这里,依旧没有到案例一的循环删除真实节点的环节。接着往下看。

处理完Framgent,再回到<div v-if>对应的虚拟节点的那一层。

ts 复制代码
 if (doRemove) {
      remove(vnode)
    }

此时doRemove为true,进入remove函数。

ts 复制代码
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()
     }
   }

调用performRemove,此时<div v-if>这个节点从文档中移除,那么它所包含的所有子节点也移除了。

此时卸载阶段完成。

现在来思考一下,游离的节点是从哪里来的?

同样的,_hoisted_5中的两个虚拟节点中还各自保存着相应的el 即span。那么根据其引用父亲和儿子将全部不能被GC,所以变成了游离的节点。

这时的你会有所疑惑,为什么同样是维持着父子关系,案例一种游离的只有2个span,2个text和一个div,而这却多了这么多。

进入解答环节:

对比可以发现,关键点在于方案二中处理Fragment时没有进入到

ts 复制代码
 if (doRemove) {
      remove(vnode)
    }

也就没有到

ts 复制代码
 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中的两个虚拟节点中保存的el,其parentNode是div,而div中又保持着兄弟节点(因为没有显示地删除,所以会继续存在),即剩余的两个div以及它的父节点即<div v-if>,而各自又保存着各自的儿子。

所以游离的个数:

Span: 3 * 2 =6

Div: 3 + 1 = 4

Text: 3*2 = 6

等等,text节点不是有8个吗,还有两个在哪里?

这就要提到vue处理Fragment的时候做的处理了。

可以看到,处理Fragment时会创建两个空节点,作为子节点插入的锚点。所以上下会多了两个文本节点。如下图。

所以最终的游离个数:

Span: 3 * 2 =6

Div: 3 + 1 = 4

Text: 3*2+2 = 8

到此,完美解释了所有的游离节点。

通过案例引出解决方案

可以看到一个标签的改变,直接改变了游离节点的个数,设想一下,这是一个表格,而且里面包含静态提升的节点,那么整个表格将会变成游离节点,发生内存泄漏,这是我在项目中的亲身经历,才会引发我写出这篇文章。

好了,为什么使用element的图标库会造成内存泄漏?

看看源码:

ts 复制代码
// 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);
    }

这是什么?天,静态提升!所以只要使用了就会造成内存泄漏!

解决方案一在开头就有,就是关闭静态提升。没错。关闭后就不会出现静态提升的数组,就不会有数组中的虚拟节点一直引用着el。

一开始以为是element的锅,经过深层次分析,其实不是。这里只要换成任意静态节点并开启静态提升都会有这个问题。

解决方案2

先来看看关闭静态提升后,案例一和案例二编译后的结果: 案例一:

ts 复制代码
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)
        ]))
      }
    }

案例二:

ts 复制代码
    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)
        ]))
      }
    }

可以看到关键在于每次都会创建一个新的数组,这样卸载之后,这个数组能被GC,自然就不会存在对el的引用,不会产生游离的节点,自然就不会发生内存泄漏。

所以,编写一个插件,对element的图标组件库进行改造。这里以vite为例。

ts 复制代码
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(),
    ],
  };
});
ts 复制代码
    // 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;
        }
      },
    });

这里利用正则将数组替换成解构的数组。

因为vite会进行依赖预构建,所以开发阶段需要添加配置排除。详情查看

编译后

ts 复制代码
 // 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]);
    }

打开控制台进行测试,完美解决内存泄漏!

相关推荐
web130933203982 小时前
vue elementUI form组件动态添加el-form-item并且动态添加rules必填项校验方法
前端·vue.js·elementui
supermapsupport4 小时前
iClient3D for Cesium在Vue中快速实现场景卷帘
前端·vue.js·3d·cesium·supermap
m0_748254884 小时前
vue+elementui实现下拉表格多选+搜索+分页+回显+全选2.0
前端·vue.js·elementui
苹果醋35 小时前
Golang的文件加密工具
运维·vue.js·spring boot·nginx·课程设计
关你西红柿子6 小时前
小程序app封装公用顶部筛选区uv-drop-down
前端·javascript·vue.js·小程序·uv
济南小草根6 小时前
把一个Vue项目的页面打包后再另一个项目中使用
前端·javascript·vue.js
m0_748256567 小时前
Vue - axios的使用
前端·javascript·vue.js
慢知行7 小时前
Vite 构建 Vue3 组件库之路:工程基础搭建与目录结构优化
前端·vue.js
阿克苏的滚滚馕8 小时前
alioss 批量断点续传 开箱即用
javascript·vue.js
Simaoya8 小时前
【vue】圆环呼吸灯闪烁效果(模拟扭蛋机出口处灯光)
javascript·css·vue.js