前言:
我们在页面渲染的时候经常会遇到数据处理的需求,而每次props和state的变动都会引起页面重新渲染,数据处理需要重新执行。为了减少重复的数据处理,我们可以引入记忆性技术Memoize解决这种问题。
简介:
Memoize的基本处理原理是,通过判断函数上一次的传入参数和新一次的传入参数是否一致,去决定是否执行函数。如果前后两次传参一样,返回之前的缓存结果,否则则重新运行函数得到新的运算结果。这样做可以减少相同传参的数据处理,减少页面运算量,提高性能。
以下是一些比较常见的Memoize工具库:
MemoizeOne:www.npmjs.com/package/mem… Memoizee:www.npmjs.com/package/mem… Lodash: www.lodashjs.com/docs/latest… 以及性能比较结果:
单个入参:www.measurethat.net/Benchmarks/… 多个入参:www.measurethat.net/Benchmarks/…
MemoizeOne:
下面主要介绍一下MemoizeOne的原理和使用。
顾名思义,这个库对每一个实例都只缓存一个结果。先看官方给的例子:
import memoizeOne from 'memoize-one';
const add = (a, b) => a + b;
const memoizedAdd = memoizeOne(add);
memoizedAdd(1, 2); // 3
memoizedAdd(1, 2); // 3
// Add function is not executed: previous result is returned
memoizedAdd(2, 3); // 5
// Add function is called to get new value
memoizedAdd(2, 3); // 5
// Add function is not executed: previous result is returned
memoizedAdd(1, 2); // 3
// Add function is called to get new value.
// While this was previously cached,
// it is not the latest so the cached result is lost
从例子看出来,当传入参数完全一致的时候,add 函数是不会被调用的,memoizeAdd 函数直接返回上一次缓存的结果。而当传入参数改变了,则就重新运行 add 函数。
这里我们可以发现一个问题,入参的比对是如何处理的?例子中使用的参数都是字符串,使用了全等(===)对比,那入参是对象或者数组,就不能通过这个方法去对比了。对此,MemoizeOne提供了自定义匹配方法的配置。我们可以试试下面的用法:
import memoizeOne from 'memoize-one';
import { isEqual } from 'lodash';
const func= (list) => list.filter(item => !item.hidden);
const memoizedFilterList = memoizeOne(func, isEqual);
// 使用lodash的isEqual方法对传入参数进行深比较
const func= (id, name) => `${id}-${name}`;
const compare = (prevArgs, nextArgs) => return prevArgs[0] === nextArgs[0]
const memoizedFilterList = memoizeOne(func, compare);
// 使用自定义方法对传入参数进行深比较
// prevArgs和nextArgs对应原来的入参数组和新的入参数组,可以按自己的需要去定制比较方法
// 上例子中只比较传入id的值,id变动时才重新调用函数,name不作为判断条件
还有一点需要注意的是,this 也是作为需要比对的环境变量默认进行比对的,如果 this 的指向变了,memoize是不会使用上一次的缓存结果的,可以参照下面的例子:
function getA() {
return this.a;
}
const temp1 = {
a: 20,
};
const temp2 = {
a: 30,
};
getA.call(temp1); // 20
getA.call(temp2); // 30
源码解析: memoizeOne是一个非常轻量的库,主要实现代码如下所示:
// areInputsEqual
// 默认使用的比较函数
export default function areInputsEqual(
newInputs: readonly unknown[],
lastInputs: readonly unknown[],
): boolean {
// 先比较入参数组的长度,如果不一样则不进行下一步对比,直接返回不同
if (newInputs.length !== lastInputs.length) {
return false;
}
// 如果入参数组长度相同,则使用 !== 比较里面每个参数是否相同
for (let i = 0; i < newInputs.length; i++) {
// using shallow equality check
if (newInputs[i] !== lastInputs[i]) {
return false;
}
}
return true;
}
import areInputsEqual from './are-inputs-equal';
// Using ReadonlyArray<T> rather than readonly T as it works with TS v3
export type EqualityFn = (newArgs: any[], lastArgs: any[]) => boolean;
export default function memoizeOne<
// Need to use 'any' rather than 'unknown' here as it has
// The correct Generic narrowing behaviour.
ResultFn extends (this: any, ...newArgs: any[]) => ReturnType<ResultFn>
>(resultFn: ResultFn, isEqual: EqualityFn = areInputsEqual): ResultFn {
let lastThis: unknown;
let lastArgs: unknown[] = [];
let lastResult: ReturnType<ResultFn>;
let calledOnce: boolean = false;
// breaking cache when context (this) or arguments change
function memoized(this: unknown, ...newArgs: unknown[]): ReturnType<ResultFn> {
// 如果之前已经运行过这个函数,并且环境变量this没有改变,入参也相同,则直接返回上次结果
if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) {
return lastResult;
}
// 否则则运行函数,并记录此次运行的this、入参和结果
// Throwing during an assignment aborts the assignment: https://codepen.io/alexreardon/pen/RYKoaz
// Doing the lastResult assignment first so that if it throws
// nothing will be overwritten
lastResult = resultFn.apply(this, newArgs);
calledOnce = true;
lastThis = this;
lastArgs = newArgs;
return lastResult;
}
return memoized as ResultFn;
}
实战: Memoize记忆性技术可以有效帮我们提高程序的性能,但这其实也是一个以空间换取速度的方法,并不是所有情况都适用。一般来说,涉及大量计算的纯函数、递归函数、图表数据处理等就可以有效利用这个技术;而当函数输出结果并不完全依靠入参,或者入参变动非常频繁,等等的一些情况可能就不太适合使用了。
在我们当前的后台项目中,框架里有一些地方是有使用MemoizeOne去做处理的,例如menu.js里面就有使用。菜单在我们进入后台之后变化是比较少的,但是经常会重新渲染,此时就适合使用这个方法去提高渲染性能:
import memoizeOne from 'memoize-one';
import { isEqual } from 'lodash';
/**
* 格式化菜单
*/
function formatter(data, parentAuthority, parentName) {
return data
.map(item => {
if (!item.name || !item.path) {
return null;
}
// 如下是 菜单多语言设置
let locale = 'menu';
if (parentName) {
locale = `${parentName}.${item.name}`;
} else{
locale = `menu.${item.name}`;
}
// if enableMenuLocale use item.name,close menu international
const name = menu.disableLocal
? item.name
: formatMessage({ id: locale, defaultMessage: item.name });
const result = {
...item,
name,
locale,
authority: item.authority || parentAuthority,
};
if (item.routes) {
const children = formatter(item.routes, item.authority, locale);
// Reduce memory usage
result.children = children;
}
delete result.routes;
return result;
})
.filter(item => item);
}
const memoizeOneFormatter = memoizeOne(formatter, isEqual);
/**
* 获取面包屑映射
*/
const getBreadcrumbNameMap = menuData => {
const routerMap = {};
const flattenMenuData = data => {
data.forEach(menuItem => {
if (menuItem.children) {
flattenMenuData(menuItem.children);
}
// Reduce memory usage
routerMap[menuItem.path] = menuItem;
});
};
flattenMenuData(menuData);
return routerMap;
};
const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual);
总结:
Memoize技术可以有效帮我们解决日常开发里的性能问题,我们在开发过程中也可以思考一下,有什么地方,可以用什么方法去减轻计算、优化性能。