了解JavaScript中的Memoization以提升性能,再看React的应用

英文: Understanding Memoization in JavaScript to Improve Performancejavascript

中文: 了解JavaScript中的Memoization以提升性能--react的应用(欢迎star)html

咱们渴望提升应用程序的性能,MemoizationJavaScript中的一种技术,经过缓存结果并在下一个操做中从新使用缓存来加速查找费时的操做。java

在这里,咱们将看到memoization的用法以及它如何帮助优化应用的性能。node

Memoization: 基本理念

若是咱们有CPU密集型操做,咱们能够经过将初始操做的结果存储在缓存中来优化使用。若是操做必然会再次执行,咱们将再也不麻烦再次使用咱们的CPU,由于相同结果的结果存储在某个地方,咱们只是简单地返回结果。react

能够看下面的例子:git

function longOp(arg) {
    if( cache has operation result for arg) {
        return the cache
    }
    else {
        假设执行一个耗时30分钟的操做
        把结果存在`cache`缓存里
    }
    return the result
}
longOp('lp') // 由于第一次执行这个参数的操做,因此须要耗时30分钟
// 接下来会把结果缓存起来
longOp('bp') // 一样的第一次执行bp参数的操做,也须要耗时30分钟
// 一样会把结果缓存起来
longOp('bp') // 第二次出现了
// 会很快的把结果从缓存里取出来
longOp('lp') //也一样出现过了
// 快速的取出结果
复制代码

就CPU使用而言,上面的伪函数longOp是一种耗时的功能。上面的代码会把第一次的结果给缓存起来,后面具备相同输入的调用都会从缓存中提取结果,这样就会绕过期间和资源消耗。github

下面看一个平方根的例子:算法

function sqrt(arg) {
    return Math.sqrt(arg);
}
log(sqrt(4)) // 2
log(sqrt(9)) // 3
复制代码

如今咱们可使用memoize来处理这个函数:shell

function sqrt(arg) {
    if (!sqrt.cache) {
        sqrt.cache = {}
    }
    if (!sqrt.cache[arg]) {
        return sqrt.cache[arg] = Math.sqrt(arg)
    }
    return sqrt.cache[arg]
}
复制代码

能够看到,结果会缓存在cache的属性里。api

Memoization:履行

在上面部分,咱们为函数添加了memoization

如今,咱们能够建立一个独立的函数来记忆任何函数。咱们将此函数称为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将要缓存的函数做为参数传递。

memoizedFunction = memoize(funtionToMemoize)
memoizedFunction(args)
复制代码

咱们如今把上面的例子加入到这个里面:

function sqrt(arg) {
    return Math.sqrt(arg);
}
const memoizedSqrt = memoize(sqrt)
复制代码

返回的函数memoizedSqrt如今是sqrtmemoized版本。

咱们来调用下:

//...
memoizedSqrt(4) // 2 calculated(计算)
memoizedSqrt(4) // 2 cached
memoizedSqrt(9) // 3 calculated
memoizedSqrt(9) // 3 cached
memoizedSqrt(25) // 5 calculated
memoizedSqrt(25) // 5 cached
复制代码

咱们能够将memoize函数添加到Function原型中,以便咱们的应用程序中定义的每一个函数都继承memoize函数并能够调用它。

Function.prototype.memoize = function() {
    var self = this
    return function () {
        var args = Array.prototype.slice.call(arguments)
        self.cache = self.cache || {};
        return self.cache[args] ? self.cache[args] : (self.cache[args] = self(args))
    }
}
复制代码

咱们知道JS中定义的全部函数都是从Function.prototype继承的。所以,添加到Function.prototype的任何内容均可用于咱们定义的全部函数。

咱们如今再来试试:

function sqrt(arg) {
    return Math.sqrt(arg);
}
// ...
const memoizedSqrt = sqrt.memoize()
log(memoizedSqrt(4)) // 2, calculated
log(memoizedSqrt(4)) // 2, returns result from cache
log(memoizedSqrt(9)) // 3, calculated
log(memoizedSqrt(9)) // 3, returns result from cache
log(memoizedSqrt(25)) // 5, calculated
log(memoizedSqrt(25)) // 5, returns result from cache
复制代码

Memoization: Speed and Benchmarking

memoization的目标是速度,他经过内存来提高速度。

看下面的对比: 文件名: memo.js:

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))
    }
}

function sqrt(arg) {
    return Math.sqrt(arg);
}
const memoizedSqrt = memoize(sqrt)
console.time("non-memoized call")
console.log(sqrt(4))
console.timeEnd("non-memoized call")
console.time("memoized call")
console.log(sqrt(4))
console.timeEnd("memoized call")
复制代码

而后node memo.js能够发现输出,我这里是:

2
non-memoized call: 2.210ms
2
memoized call: 0.054ms
复制代码

能够发现,速度仍是提高了很多。

Memoization: 该何时使用

在这里,memoization一般会缩短执行时间并影响咱们应用程序的性能。当咱们知道一组输入将产生某个输出时,memoization最有效。

遵循最佳实践,应该在纯函数上实现memoization。纯函数输入什么就返回什么,不存在反作用。

记住这个是以空间换速度,因此最好肯定你是否值得那么作,有些场景颇有必要使用。

在处理递归函数时,Memoization最有效,递归函数用于执行诸如GUI渲染,Sprite和动画物理等繁重操做。

Memoization: 何时不要使用

不是纯函数的时候(输出不彻底依赖于输入)。

使用案例:斐波那契系列(Fibonacci)

Fibonacci是许多复杂算法中的一种,使用memoization优化的做用很明显。

1,1,2,3,5,8,13,21,34,55,89 每一个数字是前面两个数字的和。 如今咱们用js实现:

function fibonacci(num) {
    if (num == 1 || num == 2) {
        return 1
    }
    return fibonacci(num-1) + fibonacci(num-2)
}
复制代码

若是num超过2,则此函数是递归的。它以递减方式递归调用自身。

log(fibonacci(4)) // 3
复制代码

让咱们根据memoized版本对运行斐波那契的有效性进行测试。 memo.js文件:

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))
    }
}



function fibonacci(num) {
    if (num == 1 || num == 2) {
        return 1
    }
    return fibonacci(num-1) + fibonacci(num-2)
}

const memFib = memoize(fibonacci)
console.log('profiling tests for fibonacci')
console.time("non-memoized call")
console.log(memFib(6))
console.timeEnd("non-memoized call")
console.time("memoized call")
console.log(memFib(6))
console.timeEnd("memoized call")
复制代码

接下来调用:

$ node memo.js
profiling tests for fibonacci
8
non-memoized call: 1.027ms
8
memoized call: 0.046ms
复制代码

能够发现,很小的一个数字,时间差距就那么大了。

上面是参考原文,下面是我的感想。

咋说呢, 第一时间想到了reactmemo组件(注意 这里,现版本(16.6.3)有两个memo,一个是React.memo,还有一个是React.useMemo, 咱们这里说的是useMemo),相信关注react动态的都知道useMemo是新出来的hooks api,而且这个api是做用于function组件,官方文档写的是这个能够优化用以优化每次渲染的耗时工做。

文档这里介绍的也挺明白。今天看到medium的这篇文章,感受和react memo有关系,就去看了下源码,发现的确是和本文所述同样。

export function useMemo<T>( nextCreate: () => T, inputs: Array<mixed> | void | null, ): T {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber(); //返回一个变量
  workInProgressHook = createWorkInProgressHook(); // 返回包含memoizedState的hook对象

  const nextInputs =
    inputs !== undefined && inputs !== null ? inputs : [nextCreate]; // 须要保存下来的inputs,用做下次取用的key

  const prevState = workInProgressHook.memoizedState; // 获取以前缓存的值
  if (prevState !== null) {
    const prevInputs = prevState[1];
    // prevState不为空,而且取出上次存的`key`, 而后下面判断(先后的`key`是否是同一个),若是是就直接返回,不然继续向下
    if (areHookInputsEqual(nextInputs, prevInputs)) {
      return prevState[0];
    }
  }

  const nextValue = nextCreate(); //执行useMemo传入的第一个参数(函数)
  workInProgressHook.memoizedState = [nextValue, nextInputs]; // 存入memoizedState以便下次对比使用
  return nextValue; 
}
复制代码

进行了缓存(workInProgressHook.memoizedState就是hook返回的对象而且包含memoizedState,进行对比先后的inputs是否相同,而后再次进行操做),而且支持传递第二个数组参数做为key

果真, useMemo就是用的本文提到的memoization来提升性能的。

其实从官方文档就知道这个两个有关系了 :cry: :

Pass a “create” function and an array of inputs. useMemo will only recompute the memoized value when one of the inputs has changed. This optimization helps to avoid expensive calculations on every render.

我的学习记录--欢迎star&watch 一块儿学习哦

相关文章
相关标签/搜索