时间轴分享展示

技术学习笔记

使用Vue3.2新指令v-memo提升性能(含源码浅析)

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情

前言

大家好,我是 Taylor,一个有趣且乐于分享的人,目前专注前端、Node.js技术栈分享,如果你对 前端、Node.js 学习感兴趣的话(后续有计划也可以),可以关注我掘金.

背景

看vue3.2的CHANGELOG 发现新增了一个指令 v-memo

image.png

一听这名字, 用过react的同学肯定想到了useMemo, 没错,这是一个性能优化的指令, 一起来学习一下吧。

v-memo定义

官方文档里是这样描述的:

缓存一个模板的子树。在元素和组件上都可以使用。为了实现缓存,该指令需要传入一个固定长度的依赖值数组进行比较。如果数组里的每个值都与最后一次的渲染相同,那么整个子树的更新将被跳过.

毕竟是官方文档,肯定不那么通俗易懂。

简单的说, 就是 v-memo中依赖的值若不发生变化,当前DOM及整个子树DOM都不会重新渲染, 会使用最后一次缓存的结果。

它存储先前渲染的结果以加速未来的渲染。

听到这, 心想, 呀 这不就是computed嘛, 我的理解, 干的事差不多,不过是一个缓存dom, 一个缓存数据。

事实上,v-memo 可以看作是模板部分的计算属性!

使用

v-memo接受一个数组

1、示例一 空的依赖项数组

如果传入一个空的依赖项数组, 他将永远不会重新渲染,永远使用第一次渲染的缓存结果, 和v-once类似

<template>  
    <div v-memo="[]">{{ data }}</div>
    <!-- 等同 -->
    <div v-once>{{ data }}</div>
/template>
复制代码

2、 示例二 控制更新、控制(大)DOM重新渲染时间

看接下来这个例子, 我设置了2个计数器, 但只有在被依赖的 subscribers 变化时, 子节点才会重新渲染。

代码

<script setup>
import { ref } from 'vue'
// v-memo依赖项
const subscribers = ref(10000);
// v-memo子节点依赖项
const inner = ref(500);
</script>
<template>
  <div>
    <p>视图状态</p>
    <div v-memo="[subscribers]">
      <p>Subscribers: {{ subscribers }}</p>
      <p>inner: {{ inner }}</p>
    </div>
    <button @click="subscribers++">v-memo依赖项(Subscribers)++</button>
    <!-- 这里改变内部依赖 视图不会更新 -->
    <button @click="inner++">子节点依赖项(inner)++</button>
    <div>
      <p>当前状态</p>
      <p>Subscribers: {{ subscribers }}</p>
      <p>inner: {{ inner }}</p>
    </div>
  </div>
</template>    
复制代码

如果我们修改了子节点依赖的inner,我们的 div 将不会重新渲染,但只要我们更新subscribers的值即会重新渲染, 发现没

我们可以控制dom的重新渲染

案例效果展示

v-memo.gif

结论

在某些情况,业务逻辑复杂的时候,手动控制整体的更新来提高性能。

如果我们需要准确控制大型组件的重新渲染时间,这将非常滴好用。

3、示例三 配合v-for优化列表

当搭配 v-for 使用 v-memo,确保两者都绑定在同一个元素上。v-memo 不能用在 v-for 内部。

image.png

我仅仅从渲染时间评判了一下。

关于子组件渲染的v-memo优化问题可以看这篇 learnvue.co/tutorials/v…

列表测试代码

官方文档说的过于简略, 于是我,完善了一个测试demo, 大家也可以复制去测试

方法及变量释意

  • selected 被选中的值,我们这里只做一个选中

  • list 列表数据,根据array from 造的数据

  • onClickSelect 点击选中方法, 选中数据、计算渲染时间

完整代码

当组件的 selected 状态改变,默认会重新创建大量的 vnode,尽管绝大部分都跟之前是一模一样的。v-memo 用在这里本质上是在说“只有当该项的被选中状态改变时才需要更新”。接下来我们来对比看看吧


<script setup>
import { ref } from 'vue'
   // 被选中的值
  const selected = ref(0);
  // 造的列表数据
  const list = ref(
    Array.from({ length: 1001 }, (_, index) => {
      return {
        id: index,
        name: `test${index}`,
      };
    }),
  );
  // 选中点击方法
  const onClickSelect = (id) => {
    selected.value = id;
    // 记录开始时间
    console.time()
    nextTick(()=> {
      // 记录结束时间
      console.timeEnd()
    })
  }
</script>
<template>
  <div>
    <div v-for="item in list" @click="onClickSelect(item.id)">
      <p>ID: {{ item.id }} - selected: {{ item.id === selected }}</p>
    </div>
  </div>
</template>
复制代码

长度1000的列表

造1000条数据

const list = ref(
Array.from({ length: 1000}, (_, index) => {
  return {
    id: index,
    name: `test${index}`,
  };
}),
);
复制代码

改变slected值触发更新

点击列表项

image.png

控制台查看渲染时间

加上 v-memo="[item.id == selected]"

image.png

不加

image.png 没有很大差别

长度2w的列表

1000的对比不出来,我们把数据量增加到两万看看

改变slected值触发更新

点击列表项

image.png

控制台查看渲染时间

加上 v-memo="[item.id == selected]"

平均DOM更新时间 30ms+

image.png

不加

平均DOM更新时间 55ms+

image.png 30ms和50多ms, 差了快一倍, 但2w数据...., 其实感觉差的也不多。

结论

确实如官方文档所说是用于微小优化, 长度1000的list经过我的测试, 渲染时间几乎没有太大差别。

本节实验可得: 用于性能至上场景中的微小优化

源码浅读

我们这里只看了运行时的代码

查找

因本人未系统了解vue3主仓库源码, 所以想到了从以下几个方面快速查找。

希望能给你带来一些日后找源码的思路。

vue/core主仓库COMMIT记录

发现了 尤大的一条 feat: v-memo 的提交记录

feat: v-memo commit链接

image.png

看新增和修改了那儿些文件, 发现 新增了 好几个memo相关的文件 image.png

这个tab先别关,打开一个新tab我们进行下一步

vue在线的模板编译工具

查看v-memo被编译成了什么样子

链接 https://template-explorer.vuejs.org

于是我在里面写了一个v-memo="[]", 发现vue编译成了如下。

import { createTextVNode as _createTextVNode, openBlock as _openBlock, createElementBlock as _createElementBlock, withMemo as _withMemo } from "vue"

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return _withMemo([], () => (_openBlock(), _createElementBlock("div", null, [
    _createTextVNode("Hello World!")
  ])), _cache, 0)
}
复制代码

查找关键词memo, 发现有一个_withMemo 的方法把生成dom的方法包裹了起来, 我分析

image.png

  • 包了一层, 首先这是面向切换的思想
  • 包了一层处理, 我猜测在这个_withMemo方法里,如果数据没变,就返回缓存,变了就生成一份并缓存, 再返回。

那我下一步就需要找这个withMemo方法看他怎么写的

withMemo方法

用第一步看的commit记录 很快定位到了runtime里的withMemo方法,

withMemo代码链接 github1s.com/zhangbowy/c…

运行时 withMemo方法解读

参数
  • memo v-memo里传的数据数组
  • render dom渲染方法
  • cache 缓存
  • index 下标
1、有缓存无数据变化、返回缓存
// 获取缓存
const cached = cache[index] as VNode | undefined
// 如果存在缓存或者数据没变
if (cached && isMemoSame(cached, memo)) {
// 返回缓存
    return cached
}
复制代码
2、执行渲染方法、缓存一份、然后返回

走到这一步说明没有命中缓存, 就生成一份、缓存下来, 然后返回,下次进来,如果数据没有变化,那就走第一步,直接返回缓存。

// 执行渲染方法生成DOM
const ret = render()
// 克隆一份依赖数据
// shallow clone
ret.memo = memo.slice()
// 返回DOM 并缓存。
return (cache[index] = ret)
复制代码
完整代码

withMemo方法完整代码及解读

// https://github1s.com/zhangbowy/core/blob/main/packages/runtime-core/src/helpers/withMemo.ts#L1-L39
import { hasChanged } from '@vue/shared'
import { currentBlock, isBlockTreeEnabled, VNode } from '../vnode'
export function withMemo(
  memo: any[],
  render: () => VNode<any, any>,
  cache: any[],
  index: number
) {
    // 获取缓存
    const cached = cache[index] as VNode | undefined
    // 如果存在缓存或者数据没变
    if (cached && isMemoSame(cached, memo)) {
    // 返回缓存
        return cached
    }
    // 执行渲染方法生成DOM
    const ret = render()
    // 克隆一份依赖数据
    // shallow clone
    ret.memo = memo.slice()
    // 返回DOM 并缓存。
    return (cache[index] = ret)
}
export function isMemoSame(cached: VNode, memo: any[]) {
    ...
}

复制代码

源码总结

1、v-memo代码被编译成了withMemo方法包起来.

v-memo在模版编译阶段被处理成了withMemo包了一层

模板转换的源码有兴趣的小伙伴可以自己去研究

template里书写

<div v-memo="[]">Hello World</div>
复制代码

模板编译后

export function render(_ctx, _cache, $props, $setup, $data, $options) {
  return _withMemo([], () => (_openBlock(), _createElementBlock("div", null, [
    _createTextVNode("Hello World!")
  ])), _cache, 0)
}
复制代码

2、withMemo方法

withmemo方法, 先是判断有缓存和数据无变化,就返回上一次生成的DOM

否则就执行渲染方法,生成一份,缓存后并返回。

总结

v-memo的使用总结如下:

  • 如果v-memo依赖一个空list, 不推荐使用.
  • 在某些情况,业务逻辑复杂的时候,手动控制整体的更新来提高性能。
  • 如果我们需要控制大型组件的重新渲染时间,这很有帮助。
  • 大list渲染优化

如果有问题, 欢迎大佬们评论区Q我!