React 的內聯函數和性能

React 的內聯函數和性能

我和妻子近期完成了一次聲勢浩大的裝修。咱們火燒眉毛地想向人們展現咱們的新意。咱們讓個人婆婆來參觀,她走進那間裝修得很漂亮的臥室,擡頭看了看那扇構造精巧的窗戶,而後說:「竟然沒有百葉窗?」😐html

咱們的新臥室;天哪,它看起來就像一張雜誌的照片。並且,沒有百葉窗。前端

我發現,當我談論 React 的時候,會有一樣的情緒。我將經過研討會的第一堂課,展現一些很酷的新特性。老是有人說:「內聯函數? 我據說它們很慢。」react

並不老是這樣,但最近幾個月這個觀點天天都會出現。做爲一名講師和代碼庫的做者,這讓人感到精疲力竭。不幸的是,我可能有點傻,以前只知道在 Twitter 上咆哮,而不是去寫一些可能對別人來講有深入看法的東西。因此,我就來嘗試一下更好的選擇了 😂。android

「內聯函數」是什麼

在 React 的語境中,內聯函數是指在 React 進行 "rendering" 時定義的函數。 人們經常對 React 中 "render" 的兩種含義感到困惑,一種是指在 update 期間從組件中獲取 React 元素(調用組件的 render 方法);另外一種是渲染更新真實的 DOM 結構。本文中提到的 "rendering"都是指第一種。ios

下列是一些內聯函數的栗子🌰:git

class App extends Component {
  // ...
  render() {
    return (
      <div>
        
        {/* 1. 一個內聯的「DOM組件」事件處理程序 */}
        <button
          onClick={() => {
            this.setState({ clicked: true })
          }}
        >
          Click!
        </button>
        
        {/* 2. 一個「自定義事件」或「操做」 */}
        <Sidebar onToggle={(isOpen) => {
          this.setState({ sidebarIsOpen: isOpen })
        }}/>
        
        {/* 3. 一個 render prop 回調 */}
        <Route
          path="/topic/:id"
          render={({ match }) => (
            <div>
              <h1>{match.params.id}</h1>}
            </div>
          )
        />
      </div>
    )
  }
}
複製代碼

過早的優化是萬惡之源

在開始下一步以前,咱們須要討論一下如何對程序進行優化。詢問任意一個性能方面的專家他們都會告訴你不要過早地優化你的程序。是的,全部具備豐富的性能調優經驗的人,都會告訴你不要過早地優化你的代碼。github

若是你不去進行測量,你甚至不知道你所作的優化是使得程序變好仍是變得更糟。chrome

我記得個人朋友 Ralph Holzmann 發表的關於 gzip 如何工做的演講,這個演講鞏固了我對此的見解。他談到了一個他用古老的腳本加載庫 LABjs 作的實驗。你能夠觀看這個視頻的 30:02 到 32:35 來了解它,或者繼續閱讀本文。vim

當時 LABjs 的源碼在性能上作了一些使人尷尬的事情。它沒有使用普通的對象表示法(obj.foo),而是將鍵存儲在字符串中,並使用方括號表示法來訪問對象(obj[stringForFoo])。這樣作的想法源於,通過小型化和 gzip 壓縮以後,非天然編寫的代碼將比天然編寫的代碼體積小。你能夠在這裏看到它後端

Ralph fork 了源代碼,沒有去考慮如何優化以實現小型化 和 gzip,而是經過天然地編寫代碼移除了優化的部分。

事實證實,移除「優化部分」後,文件大小削減了 5.3%!若是你不去進行測量,你甚至不知道你所作的優化是使得程序變好仍是變得更糟!

過早的優化不只會佔用開發時間,損害代碼的整潔,甚至會產生拔苗助長的結果致使性能問題,就像 LABjs 那樣。若是做者一直在進行測量,而不只僅是想象性能問題,就會節省開發時間,同時能讓代碼更簡潔,性能更好。

不要過早地進行優化。好了,回到 React 。

爲何人們說內聯函數很慢?

兩個緣由:內存/垃圾回收問題和 shouldComponentUpdate

內存和垃圾回收

首先,人們(和 eslint configs)擔憂建立內聯函數產生的內存和垃圾回收成本。在箭頭函數普及以前,不少代碼都會內聯地調用 bind ,這在歷史上表現不佳。例如:

<div>
  {stuff.map(function(thing) {
    <div>{thing.whatever}</div>
  }.bind(this)}
</div>
複製代碼

Function.prototype.bind 的性能問題在此獲得瞭解決,並且箭頭函數要麼是原生函數,要麼是由 Babel 轉換爲普通函數;在這兩種狀況下,咱們均可以假定它並不慢。

記住,你不要坐在那裏而後想象「我賭這個代碼確定慢」。你應該天然地編寫代碼,而後測量它。若是存在性能問題,就修復它們。咱們不須要證實一個內聯的箭頭函數是快的,也不須要另外一些人來證實它是慢的。不然,這就是一個過早的優化。

據我所知,尚未人對他們的應用程序進行分析,代表內聯箭頭函數很慢。在進行分析以前,這甚至不值得談論 —— 但不管如何,我會提供一個新思路 😝

若是建立內聯函數的成本很高,以致於須要使用 eslint 規則來規避它,那麼咱們爲何要將該開銷轉移到初始化的熱路徑上呢?

class Dashboard extends Component {
  state = { handlingThings: false }
  
  constructor(props) {
    super(props)
    
    this.handleThings = () =>
      this.setState({ handlingThings: true })

    this.handleStuff = () => { /* ... */ }

    // bind 的開銷更昂貴
    this.handleMoreStuff = this.handleMoreStuff.bind(this)
  }

  handleMoreStuff() { /* ... */ }

  render() {
    return (
      <div>
        {this.state.handlingThings ? (
          <div>
            <button onClick={this.handleStuff}/>
            <button onClick={this.handleMoreStuff}/>
          </div>
        ) : (
          <button onClick={this.handleThings}/>
        )}
      </div>
    )
  }
}
複製代碼

由於過早地優化,咱們已經將組件的初始化速度下降了 3 倍!若是全部處理程序都是內聯的,那麼在初始化中只須要建立一個函數。相反的,咱們則要建立 3 個。咱們沒有測量任何東西,因此沒有理由認爲這是一個問題。

若是你想徹底忽略這一點,那麼就去制定一個 eslint 規則,來要求在任何地方都使用內聯函數來加快初始渲染速度🤦🏾‍♀。

PureComponent 和 shouldComponentUpdate

這纔是問題真正的癥結所在。你能夠經過理解兩件事來看到真正的性能提高: shouldComponentUpdate 和 JavaScript 嚴格相等的比較。若是不能很好地理解它們,就可能在無心中以性能優化的名義使 React 代碼更難處理。

當你調用 setState 時,React 會將舊的 React 元素與一組新的 React 元素進行比較(這稱爲 r_econciliation_ ,你能夠在這裏閱讀相關資料 ),而後使用該信息更新真實的 DOM 元素。有時候,若是你有不少元素須要檢查,這個過程就會變得很慢(好比一個大的 SVG )。React 爲這類狀況提供了逃生艙口,名叫 shouldComponentUpdate

class Avatar extends Component {
  shouldComponentUpdate(nextProps, nextState) {
    return stuffChanged(this, nextProps, nextState))
  }
  
  render() {
    return //...
  }
}
複製代碼

若是你的組件定義了 shouldComponentUpdate ,那麼在 React 進行新舊元素對比以前,它會詢問 shouldComponentUpdate 有沒有變動發生。若是返回了false,那麼React將會直接跳過元素diff檢查,從而節省一些時間。若是你的組件足夠大,這會對性能產生至關大的影響。

優化組件的最多見方法是擴展 "React.PureComponent" 而不是 "React.Component" 。一個 PureComponent 會在 shouldComponentUpdate 中比較 props 和 state ,這樣你就不用手動執行了。

class Avatar extends React.PureComponent { ... }
複製代碼

當被要求更新時,Avatar 會對它的 props 和 state 使用一個嚴格相等比較,但願以此來加快速度。

嚴格相等比較

JavaScript 中有六種基本類型:string, number, boolean, null, undefined, 和 symbol。當你對兩個值相同的基本類型進行「嚴格相等比較」的時候,你會獲得一個 true 值。舉個例子🌰:

const one = 1
const uno = 1
one === uno // true
複製代碼

PureComponent 比較 props 時,它會使用嚴格相等比較。這對內聯原始值很是有效: <Toggler isOpen={true}/>

prop 的比較只會在有非原始類型們出現的時候產生問題——啊,說錯了,抱歉,是類型而不是類型們。只有一種其餘類型,那就是 Object。你問函數和數組?事實上,它們都是對象(Object)。

函數是具備附加的可調用功能的常規對象。

哈哈哈,不愧是 JavaScript。不管如何,對對象使用嚴格相等檢查,即便表面上看起來相等的值,也會被斷定爲 false(不相等):

const one = { n: 1 }
const uno = { n: 1 }
one === uno // false
one === one // true
複製代碼

因此,若是你在 JSX 中內聯地使用一個對象,它會使 PureComponent 的 prop diff 檢查失效,轉而使用較昂貴的方式對 React 元素進行 diff 檢查。元素的 diff 將變爲空,這樣就浪費了兩次進行差別比較的時間。

// 第一次 render
<Avatar user={{ id: 'ryan' }}/>

// 下一次 render
<Avatar user={{ id: 'ryan' }}/>

// prop diff 認爲有東西發生了變化,由於 {} !== {}
// 元素 diff 檢查 (reconciler) 發現沒有任何變化
複製代碼

因爲函數是對象,並且 PureComponent 會對 props 進行嚴格相等的檢查,所以,一個內聯的函數將老是沒法經過 prop 的 diff 檢查,從而轉向 reconciler 中的元素 diff 檢查。

能夠看出,這不只僅只關乎內聯函數。函數簡直就是 object, function, array 三部曲演繹推廣的主唱。

爲了讓 shouldComponentUpdate 高興,你必須保持函數的引用標識。對經驗豐富的 JavaScript 開發者來講,這不算糟。可是 Michael 和我領導了一個有3500多人蔘加的研討會,他們的開發經驗各不相同,而這對不少人來講都並不容易。ES 的類也沒有提供引導咱們進入各類 JavaScript 路徑的幫助:

class Dashboard extends Component {
  constructor(props) {
    super(props)
    
    // 使用 bind ?拖慢初始化的速度,看上去不妙
    // 當你有 20 個 bind 的時候(我見過你的代碼,我知道)
    // 它會增長打包後文件的大小
    this.handleStuff = this.handleStuff.bind(this)

    // _this 一點也不優雅
    var _this = this
    this.handleStuff = function() {
      _this.setState({})
    }
    
    // 若是你會用 ES 的類,那你極可能會使用箭頭
    // 函數(經過 babel ,或使用現代瀏覽器)。這不是很難可是
    // 把你全部的處理程序都放在構造函數中就
    // 不太好了
    this.handleStuff = () => {
      this.setState({})
    }
  }
  
  // 這個很不錯,但它不是 JavaScript ,至少如今還不是,因此如今
  // 咱們要討論的是 TC39 如何工做,並評估咱們的草案
  // 階段風險容忍度
  handleStuff = () => {}
}
複製代碼

學習如何保持函數的引用標識將會引出一個使人驚訝的長篇大論。

一般沒有理由強迫人們這麼作,除非有一個 eslint 配置對他們大喊大叫。我想展現的是,內聯函數和提高性能二者能夠兼得。但首先,我想講一個我本身遇到的性能相關的故事。

我使用 PureComponent 的經歷

當我第一次瞭解到 PureRenderMixin(在 React 的早期版本中叫這個,後來改成 PureComponent )時,我進行了大量的測試,來測試個人應用程序的性能。而後,我將 PureRenderMixin 添加到每一個組件中。當我採起了一套優化後的測量方法時,我但願有一個關於一切變得有多快的很酷的故事能夠講。

讓人大跌眼鏡的是,個人應用程序變慢了 🤔。

爲何呢?仔細想一想,若是你有一個 Component ,會有多少次 diff 檢查?若是你有一個 PureComponent ,又會有多少次 diff 檢查?答案分別是「只有一次」和「至少一次,有時是兩次」。若是一個組件常常在更新時發生變化,那麼 PureComponent 將會執行兩次 diff 檢查而不是一次(props 和 state 在 shouldComponentUpdate 中進行的嚴格相等比較,以及常規的元素 diff 檢查)。這意味着一般它會變慢,偶爾會變快。顯然,個人大部分組件大部分時間都在變化,因此總的來講,個人應用程序變慢了。啊哦😯。

在性能方面沒有銀彈。你必須測量。

三種情景

在本文的開頭,我展現了三種內聯函數。如今咱們已經瞭解了一些背景,讓咱們來一一討論一下它們。可是請記住,在你有一個衡量標準來斷定以前,請先將 PureComponent 束之高閣。

DOM 組件事件處理程序

<button
  onClick={() => this.setState(…)}
>click</button>
複製代碼

一般,在 buttons,inputs,和其餘 DOM 組件的事件處理程序中,除了 setState 之外,不會作其餘的事情。這讓內聯函數成爲了一般狀況下最乾淨的方法。它們不是在文件中跳來跳去尋找事件處理程序,而是把內容放在同一位置。React 社區一般歡迎這種方式。

button 組件(以及全部其餘的DOM組件)甚至都算不上是 PureComponent,因此這裏也不存在 shouldComponentUpdate 引用標識的問題。

因此,認爲這個過程很慢的惟一緣由是,你是否定爲簡單地定義一個函數會產生足以讓人擔憂的開銷。咱們已經討論過,這在任何地方都未被證明。這只是紙上談兵的性能假設。在被證明以前,這樣作沒問題。

一個「自定義事件」或「操做」

<Sidebar onToggle={(isOpen) => {
  this.setState({ sidebarIsOpen: isOpen })
}}/>
複製代碼

若是 SidebarPureComponent,咱們將會打破 prop 的 diff 檢查。再一次,因爲處理程序很簡單,最好把它們都放在同一位置。

對於像 onToggle 這樣的事件,Sidebar 還有什麼必要對它執行 diff 檢查呢?只有兩種狀況才須要將 prop 包含在 shouldComponentUpdate 的 diff 檢查中:

  1. 你使用 prop 來進行渲染
  2. 你使用 prop 來在 componentWillReceivePropscomponentDidUpdate,或者 componentWillUpdate 中產生一些其餘的做用

大多數 on<whatever> prop 都不符合這些要求。所以,多數 PureComponent 的用法都會致使屢次執行 diff 檢查,迫使開發人員沒必要要地維護處理程序的引用標識。

咱們只應該對會產生影響的 prop 執行 diff 檢查。這樣,人們就能夠將處理程序放在同一位置,而且仍然能夠得到想要尋求的性能提高(並且因爲咱們關心性能,因此咱們但願執行更少次數的 diff 檢查!)

對於大多數組件,我建議建立一個 PureComponentMinusHandlers 類並從中繼承,而不是從 PureComponent 中繼承。它能夠跳過對函數的全部檢查。魚與熊掌兼得。

好吧,差很少是這樣的。

若是你接收到一個函數並直接將它傳遞給另外一個組件,它將會沒法及時更新。看一下這個:

// 1. App 會傳遞一個 prop 給 From 表單
// 2. Form 將向下傳遞一個函數給 button
//    這個函數與它從 App 獲得的 prop 相接近
// 3. App 會在 mounting 以後 setState,並傳遞
//    一個**新**的 prop 給 Form
// 4. Form 傳遞一個新的函數給 Button,這個函數與
//    新的 prop 相接近
// 5. Button 會忽略新的函數, 並沒有法
//    更新點擊處理程序,從而提交陳舊的數據

class App extends React.Component {
  state = { val: "one" }

  componentDidMount() {
    this.setState({ val: "two" })
  }

  render() {
    return <Form value={this.state.val} />
  }
}

const Form = props => (
  <Button
    onClick={() => {
      submit(props.value)
    }}
  />
)

class Button extends React.Component {
  shouldComponentUpdate() {
    // 讓咱們僞裝比較了除函數之外的一切東西
    return false
  }

  handleClick = () => this.props.onClick()

  render() {
    return (
      <div>
        <button onClick={this.props.onClick}>這個的數據是舊的</button>
        <button onClick={() => this.props.onClick()}>這個工做正常</button>
        <button onClick={this.handleClick}>這個也工做正常</button>
      </div>
    )
  }
}
複製代碼

這是一個運行該應用程序的沙箱

所以,若是你喜歡從 PureRenderWithoutHandlers 繼承的想法,請確保永遠不要將你要在 diff 檢查中要忽略的處理程序直接傳遞給其餘組件——你須要以某種方式包裝它們。

如今,咱們要麼必須維護引用標識,要麼必須避免引用標識!歡迎來到性能優化。至少在這種方法中,必須處理的是優化組件,而不是使用它的代碼。

我要坦率地說,這個示例應用程序是我在發佈 Andrew Clark 後所作的編輯,它引發了個人注意。在這裏,您認爲我足夠聰明,知道何時管理引用標識,何時無論理了吧!😂

一個 render prop

<Route
  path="/topic/:id"
  render={({ match }) => (
    <div>
      <h1>{match.params.id}</h1>}
    </div>
  )
/>
複製代碼

用來渲染的 prop 是一種模式,它用來建立一個用於組成和管理共享狀態的組件。(你能夠在這裏瞭解更多)。它的內容對組件來講是未知的,舉個栗子🌰:

const App = (props) => (
  <div>
    <h1>Welcome, {props.name}</h1>
    <Route path="/" render={() => (
      <div>
        {/*
          prop.name 是從路由外部傳入的,它不是做爲 prop 傳遞進來的,
          所以路由不能可靠地成爲一個PureComponent,它
          不知道在組件內部會渲染什麼
        */}
        <h1>Hey, {props.name}, let's get started!</h1> </div> )}/> </div> ) 複製代碼

這意味着一個內聯的用來渲染的 prop 函數不會致使 shouldComponentUpdate 的問題:它永遠沒有足夠的信息來成爲一個 PureComponent

因此,惟一的反對意見又回到了相信簡單地定義一個函數是緩慢的。重複第一個例子:沒有證據支持這一觀點。這只是紙上談兵的性能假設。

總結

  1. 天然地編寫代碼,設計代碼
  2. 測量你的交互,找到慢在哪裏。這裏是方法.
  3. 僅在須要的時候使用 PureComponentshouldComponentUpdate,避免使用 prop 函數(除非它們在生命週期的鉤子函數中爲產生某種做用而使用)。

若是你真的相信過早的優化不是好主意,那麼你就不須要證實內聯函數是快的,而是須要證實它們是慢的。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索