英文: 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.