瞭解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 一塊兒學習哦

相關文章
相關標籤/搜索