javascript
了解JavaScript中的Memoization以提高性能,再看React的应用
英文: Understanding Memoization in JavaScript to Improve Performance
中文: 了解JavaScript中的Memoization以提高性能--react的應(yīng)用(歡迎star)
我們渴望提高應(yīng)用程序的性能,Memoization是JavaScript中的一種技術(shù),通過緩存結(jié)果并在下一個(gè)操作中重新使用緩存來加速查找費(fèi)時(shí)的操作。
在這里,我們將看到memoization的用法以及它如何幫助優(yōu)化應(yīng)用的性能。
Memoization: 基本理念
如果我們有CPU密集型操作,我們可以通過將初始操作的結(jié)果存儲在緩存中來優(yōu)化使用。如果操作必然會再次執(zhí)行,我們將不再麻煩再次使用我們的CPU,因?yàn)橄嗤Y(jié)果的結(jié)果存儲在某個(gè)地方,我們只是簡單地返回結(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分鐘 // 接下來會把結(jié)果緩存起來 longOp('bp') // 同樣的第一次執(zhí)行bp參數(shù)的操作,也需要耗時(shí)30分鐘 // 同樣會把結(jié)果緩存起來 longOp('bp') // 第二次出現(xiàn)了 // 會很快的把結(jié)果從緩存里取出來 longOp('lp') //也同樣出現(xiàn)過了 // 快速的取出結(jié)果 復(fù)制代碼就CPU使用而言,上面的偽函數(shù)longOp是一種耗時(shí)的功能。上面的代碼會把第一次的結(jié)果給緩存起來,后面具有相同輸入的調(diào)用都會從緩存中提取結(jié)果,這樣就會繞過時(shí)間和資源消耗。
下面看一個(gè)平方根的例子:
function sqrt(arg) {return Math.sqrt(arg); } log(sqrt(4)) // 2 log(sqrt(9)) // 3 復(fù)制代碼現(xiàn)在我們可以使用memoize來處理這個(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] } 復(fù)制代碼可以看到,結(jié)果會緩存在cache的屬性里。
Memoization:履行
在上面部分,我們?yōu)楹瘮?shù)添加了memoization。
現(xiàn)在,我們可以創(chuàng)建一個(gè)獨(dú)立的函數(shù)來記憶任何函數(shù)。我們將此函數(shù)稱為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))} } 復(fù)制代碼我們可以看到這段代碼接收另外一個(gè)函數(shù)作為參數(shù)并返回。
要使用此函數(shù),我們調(diào)用memoize將要緩存的函數(shù)作為參數(shù)傳遞。
memoizedFunction = memoize(funtionToMemoize) memoizedFunction(args) 復(fù)制代碼我們現(xiàn)在把上面的例子加入到這個(gè)里面:
function sqrt(arg) {return Math.sqrt(arg); } const memoizedSqrt = memoize(sqrt) 復(fù)制代碼返回的函數(shù)memoizedSqrt現(xiàn)在是sqrt的memoized版本。
我們來調(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 復(fù)制代碼我們可以將memoize函數(shù)添加到Function原型中,以便我們的應(yīng)用程序中定義的每個(gè)函數(shù)都繼承memoize函數(shù)并可以調(diào)用它。
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))} } 復(fù)制代碼我們知道JS中定義的所有函數(shù)都是從Function.prototype繼承的。因此,添加到Function.prototype的任何內(nèi)容都可用于我們定義的所有函數(shù)。
我們現(xiàn)在再來試試:
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 復(fù)制代碼Memoization: Speed and Benchmarking
memoization的目標(biāo)是速度,他通過內(nèi)存來提升速度。
看下面的對比: 文件名: 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") 復(fù)制代碼然后node memo.js可以發(fā)現(xiàn)輸出,我這里是:
2 non-memoized call: 2.210ms 2 memoized call: 0.054ms 復(fù)制代碼可以發(fā)現(xiàn),速度還是提升了不少。
Memoization: 該什么時(shí)候使用
在這里,memoization通常會縮短執(zhí)行時(shí)間并影響我們應(yīng)用程序的性能。當(dāng)我們知道一組輸入將產(chǎn)生某個(gè)輸出時(shí),memoization最有效。
遵循最佳實(shí)踐,應(yīng)該在純函數(shù)上實(shí)現(xiàn)memoization。純函數(shù)輸入什么就返回什么,不存在副作用。
記住這個(gè)是以空間換速度,所以最好確定你是否值得那么做,有些場景很有必要使用。
在處理遞歸函數(shù)時(shí),Memoization最有效,遞歸函數(shù)用于執(zhí)行諸如GUI渲染,Sprite和動(dòng)畫物理等繁重操作。
Memoization: 什么時(shí)候不要使用
不是純函數(shù)的時(shí)候(輸出不完全依賴于輸入)。
使用案例:斐波那契系列(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) } 復(fù)制代碼如果num超過2,則此函數(shù)是遞歸的。它以遞減方式遞歸調(diào)用自身。
log(fibonacci(4)) // 3 復(fù)制代碼讓我們根據(jù)memoized版本對運(yùn)行斐波那契的有效性進(jìn)行測試。 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") 復(fù)制代碼接下來調(diào)用:
node memo.js profiling tests for fibonacci 8 non-memoized call: 1.027ms 8 memoized call: 0.046ms 復(fù)制代碼可以發(fā)現(xiàn),很小的一個(gè)數(shù)字,時(shí)間差距就那么大了。
上面是參考原文,下面是個(gè)人感想。
咋說呢, 第一時(shí)間想到了react的memo組件(注意 這里,現(xiàn)版本(16.6.3)有兩個(gè)memo,一個(gè)是React.memo,還有一個(gè)是React.useMemo, 我們這里說的是useMemo),相信關(guān)注react動(dòng)態(tài)的都知道useMemo是新出來的hooks api,并且這個(gè)api是作用于function組件,官方文檔寫的是這個(gè)可以優(yōu)化用以優(yōu)化每次渲染的耗時(shí)工作。
看文檔這里介紹的也挺明白。今天看到medium的這篇文章,感覺和react memo有關(guān)系,就去看了下源碼,發(fā)現(xiàn)的確是和本文所述一樣。
export function useMemo<T>(nextCreate: () => T,inputs: Array<mixed> | void | null, ): T {currentlyRenderingFiber = resolveCurrentlyRenderingFiber(); //返回一個(gè)變量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`是不是同一個(gè)),如果是就直接返回,否則繼續(xù)向下if (areHookInputsEqual(nextInputs, prevInputs)) {return prevState[0];}}const nextValue = nextCreate(); //執(zhí)行useMemo傳入的第一個(gè)參數(shù)(函數(shù))workInProgressHook.memoizedState = [nextValue, nextInputs]; // 存入memoizedState以便下次對比使用return nextValue; } 復(fù)制代碼進(jìn)行了緩存(workInProgressHook.memoizedState就是hook返回的對象并且包含memoizedState,進(jìn)行對比前后的inputs是否相同,然后再次進(jìn)行操作),并且支持傳遞第二個(gè)數(shù)組參數(shù)作為key。
果然, useMemo就是用的本文提到的memoization來提高性能的。
其實(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.
個(gè)人學(xué)習(xí)記錄--歡迎star&watch 一起學(xué)習(xí)哦
總結(jié)
以上是生活随笔為你收集整理的了解JavaScript中的Memoization以提高性能,再看React的应用的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: iOS提交审核:您的 App 正在使用广
- 下一篇: jQuery处理点击父级checkbox