React 函數式組件優化

前言

React 推出後,前後出現了三種定義組件的方式,分別是:css

  • 函數式組件
  • React.createClass 建立組件
  • React.Component 建立組件

相信你們在平常中使用的最多的仍是函數式組件和 React.Component 組件吧,今天就簡單的說下函數式組件的兩個優化方法。react

函數式組件

什麼是函數式組件

在談到函數式組件以前咱們先看一個概念 - 純函數數組

何爲純函數?dom

引用一段維基百科的概念。函數

在程序設計中,若一個函數符合如下要求,則它可能被認爲是純函數性能

此函數在相同的輸入值時,需產生相同的輸出。函數的輸出和輸入值之外的其餘隱藏信息或狀態無關,也和由I/O設備產生的外部輸出無關。測試

該函數不能有語義上可觀察的函數反作用,諸如「觸發事件」,使輸出設備輸出,或更改輸出值之外物件的內容等。優化

能夠看到,純函數有着相同的輸入一定產生相同的輸出,沒有反作用的特性。this

同理,函數式組件的輸出也只依賴於 propscontext,與 state 無關。spa

函數式組件的特色

  • 沒有生命週期
  • 無組件實例,沒有 this(相信不少同窗被 this 煩過)
  • 沒有內部狀態(state)

函數式組件的優勢

  • 不須要聲明 class,沒有 constructorextends等代碼,代碼簡潔,佔用內存小。
  • 不須要使用 this
  • 能夠寫成無反作用的純函數。
  • 更佳的性能。函數式組件沒有了生命週期,不須要對這部分進行管理,從而保證了更好地性能。

函數式組件的缺點

  • 沒有生命週期方法。
  • 沒有實例化。
  • 沒有 shouldComponentUpdate,不能避免重複渲染。

React.memo

一個例子

這裏先看一個例子,代碼以下,也可點擊這裏進行查看。

import * as React from "react";
import { render } from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  return (
    <div className="App">
      <div>React.memo demo-1</div>
      <button onClick={onClick}>update {n}</button>
      <Child />
    </div>
  );
}

const Child = () => {
  console.log("render child");
  return <div className="child">Child Component</div>;
};

const rootElement = document.getElementById("root");
render(<App />, rootElement);

複製代碼

它實現了什麼?就一個很簡單的東西,一個父組件包了一個子組件。

這個你們判斷一下,當我點擊父組件的 button 更新 N 的時候,子組件中的 log 會不會執行?

按照通常的思惟來看,你父組件更新關我子組件什麼事?我子組件的 DOM 又不用更新,既然沒有更新,那還打什麼 log

但實際效果是,每點擊一次 button,子組件的 log 就會執行一次,雖然子組件的 DOM 沒更新,但並不表明子組件的渲染函數沒有執行。

如下是執行的效果圖。

React.memo demo-1.gif

優化

針對上述狀況,class 組件可使用 shouldComponentUpdate 來進行優化,可是函數式組件呢?React 一樣也提供了一個優化方法,那就是 React.memo

memomemorized,意思是記住。若是輸入的參數沒有變,依據純函數的定義,輸出也不會變,那麼直接返回以前記憶的結果不就好了。

const Child = React.memo(() => {
  console.log("render child");
  return <div className="child">Child Component</div>;
});
複製代碼

Child 使用 React.memo 處理一下,這樣的話,不管你點擊多少次父組件的 button,子組件的 log 都不會執行。

完整代碼點這裏

效果圖以下:

React.memo demo-2.gif

React.useCallback

咱們將上述代碼稍微改一下, 讓 Child 接受一個匿名函數,看看會產生什麼後果。完整代碼點這裏

import * as React from "react";
import { render } from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  return (
    <div className="App">
      <div>React.useCallback demo-1</div>
      <button onClick={onClick}>update {n}</button>
      <Child onClick={() => {}} />
    </div>
  );
}

const Child = React.memo((props: { onClick: () => void }) => {
  console.log("render child");
  return <div className="child">Child Component</div>;
});

const rootElement = document.getElementById("root");
render(<App />, rootElement);
複製代碼

觀察代碼能夠看到也沒啥變化嘛,只是子組件接受了一個空函數。

那麼問題又來了,此次點擊 button 子組件的 log 會執行嗎?

看到這裏各位同窗應該會想,每次都傳一個空的匿名函數,props 也沒變啊,那就不用從新渲染唄。具體結果如何,來看下效果:

React.useCallback demo-1.gif

能夠看到每次點擊 button 時,子組件的 log 依舊會再次執行。那麼這是爲何呢?

由於每次點擊 button 更新父組件的時候,會從新生成一個空的匿名函數,雖然它們都是空的匿名函數,可是它們不是同一個函數。

函數是一個複雜類型的值,JavaScript 在比較複雜類型的值時,是對比它們的內存地址。不是同一個函數,那麼內存地址也就不一樣, React 會認爲子組件的 props 發生了變化,子組件將從新渲染。

優化

那麼怎麼保證子組件每次都接受同一個函數呢?

很簡單。既然父組件在更新的時候會從新生成一個函數,那麼我把函數放到父組件外面不就能夠了嘛,這樣父組件在更新的時候子組件就會接受同一個函數。

代碼以下。也可點擊這裏查看。

import * as React from "react";
import { render } from "react-dom";

import "./styles.css";

const childOnClick = () => {};

function App() {
  const [n, setN] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  return (
    <div className="App">
      <div>React.useCallback demo-2</div>
      <button onClick={onClick}>update {n}</button>
      <Child onClick={childOnClick} />
    </div>
  );
}

const Child = React.memo((props: { onClick: () => void }) => {
  console.log("render child");
  return <div className="child">Child Component</div>;
});

const rootElement = document.getElementById("root");
render(<App />, rootElement);
複製代碼

效果圖以下:

React.useCallback demo-2.gif

這樣子看起來好像解決了子組件每次都接受不一樣的函數致使從新渲染的問題,可是好像哪裏不對勁,實現也不優雅。

缺點

若是子組件的函數依賴父組件裏面的值,那麼這種方式就不可行。

怎麼辦呢?若是能將函數也 memorized 就行了。

Hook

React16.8.0 的版本中正式推出了 Hooks,其中有一個 Hook 叫作 useCallback,它能將函數也 memorized 化。

useCallback 接受兩個參數,第一個參數是一個函數,第二個參數是一個依賴數組,返回一個 memorized 後的函數。只有當依賴數組中的值發生了變化,它纔會返回一個新函數。

看看使用 useCallback 後的代碼,也能夠點擊這裏查看。

import * as React from "react";
import { render } from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const childOnClick = React.useCallback(() => {
    console.log(`m: ${m}`);
  }, [m]);
  return (
    <div className="App">
      <div>React.useCallback demo-3</div>
      <button
        onClick={() => {
          setN(n + 1);
        }}
      >
        update n: {n}
      </button>
      <button
        onClick={() => {
          setM(m + 1);
        }}
      >
        update m: {m}
      </button>
      <Child onClick={childOnClick} />
    </div>
  );
}

const Child = React.memo((props: { onClick: () => void }) => {
  console.log("render child");
  return (
    <div className="child">
      <div>Child Component</div>
      <button onClick={props.onClick}>log m</button>
    </div>
  );
});

const rootElement = document.getElementById("root");
render(<App />, rootElement);
複製代碼

在上述代碼中,子組件接受一個使用了 useCallback 的函數,它的依賴參數是 m,只有當 m 發生了變化,子組件接受的函數纔會是一個從新生成的函數。也就是說,不管點擊多少次更新 nbutton,子組件都不會更新,只有點擊更新 mbutton 時,子組件纔會更新。

看看效果如何:

React.useCallback demo-3.gif

實際效果符合咱們的預期。

歪心思

看到這裏有些同窗就會想了,若是使用 useCallback 的時候,傳一個空數組做爲依賴數組,那麼子組件就再也不受父組件的影響了,即便你父組件的 m 變化了,我子組件依舊不會從新渲染,這樣子豈不是性能更好?話很少說,咱們來測試一下就行了。代碼點擊這裏查看,效果以下:

React.useCallback demo-4.gif

能夠看到,雖然子組件確實沒重複渲染了,但一樣的也致使一個問題,打印出來的 m 永遠都是 0,再也讀取不到更新後的 m 的值。

由此能夠得出結論,傳一個空數組做爲依賴數組的後果就是,子組件接受的函數裏面的參數永遠都是初始化使用 useCallback 時的值,這樣的結果並非咱們想要的。

因此歪心思仍是少來了。

總結

隨着 React 正式推出 Hooks,帶來一系列新的特性,極大地加強了函數式組件的功能,利用這些新特性能夠實現和 class 組件同樣的效果。

有了 React Hooks,咱們能夠拋棄沉重的 class 組件,使用更加輕便,性能更加優異的函數式組件,所以掌握一些函數式組件的優化方法對咱們使用函數式組件開發是很是有用處的。

React Hooks 無論你香不香,反正我是先香了。

默默說一句,Vue 3.0 也會推出函數式組件。

相關文章
相關標籤/搜索