javascript
了解JavaScript中的Memoization以提高性能,再看React的应用
英文: Understanding Memoization in JavaScript to Improve Performance
中文: 了解JavaScript中的Memoization以提高性能--react的應用(歡迎star)
我們渴望提高應用程序的性能,Memoization是JavaScript中的一種技術,通過緩存結果并在下一個操作中重新使用緩存來加速查找費時的操作。
在這里,我們將看到memoization的用法以及它如何幫助優化應用的性能。
Memoization: 基本理念
如果我們有CPU密集型操作,我們可以通過將初始操作的結果存儲在緩存中來優化使用。如果操作必然會再次執行,我們將不再麻煩再次使用我們的CPU,因為相同結果的結果存儲在某個地方,我們只是簡單地返回結果。
可以看下面的例子:
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是一種耗時的功能。上面的代碼會把第一次的結果給緩存起來,后面具有相同輸入的調用都會從緩存中提取結果,這樣就會繞過時間和資源消耗。
下面看一個平方根的例子:
function sqrt(arg) {return Math.sqrt(arg); } log(sqrt(4)) // 2 log(sqrt(9)) // 3 復制代碼現在我們可以使用memoize來處理這個函數:
function sqrt(arg) {if (!sqrt.cache) {sqrt.cache = {}}if (!sqrt.cache[arg]) {return sqrt.cache[arg] = Math.sqrt(arg)}return sqrt.cache[arg] } 復制代碼可以看到,結果會緩存在cache的屬性里。
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現在是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 = thisreturn 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 復制代碼可以發現,很小的一個數字,時間差距就那么大了。
上面是參考原文,下面是個人感想。
咋說呢, 第一時間想到了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,用作下次取用的keyconst 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 一起學習哦
總結
以上是生活随笔為你收集整理的了解JavaScript中的Memoization以提高性能,再看React的应用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS提交审核:您的 App 正在使用广
- 下一篇: Spring Boot参数校验