JavaScript 记忆(Memoization)函数

记忆化(Memoization)是一种性能优化技术,通过缓存函数结果来避免重复计算。本文以 fibonacci 数列为例,对比普通递归与记忆化递归的执行效率,并介绍了 lodash/memoize 和 memoize-one 两个库的使用。在 React 组件中,使用 memoize-one 可以有效避免内存泄漏,提高组件性能。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

记忆函数(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

github.com/alexreardon…

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 并展示,根据其他参数完成其他事务。

方案:

  1. 一种比较繁琐的方案:维护 state.filteredList,手动监听 props.liststate.activeKey, 当 props.liststate.activeKey 变化时更新 state.filteredList

  2. 一种简洁但存在性能问题的方案:每次 render() 时使用 state.activeKey 过滤 props.list 并渲染。

  3. 使用 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.liststate.activeKey 的变化
  • 只在 props.liststate.activeKey 变化时重新计算 filteredList,不存在性能问题

转载于:https://juejin.im/post/5cb8a0555188253238022b7c

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值