由一道React基礎面試題引起的思考

提問:react項目中的JSX裏,onChange={this.func.bind(this)}的寫法,爲何要比非bind的func = () => {}的寫法效率高?javascript

聲明: 因爲本人水平有限,有考慮不周之處,或者出現錯誤的,請嚴格指出,小弟感激涕零。這是小弟第一篇文章,有啥潛規則不懂的,大家就告訴我。小弟明天有分享,等分享完了以後,繼續完善。java

以前不經意間看到這道題,聽說是阿里p5-p6級別的題目,咱們先看一下這道題目,明面上是考察對react的瞭解深度,實際上涉及的考點不少:bind,arrow function,react各類綁定this的方法,優缺點,適合的場景,類的繼承,原型鏈等等,因此綜合性很強。react

咱們今天的主題就是由此題目,來總結一下相關的知識點,這裏我會着重分析題目中第二種綁定方案git

五種this綁定方案的差別性

方案一: React.createClass

這是老版本React中用來聲明組件的方式,在那個版本,沒有引入class這種概念,因此經過這種方式來建立一個組件類(constructor) ES6的class相比createClass,移除了兩點:一個是mixin 一個是this的自動綁定。前者能夠用HOC替代,後者則是完徹底全的沒有,緣由是FB認爲這樣能夠避免和JS的語法產生混淆,因此去掉了。 使用這種方法,咱們不須要擔憂this,它會自動綁定到組件實例身上,可是這個API已經廢棄了,因此只須要了解。es6

const App = React.createClass({
  handleClick() {
    console.log(this)
  },
  render() {
    return <div onClick={this.handleClick}>你好</div>
  }
})
複製代碼

方案二:在render函數中使用bind

class Test extends Component {
  handleClick() {
    console.log(this)
  }
  render() {
    return <div onClick={this.handleClick.bind(this)}></div>
  }
}
複製代碼

方案三:在render函數中使用箭頭函數

class Test extends Component {

  handleClick() {
    console.log(this)
  }
  render() {
    return <div onClick={() => this.handleClick()}></div>
  }
}
複製代碼

這兩個方案簡潔明瞭,能夠傳參,可是也存在潛在的性能問題: 會引發沒必要要的渲染github

咱們經常會在代碼中看到這些場景: 更多演示案例請點擊面試

class Test extends Component {
  render() {
    return <div>
      <Input />
      <button>添加<button>
      <List options={this.state.options || Immutable.Map()} data={this.state.data} onSelect={this.onSelect.bind(this)} /> // 1 pureComponent
    </div>
  }
}
複製代碼

場景一:使用空對象/數組來作兜底方案,避免options沒有數據時運行時報錯。 場景二:使用箭頭函數來綁定this。數組

可能在一些不須要關心性能的場景下這兩種寫法沒有什麼太大的壞處,可是若是咱們正在考慮性能優化,譬如咱們使用了PureComponent來去優化咱們的渲染性能 這裏面React有使用shallowEqual作第一層的比較,這個時候咱們關注的多是這個data(數據是否有變化從而影響渲染),然而被咱們忽視的options,onSelect卻會直接致使PureComponent失效,然而咱們找不到優化失敗的緣由。瀏覽器

而假設咱們的核心data是Immutable的,這樣其實優化了咱們作diff相關的性能。當data爲null時,此時咱們指望的是不會重複渲染,然而當咱們的Test組件有狀態更新,觸發了Test的從新渲染,此時render執行,List依舊會從新渲染。緣由就是咱們每次執行render,傳遞給子組件的options,onSelect是一個新的對象/函數。這樣在作shallowEqual時,會認爲有更新,因此會更新List組件。性能優化

這個地方也有不少解決方案:

  1. 不要直接在render函數裏面作兜底,或者使用同一引用的數據源
  2. 對於事件監聽函數,咱們能夠事先作好綁定,使用方案4或者5,或者最新的hook(useCallback、useMemo)
const onSelect = useCallback(() => {
  ... //和select相關的邏輯
}, []) // 第二個參數是相關的依賴,只有依賴變了,onSelect纔會變,設置爲空數組,表示永遠不變
複製代碼

方案四:在構造函數中使用bind

class Test extends Component {
  constrcutor() {
    this.handleClick = this.handleClick.bind(this)
  }
  
  handleClick() {
    console.log(this)
  }

  render() {
    return <Button onClick={this.handleClick}>測試</Button>
  }
}
複製代碼

這種方案是React推薦的方式,只在實例化組件的時候作一次綁定,以後傳遞的都是同一引用,沒有方案2、三帶來的負面效應。

可是這種寫法相對2,3繁瑣了許多:

1. 若是咱們並不須要在構造函數裏作什麼的話,爲了作函數綁定,咱們須要手動聲明構造函數; 這裏沒有考慮到實例屬性的新寫法,直接在頂層賦值。感謝@Yes好2012指正。

  1. 針對一些複雜的組件(要綁定的方法過多),咱們須要屢次重複的去寫這些方法名;
  2. 沒法單獨處理傳參問題(這一點尤爲重要,也限制了它的使用場景)。

方案五:使用箭頭函數定義方法(class properties)

這種技術依賴於Class Properties提案,目前還在stage-2階段,若是須要使用這種方案,咱們須要安裝@babel/plugin-proposal-class-properties

class Test extends Component {
  handleClick = () => {
    console.log(this)
  }
  
  render() {
    return <button onClick={this.handleClick}>測試</button>
  }
}
複製代碼

這也是咱們面試題中提到的第二種綁定方案 先總結一下優勢:

  1. 自動綁定
  2. 沒有方案2、三所帶來的渲染性能問題(只綁定一次,沒有生成新的函數);
  3. 能夠再封裝一下,使用params => () => {}這種寫法來達到傳參的目的。

咱們在babel上作一下編譯:點擊class-properties(選擇ES2016或者更高,須要手動安裝一下這個pluginbabel-plugin-transform-class-properties相比於@babel/plugin-proposal-class-properties更直觀,前者是babel6命名方式,後者是babel7)

在使用plugin編譯後的版本咱們能夠看到,這種方案其實就是直接在構造函數中定義了一個change屬性,而後賦值爲箭頭函數,從而實現的對this的綁定,看起來很完美,很精妙。然而,正是由於這種寫法,意味着由這個組件類實例化的全部組件實例都會分配一塊內存來去存儲這個箭頭函數。而咱們定義的普通方法,實際上是定義在原型對象上的,被全部實例共享,犧牲的代價則是須要咱們使用bind手動綁定,生成了一個新的函數。

咱們看一下bind函數的polyfill:

if (!Function.prototype.bind) {
    ... // do sth
    var fBound  = function() {
          // this instanceof fBound === true時,說明返回的fBound被當作new的構造函數調用
          return fToBind.apply(this instanceof fBound
                 ? this
                 : oThis,
                 // 獲取調用時(fBound)的傳參.bind 返回的函數入參每每是這麼傳遞的
                 aArgs.concat(Array.prototype.slice.call(arguments)));
        };
    ... // do sth

    return fBound;
  };
}
複製代碼

若是在不支持bind的瀏覽器上,其實編譯後,也就至關於新生成的函數的函數體就一條語句: fToBind.apply(...)

咱們以圖片的形式看一下差距:

注: 圖中,虛線框面積表明引用函數所節省的內存,實線框的面積表明消耗的內存。 圖一:使用箭頭函數作this綁定。只有render函數定義在原型對象上,由全部實例對象共享。其餘內存消耗都是基於每一個實例上的。 圖二:在構造函數中作this綁定。render,handler都定義在原型對象上,實例上的handler實線框表明使用bind生成的函數所消耗的內存大小。

若是咱們的handler函數體自己就很小,實例數量很少,綁定的方法很少。兩種方案在內存佔用上的差別性不大,可是一旦咱們要在handler裏處理複雜的邏輯,或者該組件可能會產生大量的實例,抑或是該組件有大量的須要綁定方法,第一種的優點就突顯出來了。

若是說上面這種綁定this的方案只用在React上,可能咱們只須要考慮上面幾點,可是若是咱們使用上面的方法去建立一些工具類,可能注意的不止這些。

說到類,可能你們都會想到類的繼承,若是咱們須要重寫某個基類的方法,運行下面,你會發現,和想象中的相差甚遠。

class Base {
  sayHello() {
    console.log('Hello')
  }
  
  sayHey = () => {
    console.log('Hey')
  }
}

class A extends Base {
  constructor() {
    super()
    this.name = 'Bitch'
  }
  
  sayHey() {
    console.log('Hey', this.name)
  }
}

new A().sayHello()  // 'Hello'
new A().sayHey() // 'Hey'
複製代碼

注: 咱們但願打印出 'Hello' 'Hey Bitch',實際打印的是:'Hello' 'Hey'

緣由很簡單,在A的構造函數內,咱們調用super執行了Base的構造函數,向A實例上添加屬性,這個時候執行Base構造函數後,A實例上已經有了sayHey屬性,它的值是一個箭頭函數,打印出·Hey· 而咱們重寫的sayHey實際上是定義在原型對象上的。因此最終執行的是在Base裏定義的sayHey方法,但不是同一個方法。 據此,咱們還能夠推理一下假設咱們要先執行Base的sayHey,而後在此基礎上執增長邏輯咱們又該怎麼作?下面這種方案確定是行不通的。

sayHey() {
  super.sayHey() // 報錯
  console.log('get off!')
}
複製代碼

多說一句: 有大佬認爲這種方法的性能並很差,它考察的點是ops/s(每秒能夠實例化多少個組件,越多越好),最終得出的結論是

可是就有人提出質疑,這些方法咱們最終都會經過babel編譯成瀏覽器能識別的代碼,那麼最終運行的版本所體現的差別性是否可以表明其真實的差別性。具體的我也沒細看,有須要瞭解更多的,能夠 看一下這篇文章 Arrow Functions in Class Properties Might Not Be As Great As We Think

據此,咱們已經cover了這道題多數考點,若是下次碰到這種題,或者想出這類題不妨從下面的角度去考慮下

  1. 面試者的角度: 1.1 在回答這道題以前,寫解釋兩種方案的原理,顯然,面試官想要着重考察的是第二種的瞭解狀況,他背後到底作了什麼。而後談談他們一些常規的優缺點 1.2 回答關於效率的問題,前者每次bind,都會生成一個新的函數,可是函數體內代碼量少,最重要的仍是引用的原型上的handler,這個是共享的。可是後面這一種,他會在每一個實例上生成一個函數,若是實例數量多,或者函數體大,或者是綁定函數過多,那麼佔用的內存就明顯要超出第一種。
  2. 面試官的角度: 考bind實現,考react的綁定策略,優缺點,考性能優化策略,考箭頭函數, 考原型鏈,考繼承。發散開來,真的很廣。

總結:

每種綁定方案既然存在就有其存在的理由(除了第一種已是過去),可是也會有相應的弊端,並無絕對的誰好誰差,咱們在使用時,能夠根據實際場景作選擇。 這道題目答到點不難,怎樣讓面試官以爲你懂得全面仍是挺難的。

其次針對this綁定方案, 若是特別在乎性能,犧牲一點代碼量,可讀性:推薦四其次,若是本身自己夠細心,二三也可使用,可是必定要注意新生成的函數是否會致使多餘渲染; 若是想不加班:推薦五(如何傳參文章中有說起)。

參考

相關文章
相關標籤/搜索