记忆函数(Memoization)是一种用于长递归或长迭代操作性能优化的编程实践。
记忆函数实现原理:使用一组参数初次调用函数时,缓存参数和计算结果,当再次使用相同的参数调用该函数时,直接返回相应的缓存结果。
注意: 记忆化函数不能有副作用。
以求解 fibonacci 数为例演示记忆函数
普通递归版
function fibonacci(n) {
console.log(`calculate fibonacci(${n})...`)
if (n < 2) {
return 1;
} else {
return fibonacci(n - 2) + fibonacci(n - 1);
}
}
复制代码
记忆递归版
const memoizedFibonacci = (function () {
const cache = {} // 计算结果缓存
return function fibonacci(n) {
// 不同的参数按一定的规则计算得到不同的缓存键值
const key = n
// 每次执行时首先检查缓存
// 若当前参数的计算结果已有缓存,直接返回缓存结果
if (cache[key]) {
return cache[key]
}
// 否则,计算、缓存并返回结果
console.log(`calculate fibonacci(${n})...`)
if (n < 2) {
return cache[key] = 1;
} else {
return cache[key] = fibonacci(n - 2) + fibonacci(n - 1);
}
}
})()
复制代码
执行过程对比
// 依次执行下述语句,观察输出
fibonacci(3);
/**
* execution-log:
* calculate fibonacci(3)...
* calculate fibonacci(1)...
* calculate fibonacci(2)...
* calculate fibonacci(0)...
* calculate fibonacci(1)...
*
* return-value:
* 3
*/
fibonacci(5);
/**
* execution-log:
* calculate fibonacci(5)...
* calculate fibonacci(3)...
* calculate fibonacci(1)...
* calculate fibonacci(2)...
* calculate fibonacci(0)...
* calculate fibonacci(1)...
* calculate fibonacci(4)...
* calculate fibonacci(2)...
* calculate fibonacci(0)...
* calculate fibonacci(1)...
* calculate fibonacci(3)...
* calculate fibonacci(1)...
* calculate fibonacci(2)...
* calculate fibonacci(0)...
* calculate fibonacci(1)...
*
* return-value:
* 8
*/
/* ------------------------------- */
memoizedFibonacci(3);
/**
* execution-log:
* calculate fibonacci(3)...
* calculate fibonacci(1)...
* calculate fibonacci(2)...
* calculate fibonacci(0)...
*
* return-value:
* 3
*/
memoizedFibonacci(5);
/**
* fibonacci(0), fibonacci(1), ..., fibonacci(3) 已缓存结果,无须重新计算
*
* execution-log:
* calculate fibonacci(5)...
* calculate fibonacci(4)...
*
* return-value:
* 8
*/
memoizedFibonacci(10);
/**
* fibonacci(0), fibonacci(1), ..., fibonacci(5) 已缓存结果,无须重新计算
*
* execution-log:
* calculate fibonacci(10)...
* calculate fibonacci(8)...
* calculate fibonacci(6)...
* calculate fibonacci(7)...
* calculate fibonacci(9)...
*
* return-value:
* 89
*/
memoizedFibonacci(9);
/**
* fibonacci(0), fibonacci(1), ..., fibonacci(10) 已缓存结果,无须重新计算
*
* execution-log:
*
* return-value:
* 55
*/
复制代码
可以看到
- 普通递归版执行大量的重复计算
- 记忆化递归版避免了重复计算
函数记忆化之 lodash/memoize
_.memoize(func, [resolver])
使用该函数可以创建记忆化版本的 func 函数,例如:
import memoize from 'lodash/memoize';
function fibonacci(n) {
if (n < 2) {
return 1;
} else {
return fibonacci(n - 2) + fibonacci(n - 1);
}
}
const memoizedFibonacci = memoize(fibonacci);
复制代码
注意,使用 _.memoize
函数有内存泄漏的风险,可以根据应用场景定制 _.memoize.Cache
进行优化,例如:
import memoize from 'lodash/memoize';
/**
* @param max {number} 缓存结果数上限
*/
function getLimitedCache(max = 200) {
class LimitedCache {
constructor() {
this._max = max; // 设定缓存结果数上限
this._store = new Map();
}
set(key, value) {
const store = this._store;
const max = this._max;
if (store.size >= max) {
store.clear();
}
return store.set(key, value);
}
get(key) {
return this._store.get(key);
}
delete(key) {
return this._store.delete(key);
}
has(key) {
return this._store.has(key);
}
clear() {
return this._store.clear();
}
}
}
function limitedMemoize(...args) {
const DefaultCache = memoize.Cache;
memoize.Cache = getLimitedCache();
return memoize(...args);
memoize.Cache = DefaultCache;
}
复制代码
实现:lodash/memoize.js (github 链接)
/**
* Creates a function that memoizes the result of `func`. If `resolver` is
* provided, it determines the cache key for storing the result based on the
* arguments provided to the memoized function. By default, the first argument
* provided to the memoized function is used as the map cache key. The `func`
* is invoked with the `this` binding of the memoized function.
*
* **Note:** The cache is exposed as the `cache` property on the memoized
* function. Its creation may be customized by replacing the `memoize.Cache`
* constructor with one whose instances implement the
* [`Map`](http://ecma-international.org/ecma-262/7.0/#sec-properties-of-the-map-prototype-object)
* method interface of `clear`, `delete`, `get`, `has`, and `set`.
*
* @since 0.1.0
* @category Function
* @param {Function} func The function to have its output memoized.
* @param {Function} [resolver] The function to resolve the cache key.
* @returns {Function} Returns the new memoized function.
* @example
*
* const object = { 'a': 1, 'b': 2 }
* const other = { 'c': 3, 'd': 4 }
*
* const values = memoize(values)
* values(object)
* // => [1, 2]
*
* values(other)
* // => [3, 4]
*
* object.a = 2
* values(object)
* // => [1, 2]
*
* // Modify the result cache.
* values.cache.set(object, ['a', 'b'])
* values(object)
* // => ['a', 'b']
*
* // Replace `memoize.Cache`.
* memoize.Cache = WeakMap
*/
function memoize(func, resolver) {
if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) {
throw new TypeError('Expected a function')
}
const memoized = function(...args) {
const key = resolver ? resolver.apply(this, args) : args[0]
const cache = memoized.cache
if (cache.has(key)) {
return cache.get(key)
}
const result = func.apply(this, args)
memoized.cache = cache.set(key, result) || cache
return result
}
memoized.cache = new (memoize.Cache || Map)
return memoized
}
memoize.Cache = Map
export default memoize
复制代码
函数记忆化之 memoize-one
memoize-one 生成的记忆函数只缓存最近一次调用的参数和计算结果。当使用与最近一次相同的参数调用 memoized 函数时,返回上一次的缓存的结果,否则执行完整的计算过程,并更新缓存为此次调用的参数和结果。
memoize-one 生成的记忆函数不存在内存泄漏的风险。
memoize-one 源码(JavaScript 版):
function areInputsEqual(newInputs, lastInputs) {
if (newInputs.length !== lastInputs.length) {
return false;
}
for (let i = 0; i < newInputs.length; i++) {
if (newInputs[i] !== lastInputs[i]) {
return false;
}
}
return true;
}
function memoize(resultFn, isEqual = areInputsEqual) {
let lastThis;
let lastArgs = [];
let lastResult;
let calledOnce = false;
// breaking cache when contex (this) or arguments change
const result = function(...newArgs) {
if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) {
return lastResult;
}
lastResult = resultFn.apply(this, newArgs);
calledOnce = true;
lastThis = this;
lastArgs = newArgs;
return lastResult;
};
return result;
}
复制代码
在 React 组件中使用 memoize-one
场景:组件接收一个 list
以及其他一些参数(<FilterList list={list} {...otherProps} />
),根据不同的 state.activeKey
过滤 props.list
并展示,根据其他参数完成其他事务。
方案:
-
一种比较繁琐的方案:维护
state.filteredList
,手动监听props.list
和state.activeKey
, 当props.list
或state.activeKey
变化时更新state.filteredList
。 -
一种简洁但存在性能问题的方案:每次
render()
时使用state.activeKey
过滤props.list
并渲染。 -
使用
memoize-one
的优化方案:使用memoize-one
生成记忆函数memoizedFilter()
,在每次render()
时使用记忆函数memoizedFilter(list, activeKey)
获得过滤结果并渲染。
方案3具体实现如下:
import React from 'react';
import { Tab } from 'antd';
import memoize from 'memoize-one';
const tabKeys = ['1', '2', '3'];
class FilterList extends React.Component {
state = {
activeKey: '1',
};
// 创建 memoized filter
memoizedFilter = memoize(
(list, activeKey) => list.filter(item => item.key === activeKey)
);
switchTab = (key) = {
this.setState({ activeKey: key });
}
render() {
const { list } = this.props
const { activeKey } = this.state
/**
* 在每次 render 时进行过滤,使用 memoizedFilter 保证了
* 只有当 list 或 activeKey 变化时才重新计算过滤结果
*/
const filteredList = this.memoizedFilter(list, activeKey);
/**
* 如果直接使用 list.filter(item => item.key === activeKey)
* 则每次 render 时都要重新计算过滤结果,不管 list 和 activeKey 有无变化
*/
return (
<div>
<div className="tabs">
{tabKeys.map(key => (
<div
key={key}
onClick={() => { this.switchTab(key); }}
>
{`tab ${key}`}
</div>
))}
</div>
<div className="list">
{filteredList.map(item => (
<div key={item.id}>{item.label}</div>
))}
</div>
</div>
);
}
}
class Page extends React.Component {
state = {
list: [],
}
// ...
render() {
const { list } = this.state;
return (
<div>
{/* ... */}
<FilterList list={list} />
{/* ... */}
</div>
)
}
}
复制代码
方案3:
- 不用维护
filteredList
- 不用手动监听
props.list
和state.activeKey
的变化 - 只在
props.list
或state.activeKey
变化时重新计算filteredList
,不存在性能问题