持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第7天,点击查看活动详情
前言
大家好,我是 Taylor,一个有趣且乐于分享的人,目前专注前端、Node.js技术栈分享,如果你对 前端、Node.js 学习感兴趣的话(后续有计划也可以),可以关注我掘金.
背景
看vue3.2的CHANGELOG 发现新增了一个指令 v-memo
一听这名字, 用过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的重新渲染
案例效果展示
结论
在某些情况,业务逻辑复杂的时候,手动控制整体的更新来提高性能。
如果我们需要准确控制大型组件的重新渲染时间,这将非常滴好用。
3、示例三 配合v-for优化列表
当搭配
v-for
使用v-memo
,确保两者都绑定在同一个元素上。v-memo
不能用在v-for
内部。
我仅仅从渲染时间评判了一下。
关于子组件渲染的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值触发更新
点击列表项
控制台查看渲染时间
加上 v-memo="[item.id == selected]"
不加
没有很大差别
长度2w的列表
1000的对比不出来,我们把数据量增加到两万看看
改变slected值触发更新
点击列表项
控制台查看渲染时间
加上 v-memo="[item.id == selected]"
平均DOM更新时间 30ms+
不加
平均DOM更新时间 55ms+
30ms和50多ms, 差了快一倍, 但2w数据...., 其实感觉差的也不多。
结论
确实如官方文档所说是用于微小优化, 长度1000的list经过我的测试, 渲染时间几乎没有太大差别。
本节实验可得: 用于性能至上场景中的微小优化
源码浅读
我们这里只看了运行时的代码
查找
因本人未系统了解vue3主仓库源码, 所以想到了从以下几个方面快速查找。
希望能给你带来一些日后找源码的思路。
vue/core主仓库COMMIT记录
发现了 尤大的一条 feat: v-memo
的提交记录
看新增和修改了那儿些文件, 发现 新增了 好几个memo相关的文件
这个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的方法包裹了起来, 我分析
- 包了一层, 首先这是
面向切换的思想
- 包了一层处理, 我猜测在这个_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我!