React 的 useMemo

英文: Understanding Memoization in JavaScript to Improve Performance

我們渴望提高應(yīng)用程序的性能,MemoizationJavaScript中的一種技術(shù),通過(guò)緩存結(jié)果并在下一個(gè)操作中重新使用緩存來(lái)加速查找費(fèi)時(shí)的操作。

在這里,我們將看到memoization的用法以及它如何幫助優(yōu)化應(yīng)用的性能。

Memoization: 基本想法

如果我們有CPU密集型操作,我們可以通過(guò)將初始操作的結(jié)果存儲(chǔ)在緩存中來(lái)優(yōu)化使用。如果操作必然會(huì)再次執(zhí)行,我們將不再麻煩再次使用我們的CPU,因?yàn)橄嗤Y(jié)果的結(jié)果存儲(chǔ)在某個(gè)地方,我們只是簡(jiǎn)單地返回結(jié)果。

可以看下面的例子:

function longOp(arg) {
    if( cache has operation result for arg) {
        return the cache
    }
    else {
        假設(shè)執(zhí)行一個(gè)耗時(shí)30分鐘的操作
        把結(jié)果存在`cache`緩存里
    }
    return the result
}
longOp('lp') // 因?yàn)榈谝淮螆?zhí)行這個(gè)參數(shù)的操作,所以需要耗時(shí)30分鐘
// 接下來(lái)會(huì)把結(jié)果緩存起來(lái)
longOp('bp') // 同樣的第一次執(zhí)行bp參數(shù)的操作,也需要耗時(shí)30分鐘
// 同樣會(huì)把結(jié)果緩存起來(lái)
longOp('bp') // 第二次出現(xiàn)了
// 會(huì)很快的把結(jié)果從緩存里取出來(lái)
longOp('lp') //也同樣出現(xiàn)過(guò)了
// 快速的取出結(jié)果

就CPU使用而言,上面的偽函數(shù)longOp是一種耗時(shí)的功能。上面的代碼會(huì)把第一次的結(jié)果給緩存起來(lái),后面具有相同輸入的調(diào)用都會(huì)從緩存中提取結(jié)果,這樣就會(huì)繞過(guò)時(shí)間和資源消耗。

下面看一個(gè)平方根的例子:

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

現(xiàn)在我們可以使用memoize來(lái)處理這個(gè)函數(shù):

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

可以看到,結(jié)果會(huì)緩存在cache的屬性里。

Memoization:履行

在上面部分,我們?yōu)楹瘮?shù)添加了memoization。

現(xiàn)在,我們可以創(chuàng)建一個(gè)獨(dú)立的函數(shù)來(lái)記憶任何函數(shù)。我們將此函數(shù)稱(chēng)為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))
    }
}

我們可以看到這段代碼接收另外一個(gè)函數(shù)作為參數(shù)并返回。

要使用此函數(shù),我們調(diào)用memoize將要緩存的函數(shù)作為參數(shù)傳遞。

memoizedFunction = memoize(funtionToMemoize)
memoizedFunction(args)

我們現(xiàn)在把上面的例子加入到這個(gè)里面:

function sqrt(arg) {
    return Math.sqrt(arg);
}
const memoizedSqrt = memoize(sqrt)

返回的函數(shù)memoizedSqrt現(xiàn)在是sqrtmemoized版本。

我們來(lái)調(diào)用下:

//...
memoizedSqrt(4) // 2 calculated(計(jì)算)
memoizedSqrt(4) // 2 cached
memoizedSqrt(9) // 3 calculated
memoizedSqrt(9) // 3 cached
memoizedSqrt(25) // 5 calculated
memoizedSqrt(25) // 5 cached

我們可以將memoize函數(shù)添加到Function原型中,以便我們的應(yīng)用程序中定義的每個(gè)函數(shù)都繼承memoize函數(shù)并可以調(diào)用它。

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中定義的所有函數(shù)都是從Function.prototype繼承的。因此,添加到Function.prototype的任何內(nèi)容都可用于我們定義的所有函數(shù)。

我們現(xiàn)在再來(lái)試試:

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的目標(biāo)是速度,他通過(guò)內(nèi)存來(lái)提升速度。

看下面的對(duì)比:
文件名: 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可以發(fā)現(xiàn)輸出,我這里是:

2
non-memoized call: 2.210ms
2
memoized call: 0.054ms

可以發(fā)現(xiàn),速度還是提升了不少。

Memoization: 該什么時(shí)候使用

在這里,memoization通常會(huì)縮短執(zhí)行時(shí)間并影響我們應(yīng)用程序的性能。當(dāng)我們知道一組輸入將產(chǎn)生某個(gè)輸出時(shí),memoization最有效。

遵循最佳實(shí)踐,應(yīng)該在純函數(shù)上實(shí)現(xiàn)memoization。純函數(shù)輸入什么就返回什么,不存在副作用。

記住這個(gè)是以空間換速度,所以最好確定你是否值得那么做,有些場(chǎng)景很有必要使用。

在處理遞歸函數(shù)時(shí),Memoization最有效,遞歸函數(shù)用于執(zhí)行諸如GUI渲染,Sprite和動(dòng)畫(huà)物理等繁重操作。

Memoization: 什么時(shí)候不要使用

不是純函數(shù)的時(shí)候(輸出不完全依賴(lài)于輸入)。

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

Fibonacci是許多復(fù)雜算法中的一種,使用memoization優(yōu)化的作用很明顯。

1,1,2,3,5,8,13,21,34,55,89
每個(gè)數(shù)字是前面兩個(gè)數(shù)字的和。
現(xiàn)在我們用js實(shí)現(xiàn):

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

如果num超過(guò)2,則此函數(shù)是遞歸的。它以遞減方式遞歸調(diào)用自身。

log(fibonacci(4)) // 3

讓我們根據(jù)memoized版本對(duì)運(yùn)行斐波那契的有效性進(jìn)行測(cè)試。
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")

接下來(lái)調(diào)用:

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

可以發(fā)現(xiàn),很小的一個(gè)數(shù)字,時(shí)間差距就那么大了。

上面是參考原文,下面是個(gè)人感想。

咋說(shuō)呢, 第一時(shí)間想到了reactmemo組件(注意 這里,現(xiàn)版本(16.6.3)有兩個(gè)memo,一個(gè)是React.memo,還有一個(gè)
React.useMemo, 我們這里說(shuō)的是useMemo),相信關(guān)注react動(dòng)態(tài)的都知道useMemo是新出來(lái)的hooks api,并且這個(gè)api是作用于function組件,官方文檔寫(xiě)的是這個(gè)可以?xún)?yōu)化用以?xún)?yōu)化每次渲染的耗時(shí)工作。

文檔這里介紹的也挺明白。今天看到medium的這篇文章,
感覺(jué)和react memo有關(guān)系,就去看了下源碼,發(fā)現(xiàn)的確是和本文所述一樣。

//react/packages/react-reconciler/src/ReactFiberHooks.js
export function useMemo<T>(
  nextCreate: () => T,
  inputs: Array<mixed> | void | null,
): T {
  currentlyRenderingFiber = resolveCurrentlyRenderingFiber(); //返回一個(gè)變量
  workInProgressHook = createWorkInProgressHook(); // 返回包含memoizedState的hook對(duì)象

  const nextInputs =
    inputs !== undefined && inputs !== null ? inputs : [nextCreate]; // 需要保存下來(lái)的inputs,用作下次取用的key

  const prevState = workInProgressHook.memoizedState; // 獲取之前緩存的值
  if (prevState !== null) {
    const prevInputs = prevState[1];
    // prevState不為空,并且取出上次存的`key`, 然后下面判斷(前后的`key`是不是同一個(gè)),如果是就直接返回,否則繼續(xù)向下
    if (areHookInputsEqual(nextInputs, prevInputs)) {
      return prevState[0];
    }
  }

  const nextValue = nextCreate(); //執(zhí)行useMemo傳入的第一個(gè)參數(shù)(函數(shù))
  workInProgressHook.memoizedState = [nextValue, nextInputs]; // 存入memoizedState以便下次對(duì)比使用
  return nextValue; 
}

進(jìn)行了緩存(workInProgressHook.memoizedState就是hook返回的對(duì)象并且包含memoizedState,進(jìn)行對(duì)比前后的inputs是否相同,然后再次進(jìn)行操作),并且支持傳遞第二個(gè)數(shù)組參數(shù)作為key。

果然, useMemo就是用的本文提到的memoization來(lái)提高性能的。

其實(shí)從官方文檔就知道這個(gè)兩個(gè)有關(guān)系了 :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.

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容