編寫(xiě)更快的 React 代碼(一):memoize-one 簡(jiǎn)介

編寫(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):

  1. 減少 rerender 次數(shù) (immutable data、shouldComponentUpdate退盯、PureComponent)
  2. 減輕 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á)到怎樣的效果抬伺?

  1. state(filterText)/props(list)未發(fā)生變化時(shí),不進(jìn)行 render(引言-性能優(yōu)化第 1 點(diǎn))灾梦, 此處暫不討論
  2. 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

參考鏈接

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市囚巴,隨后出現(xiàn)的幾起案子原在,更是在濱河造成了極大的恐慌,老刑警劉巖彤叉,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件庶柿,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡秽浇,警方通過(guò)查閱死者的電腦和手機(jī)浮庐,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)柬焕,“玉大人审残,你說(shuō)我怎么就攤上這事“呔伲” “怎么了搅轿?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)富玷。 經(jīng)常有香客問(wèn)我璧坟,道長(zhǎng),這世上最難降的妖魔是什么赎懦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任雀鹃,我火速辦了婚禮,結(jié)果婚禮上铲敛,老公的妹妹穿的比我還像新娘褐澎。我一直安慰自己,他們只是感情好伐蒋,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布工三。 她就那樣靜靜地躺著迁酸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪俭正。 梳的紋絲不亂的頭發(fā)上奸鬓,一...
    開(kāi)封第一講書(shū)人閱讀 51,737評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音掸读,去河邊找鬼串远。 笑死,一個(gè)胖子當(dāng)著我的面吹牛儿惫,可吹牛的內(nèi)容都是我干的澡罚。 我是一名探鬼主播,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼肾请,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼留搔!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起铛铁,我...
    開(kāi)封第一講書(shū)人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤隔显,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后饵逐,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體括眠,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年掷豺,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片萌业。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖奸柬,靈堂內(nèi)的尸體忽然破棺而出生年,到底是詐尸還是另有隱情,我是刑警寧澤廓奕,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布抱婉,位于F島的核電站,受9級(jí)特大地震影響桌粉,放射性物質(zhì)發(fā)生泄漏蒸绩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一铃肯、第九天 我趴在偏房一處隱蔽的房頂上張望患亿。 院中可真熱鬧,春花似錦、人聲如沸步藕。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)咙冗。三九已至沾歪,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間雾消,已是汗流浹背灾搏。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留立润,地道東北人狂窑。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像桑腮,于是被迫代替她去往敵國(guó)和親蕾域。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 作為一個(gè)合格的開(kāi)發(fā)者到旦,不要只滿足于編寫(xiě)了可以運(yùn)行的代碼。而要了解代碼背后的工作原理巨缘;不要只滿足于自己的程序...
    六個(gè)周閱讀 8,449評(píng)論 1 33
  • 原教程內(nèi)容詳見(jiàn)精益 React 學(xué)習(xí)指南添忘,這只是我在學(xué)習(xí)過(guò)程中的一些閱讀筆記,個(gè)人覺(jué)得該教程講解深入淺出若锁,比目前大...
    leonaxiong閱讀 2,840評(píng)論 1 18
  • GUIDS 第一章 為什么使用React搁骑? React 一個(gè)提供了用戶(hù)接口的JavaScript庫(kù)。 誕生于Fac...
    jplyue閱讀 3,538評(píng)論 1 11
  • 原文地址:https://medium.com/airbnb-engineering/react-native-a...
    莫寂嵐閱讀 3,271評(píng)論 0 9
  • Xamarin Android工程第一次編譯時(shí)會(huì)從谷歌網(wǎng)站下載支持包文件。一旦發(fā)現(xiàn)編譯卡死仰冠,可以直接停止編譯乏冀,雙擊...
    iManuQiao閱讀 2,275評(píng)論 0 1