资讯专栏INFORMATION COLUMN

Memoization in JavaScript

ccj659 / 2095人阅读

摘要:源码函数调用过,没有变化,参数时返回缓存值。而通过,可以把上一次的计算结果保存下来,而避免重复计算。这意味着将跳过渲染组件,并重用最后渲染的结果。

1. 基本概念

在一个CPU密集型应用中,我们可以使用Memoization来进行优化,其主要用于通过存储昂贵的函数调用的结果来加速程序,并在再次发生相同的输入时返回缓存的结果。
例如一个简单的求平方根的函数:

const sqrt = Math.sqrt;
//使用cache缓存
const sqrt = (arg)=>{
    if(!sqrt.cache){
        sqrt.cache = {};
    }
    if(!sqrt.cache[arg]){
        sqrt.cache[arg] = Math.sqrt(arg)
    }
    return sqrt.cache[arg]
}
//简单的运行时间对比
//第一次运行:
console.time("start1")
sqrt(779)
console.timeEnd("start1")
VM516:3 start1: 0.01806640625ms
//第二次运行:
console.time("start1")
sqrt(779)
console.timeEnd("start1")
VM521:3 start1: 0.005859375ms
2. 简单通用实现

我们实现一个通用的memoize函数,用它来包装任意纯函数,并缓存其计算结果。

function memoize(fn){
        return function(){
            var args = Array.prototype.slice.call(arguments);
            fn.cache = fn.cache || {};
            return fn.cache[args] ?
                 fn.cache[args] :(fn.cache[args] = fn.apply(this,args))
        }
}

必须注意的是,memoize的原理是在发生相同的输入时返回缓存的结果,这意味着其包装的函数应当是一个纯函数,当函数不纯时,相同的输入在不同时间可能返回不同的结果,此时再使用memoize明显不合时宜。

3. 常见的memoize库 3.1 lodash库中的memoize

在该库中,使用Map缓存计算结果,支持传入resolver函数将传入的参数转换为存入Map的键名(默认键名是第一个参数,当键名是引用类型时,引用不变,缓存不会更新)。

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
3.2 memoize-one

与lodash不同,memoize-one仅仅保存上一次调用时的结果,如果下次参数变化,则更新缓存。因此不必担心由于maxAge, maxSize, exclusions等导致的内存泄漏。

//源码
export default function mixed>(
  resultFn: ResultFn,
  isEqual?: EqualityFn = areInputsEqual,
): ResultFn {
  let lastThis: mixed;
  let lastArgs: mixed[] = [];
  let lastResult: mixed;
  let calledOnce: boolean = false;
//函数调用过,this没有变化,参数isEqual时返回缓存值。
  const result = function(...newArgs: mixed[]) {
    if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) {
      return lastResult;
    }
    lastResult = resultFn.apply(this, newArgs);
    calledOnce = true;
    lastThis = this;
    lastArgs = newArgs;
    return lastResult;
  };

  return (result: any);
}

可以看到,可以通过第二个参数自定义参数是否相同,默认是areInputsEqual函数。

//areInputsEqual实现
export default function areInputsEqual(
  newInputs: mixed[],
  lastInputs: mixed[],
) {
//先进行参数长度比较
  if (newInputs.length !== lastInputs.length) {
    return false;
  }
 //参数浅比较
  for (let i = 0; i < newInputs.length; i++) {
    if (newInputs[i] !== lastInputs[i]) {
      return false;
    }
  }
  return true;
}

可以自定义参数比较函数进行深比较

import memoizeOne from "memoize-one";
import isDeepEqual from "lodash.isequal";

const identity = x => x;

const shallowMemoized = memoizeOne(identity);
const deepMemoized = memoizeOne(identity, isDeepEqual);

const result1 = shallowMemoized({ foo: "bar" });
const result2 = shallowMemoized({ foo: "bar" });

result1 === result2; // false - difference reference

const result3 = deepMemoized({ foo: "bar" });
const result4 = deepMemoized({ foo: "bar" });

result3 === result4; // true - arguments are deep equal
4. memoize-one在React中的应用

在React中有这样一个应用场景,当部分props变化需要改变派生state时,可以在getDerivedStateFromProps中通过if判断是否需要重新计算,但它比它需要的更复杂,因为它必须多带带跟踪和检测每个props和state的变化,当props很多时,这种判断方式会变得纠缠不清。

class Example extends Component {
  state = {
    filterText: "",
  };
  static getDerivedStateFromProps(props, state) {
    if (
      props.list !== state.prevPropsList ||
      state.prevFilterText !== state.filterText
    ) {
      return {
        prevPropsList: props.list,
        prevFilterText: state.filterText,
        filteredList: props.list.filter(item => item.text.includes(state.filterText))
      };
    }
    return null;
  }
  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };
  render() {
    return (
      
        
        
    {this.state.filteredList.map(item =>
  • {item.text}
  • )}
); } }

而通过Memoization,可以把上一次的计算结果保存下来,而避免重复计算。例如以下通过memoize-one库实现的记忆,可以让我们不用手动判断list, filterText是否变化,是否需要重新计算。

import memoize from "memoize-one";
class Example extends Component {
  // State only needs to hold the current filter text value:
  state = { filterText: "" };
  // Re-run the filter whenever the list array or filter text changes:
  filter = memoize(
    (list, filterText) => list.filter(item => item.text.includes(filterText))
  );

  handleChange = event => {
    this.setState({ filterText: event.target.value });
  };

  render() {
    const filteredList = this.filter(this.props.list, this.state.filterText);
    return (
      
        
        
    {filteredList.map(item =>
  • {item.text}
  • )}
); } }
5. 配合Redux使用的memoize库——reselect

在react中,每当组件重新渲染,计算派生状态的逻辑就会执行一遍(不管这些逻辑是放在render或者是放在getDerivedStateFromProps中,如果没有采用很多if限制的话)。上节介绍了使用memoize-one来缓存来避免重复计算,当我们使用redux时,通常在mapStateToProps中计算派生状态,每当store中的任意state更新时,都会触发mapStateToProps中的计算,然而,往往派生state通常只依赖部分state,不必每次都计算。
Reselect是一个十分贴近redux的memoize库,其和memoize-one一样只缓存前一次的计算值,并支持自定义memoize函数,自定义参数比较函数等;其输入参数由inputSelectors functions 产生,生成的的依然是inputSelectors functions,这意味的与memoize相比,这可以很容易的组合。
另外,mapStateProps的第二个参数ownProps也可以传入selector中。

import { createSelector } from "reselect"

fSelector = createSelector(
    a => state.a,
    b => state.b,
    (a, b) => f(a, b)
)
hSelector = createSelector(
    b => state.b,
    c => state.c,
    (b, c) => h(b, c)
)
gSelector =  createSelector(
    a => state.a,
    c => state.c,
    (a, c) => g(a, c)
)
uSelector = createSelector(
    a => state.a,
    b => state.b,
    c => state.c,
    (a, b, c) => u(a, b, c)
)

...
function mapStateToProps(state) {
    const { a, b, c } = state
    return {
        a,
        b,
        c,
        fab: fSelector(state),
        hbc: hSelector(state),
        gac: gSelector(state),
        uabc: uSelector(state)
    }
}
6. react中memoize原生支持——React.memo

如果你的函数组件在给定相同的props的情况下呈现相同的结果,你可以React.memo通过记忆结果将它包装在一些调用中以提高性能。这意味着React将跳过渲染组件,并重用最后渲染的结果。

默认情况下,它只会浅显比较props对象中的复杂对象。如果要控制比较,还可以提供自定义比较函数作为第二个参数。

function MyComponent(props) {
  /* render using props */
}
function areEqual(prevProps, nextProps) {
  /*
  return true if passing nextProps to render would return
  the same result as passing prevProps to render,
  otherwise return false
  */
}
export default React.memo(MyComponent, areEqual);

You Probably Don’t Need Derived State - React Blog
Understanding Memoization in JavaScript to Improve Performance
性能优化:memoization | Taobao FED | 淘宝前端团队
lodash/memoize.js at master · lodash/lodash · GitHub
GitHub - alexreardon/memoize-one: A memoization library which only remembers the latest invocation
为什么我们需要reselect - 从0开始实现react技术栈 - SegmentFault 思否
React Hooks: Memoization – Sandro Dolidze – Medium

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/104002.html

相关文章

  • JavaScript 高级技巧 Memoization

    摘要:来源于拉丁语,不要与混淆了。本文首先介绍一个简单的使用优化技术的例子,然后解读和库中使用的源码,加深理解。总结是一种优化技术,避免一些不必要的重复计算,可以提高计算速度。 memoization 来源于拉丁语 memorandum (to be remembered),不要与 memorization 混淆了。 首先来看一下维基百科的描述: In computing, memoizat...

    刘德刚 评论0 收藏0
  • 斐波那契数列求和的js方案以及优化

    摘要:在上做了一道斐波那契数列求和的题目,做完之后做了一些简单的优化和用另一种方法实现。动态规划解决方案斐波那契数列求和除了可以用递归的方法解决,还可以用动态规划的方法解决。 在codewars上做了一道斐波那契数列求和的题目,做完之后做了一些简单的优化和用另一种方法实现。 题目 function fibonacci(n) { if(n==0 || n == 1) r...

    xinhaip 评论0 收藏0
  • JS专题之memoization

    摘要:前言在计算机领域,记忆是主要用于加速程序计算的一种优化技术,它使得函数避免重复演算之前已被处理过的输入,而返回已缓存的结果。被执行了不是素数,其他数字默认是素数。我们可以看出,如果从开始打印斐波那契数列,函数被执行了次。 前言 在计算机领域,记忆(memoization)是主要用于加速程序计算的一种优化技术,它使得函数避免重复演算之前已被处理过的输入,而返回已缓存的结果。 -- wi...

    zhisheng 评论0 收藏0
  • Python 2.7终结于7个月后,这是你需要了解的3.X炫酷新特性

    摘要:截止到月号上午点,将终结于在这一段时间中,很多优秀开源项目与库已经停止了对的支持。除了,还提供了一种通过进行字符串插入的灵活方法。扩展的可迭代对象解包最低版本为对于这个特性,代码就说明了一切。从 3.0 到 3.8,Python 3 已经更新了一波又一波,但似乎我们用起来和 2.7 没有太大区别?以前该怎么写 2.7 的代码现在就怎么写,只不过少数表达方式变了而已。在这篇文章中,作者介绍了 ...

    番茄西红柿 评论0 收藏0
  • Python 2.7终结于7个月后,这是你需要了解的3.X炫酷新特性

    摘要:截止到月号上午点,将终结于在这一段时间中,很多优秀开源项目与库已经停止了对的支持。除了,还提供了一种通过进行字符串插入的灵活方法。扩展的可迭代对象解包最低版本为对于这个特性,代码就说明了一切。从 3.0 到 3.8,Python 3 已经更新了一波又一波,但似乎我们用起来和 2.7 没有太大区别?以前该怎么写 2.7 的代码现在就怎么写,只不过少数表达方式变了而已。在这篇文章中,作者介绍了 ...

    chadLi 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<