英文: Understanding Memoization in JavaScript to Improve Performancejavascript
中文: 了解JavaScript中的Memoization以提升性能--react的应用(欢迎star)html
咱们渴望提升应用程序的性能,Memoization
是JavaScript
中的一种技术,经过缓存结果并在下一个操做中从新使用缓存来加速查找费时的操做。java
在这里,咱们将看到memoization
的用法以及它如何帮助优化应用的性能。node
若是咱们有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
。
如今,咱们能够建立一个独立的函数来记忆任何函数。咱们将此函数称为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
如今是sqrt
的memoized
版本。
咱们来调用下:
//...
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
的目标是速度,他经过内存来提高速度。
看下面的对比: 文件名: 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
最有效,递归函数用于执行诸如GUI渲染,Sprite和动画物理等繁重操做。
不是纯函数的时候(输出不彻底依赖于输入)。
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
复制代码
能够发现,很小的一个数字,时间差距就那么大了。
咋说呢, 第一时间想到了react
的memo
组件(注意 这里,现版本(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.