在一個CPU密集型應用中,咱們可使用Memoization來進行優化,其主要用於經過存儲昂貴的函數調用的結果來加速程序,並在再次發生相同的輸入時返回緩存的結果。
例如一個簡單的求平方根的函數:javascript
const sqrt = Math.sqrt; //使用cache緩存 const sqrt = (arg)=>{ if(!sqrt.cache){ sqrt.cache = {}; } if(!sqrt.cache[arg]){ sqrt.cache[arg] = Math.sqrt(arg) } return sqrt.cache[arg] } //簡單的運行時間對比 //第一次運行: console.time('start1') sqrt(779) console.timeEnd('start1') VM516:3 start1: 0.01806640625ms //第二次運行: console.time('start1') sqrt(779) console.timeEnd('start1') VM521:3 start1: 0.005859375ms
咱們實現一個通用的memoize函數,用它來包裝任意純函數,並緩存其計算結果。html
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的原理是在發生相同的輸入時返回緩存的結果,這意味着其包裝的函數應當是一個純函數,當函數不純時,相同的輸入在不一樣時間可能返回不一樣的結果,此時再使用memoize明顯不合時宜。前端
在該庫中,使用Map緩存計算結果,支持傳入resolver函數將傳入的參數轉換爲存入Map的鍵名(默認鍵名是第一個參數,當鍵名是引用類型時,引用不變,緩存不會更新)。java
function memoize(func, resolver) { if (typeof func != 'function' || (resolver != null && typeof resolver != 'function')) { throw new TypeError('Expected a function') } const memoized = function(...args) { const key = resolver ? resolver.apply(this, args) : args[0] const cache = memoized.cache if (cache.has(key)) { return cache.get(key) } const result = func.apply(this, args) memoized.cache = cache.set(key, result) || cache return result } memoized.cache = new (memoize.Cache || Map) return memoized } memoize.Cache = Map
與lodash不一樣,memoize-one僅僅保存上一次調用時的結果,若是下次參數變化,則更新緩存。所以沒必要擔憂因爲maxAge, maxSize, exclusions等致使的內存泄漏。react
//源碼 export default function<ResultFn: (...any[]) => mixed>( resultFn: ResultFn, isEqual?: EqualityFn = areInputsEqual, ): ResultFn { let lastThis: mixed; let lastArgs: mixed[] = []; let lastResult: mixed; let calledOnce: boolean = false; //函數調用過,this沒有變化,參數isEqual時返回緩存值。 const result = function(...newArgs: mixed[]) { if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) { return lastResult; } lastResult = resultFn.apply(this, newArgs); calledOnce = true; lastThis = this; lastArgs = newArgs; return lastResult; }; return (result: any); }
能夠看到,能夠經過第二個參數自定義參數是否相同,默認是areInputsEqual函數。git
//areInputsEqual實現 export default function areInputsEqual( newInputs: mixed[], lastInputs: mixed[], ) { //先進行參數長度比較 if (newInputs.length !== lastInputs.length) { return false; } //參數淺比較 for (let i = 0; i < newInputs.length; i++) { if (newInputs[i] !== lastInputs[i]) { return false; } } return true; }
能夠自定義參數比較函數進行深比較github
import memoizeOne from 'memoize-one'; import isDeepEqual from 'lodash.isequal'; const identity = x => x; const shallowMemoized = memoizeOne(identity); const deepMemoized = memoizeOne(identity, isDeepEqual); const result1 = shallowMemoized({ foo: 'bar' }); const result2 = shallowMemoized({ foo: 'bar' }); result1 === result2; // false - difference reference const result3 = deepMemoized({ foo: 'bar' }); const result4 = deepMemoized({ foo: 'bar' }); result3 === result4; // true - arguments are deep equal
在React中有這樣一個應用場景,當部分props變化須要改變派生state時,能夠在getDerivedStateFromProps中經過if判斷是否須要從新計算,但它比它須要的更復雜,由於它必須單獨跟蹤和檢測每一個props和state的變化,當props不少時,這種判斷方式會變得糾纏不清。redux
class Example extends Component { state = { filterText: "", }; static getDerivedStateFromProps(props, state) { if ( props.list !== state.prevPropsList || state.prevFilterText !== state.filterText ) { return { prevPropsList: props.list, prevFilterText: state.filterText, filteredList: props.list.filter(item => item.text.includes(state.filterText)) }; } return null; } handleChange = event => { this.setState({ filterText: event.target.value }); }; render() { return ( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{this.state.filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ); } }
而經過Memoization,能夠把上一次的計算結果保存下來,而避免重複計算。例如如下經過memoize-one庫實現的記憶,可讓咱們不用手動判斷list, filterText是否變化,是否須要從新計算。segmentfault
import memoize from "memoize-one"; class Example extends Component { // State only needs to hold the current filter text value: state = { filterText: "" }; // Re-run the filter whenever the list array or filter text changes: filter = memoize( (list, filterText) => list.filter(item => item.text.includes(filterText)) ); handleChange = event => { this.setState({ filterText: event.target.value }); }; render() { const filteredList = this.filter(this.props.list, this.state.filterText); return ( <Fragment> <input onChange={this.handleChange} value={this.state.filterText} /> <ul>{filteredList.map(item => <li key={item.id}>{item.text}</li>)}</ul> </Fragment> ); } }
在react中,每當組件從新渲染,計算派生狀態的邏輯就會執行一遍(無論這些邏輯是放在render或者是放在getDerivedStateFromProps中,若是沒有采用不少if限制的話)。上節介紹了使用memoize-one來緩存來避免重複計算,當咱們使用redux時,一般在mapStateToProps中計算派生狀態,每當store中的任意state更新時,都會觸發mapStateToProps中的計算,然而,每每派生state一般只依賴部分state,沒必要每次都計算。
Reselect是一個十分貼近redux的memoize庫,其和memoize-one同樣只緩存前一次的計算值,並支持自定義memoize函數,自定義參數比較函數等;其輸入參數由inputSelectors functions 產生,生成的的依然是inputSelectors functions,這意味的與memoize相比,這能夠很容易的組合。
另外,mapStateProps的第二個參數ownProps也能夠傳入selector中。數組
import { createSelector } from 'reselect' fSelector = createSelector( a => state.a, b => state.b, (a, b) => f(a, b) ) hSelector = createSelector( b => state.b, c => state.c, (b, c) => h(b, c) ) gSelector = createSelector( a => state.a, c => state.c, (a, c) => g(a, c) ) uSelector = createSelector( a => state.a, b => state.b, c => state.c, (a, b, c) => u(a, b, c) ) ... function mapStateToProps(state) { const { a, b, c } = state return { a, b, c, fab: fSelector(state), hbc: hSelector(state), gac: gSelector(state), uabc: uSelector(state) } }
若是你的函數組件在給定相同的props的狀況下呈現相同的結果,你能夠React.memo經過記憶結果將它包裝在一些調用中以提升性能。這意味着React將跳過渲染組件,並重用最後渲染的結果。
默認狀況下,它只會淺顯比較props對象中的複雜對象。若是要控制比較,還能夠提供自定義比較函數做爲第二個參數。
function MyComponent(props) { /* render using props */ } function areEqual(prevProps, nextProps) { /* return true if passing nextProps to render would return the same result as passing prevProps to render, otherwise return false */ } export default React.memo(MyComponent, areEqual);
You Probably Don’t Need Derived State - React Blog
Understanding Memoization in JavaScript to Improve Performance
性能優化:memoization | Taobao FED | 淘寶前端團隊
lodash/memoize.js at master · lodash/lodash · GitHub
GitHub - alexreardon/memoize-one: A memoization library which only remembers the latest invocation
爲何咱們須要reselect - 從0開始實現react技術棧 - SegmentFault 思否
React Hooks: Memoization – Sandro Dolidze – Medium