編寫(xiě)更快的 React 代碼(一):memoize-one 簡(jiǎn)介
引言
不同類(lèi)型業(yè)務(wù)要求的性能標(biāo)準(zhǔn)各不相同。如果對(duì)一個(gè) ToB 的后臺(tái)管理系統(tǒng)要求首屏速度以及 SEO育谬,顯然不合理也沒(méi)必要棍郎。
第一要考慮的不是如何去優(yōu)化晒骇,而是值不值得去優(yōu)化避咆,React 性能已經(jīng)足夠優(yōu)秀裕坊,畢竟“過(guò)早優(yōu)化是魔鬼”包竹,情況總是“可以,但沒(méi)必要”籍凝。
作為一個(gè)開(kāi)發(fā)人員周瞎,深入了解工具不足之處,并擁有對(duì)其進(jìn)行優(yōu)化的能力饵蒂,是極其重要的声诸。
React 性能優(yōu)化大抵可分為兩點(diǎn):
- 減少 rerender 次數(shù) (immutable data、shouldComponentUpdate退盯、PureComponent)
- 減輕 rerender 復(fù)雜度 (memoize-one)
本文基于 memoize-one 對(duì) render 方法進(jìn)行優(yōu)化彼乌,達(dá)到減輕不必要 render 復(fù)雜度的效果泻肯。
存在的問(wèn)題
先看一個(gè)簡(jiǎn)單的組件,如下所示:
class Example extends Component {
state = {
filterText: ""
};
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
const filteredList = this.props.list.filter(item =>
item.text.includes(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>
);
}
}
該組件接收父組件傳遞的 list慰照,篩選出包含 filterText 的 filteredList 并進(jìn)行展示灶挟。
問(wèn)題是什么?
在未進(jìn)行任何處理的情況下毒租,父組件 render稚铣,總會(huì)導(dǎo)致子組件 render,即使子組件的 state/props 并未發(fā)生變化墅垮,如果篩選的數(shù)據(jù)量大惕医,篩選邏輯復(fù)雜,這將是一個(gè)很重要的優(yōu)化點(diǎn)算色。
要達(dá)到怎樣的效果抬伺?
- state(filterText)/props(list)未發(fā)生變化時(shí),不進(jìn)行 render(引言-性能優(yōu)化第 1 點(diǎn))灾梦, 此處暫不討論
- state(filterText)/props(list)未發(fā)生變化時(shí)峡钓,進(jìn)行 render,復(fù)用上一次計(jì)算結(jié)果
memoize-one
A memoization library which only remembers the latest invocation
基本使用
import memoize from "memoize-one";
const add = (a, b) => a + b; // 基本計(jì)算方法
const memoizedAdd = memoize(add); // 生成可緩存的計(jì)算方法
memoizedAdd(1, 2); // 3
memoizedAdd(1, 2); // 3
// Add 函數(shù)沒(méi)有被執(zhí)行:上一次的結(jié)果直接返回
memoizedAdd(2, 3); // 5
// Add 函數(shù)被調(diào)用獲取新的結(jié)果
memoizedAdd(2, 3); // 5
// Add 函數(shù)沒(méi)有被執(zhí)行:上一次的結(jié)果直接返回
memoizedAdd(1, 2); // 3
// Add 函數(shù)被調(diào)用獲取新的結(jié)果
// 即使該結(jié)果在之前已經(jīng)緩存過(guò)了
// 但它并不是最近一次的緩存結(jié)果斥废,所以緩存結(jié)果丟失了
在了解基本使用后椒楣,我們來(lái)對(duì)上述案例進(jìn)行優(yōu)化。
優(yōu)化案例
import memoize from "memoize-one";
class Example extends Component {
state = { filterText: "" };
// 只有在list或filterText改變的時(shí)候才會(huì)重新調(diào)用真正的filter方法(memoize入?yún)ⅲ? filter = memoize((list, filterText) =>
list.filter(item => item.text.includes(filterText))
);
handleChange = event => {
this.setState({ filterText: event.target.value });
};
render() {
// 在上一次render后牡肉,如果參數(shù)沒(méi)有發(fā)生改變捧灰,`memoize-one`會(huì)重復(fù)使用上一次的返回結(jié)果
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>
);
}
}
源碼解析
如果除去 ts 相關(guān)以及注釋?zhuān)坏?20 行。memoize-one 本質(zhì)是一個(gè)高階函數(shù)统锤,真正計(jì)算函數(shù)作為參數(shù)毛俏,返回一個(gè)新的函數(shù),新的函數(shù)內(nèi)部會(huì)緩存上一次入?yún)⒁约吧弦淮畏祷刂邓橇绻敬稳雲(yún)⑴c上一次入?yún)⑾嗟然涂埽瑒t返回上一次返回值,否則逾雄,重新調(diào)用真正的計(jì)算函數(shù)阀溶,并緩存入?yún)⒁约敖Y(jié)果,供下一次使用鸦泳。
假裝這里有一張流程圖 :)
// 默認(rèn)比較先后入?yún)⑹欠裣嗟鹊姆椒ㄒ停褂谜呖勺远x比較方法
import areInputsEqual from './are-inputs-equal';
// 函數(shù)簽名
export default function<ResultFn: (...any[]) => mixed>(
resultFn: ResultFn,
isEqual?: EqualityFn = areInputsEqual,
): ResultFn {
// 上一次的this
let lastThis: mixed;
// 上一次的參數(shù)
let lastArgs: mixed[] = [];
// 上一次的返回值
let lastResult: mixed;
// 是否已經(jīng)初次調(diào)用過(guò)了
let calledOnce: boolean = false;
// 被返回的函數(shù)
const result = function(...newArgs: mixed[]) {
// 如果參數(shù)或this沒(méi)有發(fā)生變化或非初次調(diào)用
if (calledOnce && lastThis === this && isEqual(newArgs, lastArgs)) {
// 直接返回上一次的計(jì)算結(jié)果
return lastResult;
}
// 參數(shù)發(fā)生變化或者是初次調(diào)用
lastResult = resultFn.apply(this, newArgs);
calledOnce = true;
// 保存當(dāng)前參數(shù)
lastThis = this;
// 保存當(dāng)前結(jié)果
lastArgs = newArgs;
// 返回當(dāng)前結(jié)果
return lastResult;
};
// 返回新的函數(shù)
return (result: any);
}
拓展:斐波那契數(shù)列
下面是一個(gè)計(jì)算斐波那契數(shù)列的例子,該例子使用迭代代替遞歸做鹰,并且利用閉包緩存之前的結(jié)果击纬。
const createFab = () => {
const cache = [0, 1, 1];
return n => {
if (typeof cache[n] !== "undefined") {
return cache[n];
}
for (let i = 3; i <= n; i++) {
if (typeof cache[i] !== "undefined") continue;
cache[i] = cache[i - 1] + cache[i - 2];
}
return cache[n];
};
};
const fab = createFab();
總結(jié)
本文基于 React 介紹了 memoize-one 庫(kù)的相關(guān)使用及其原理,在 React 中實(shí)現(xiàn)了類(lèi)似與 Vue 計(jì)算屬性(computed)的效果 —— 基于依賴(lài)緩存計(jì)算結(jié)果钾麸,達(dá)到減輕不必要 render 復(fù)雜度的效果更振。
從業(yè)務(wù)開(kāi)發(fā)角度來(lái)講炕桨,Vue 提供的 API 極大地提高了開(kāi)發(fā)效率。
React 自身解決的問(wèn)題并不多肯腕,但得益于活躍的社區(qū)献宫,工作中遇到的解決問(wèn)題都能找到解決方案,并且在摸索這些解決方案的同時(shí)实撒,我們能夠?qū)W習(xí)到諸多經(jīng)典的編程思想遵蚜,從而減輕對(duì)框架的依賴(lài)。
I’ve always said that React will make you a better JavaScript developer. - Tyler McGinnis
參考鏈接
- You Probably Don't Need Derived State奈惑,by React Team
- 記憶化技術(shù) memoize-one, by Leon
- memoize-one睡汹,by alexreardon
- 計(jì)算屬性和偵聽(tīng)器肴甸,by Vue