新手學習 react 迷惑的點(二)

沒看第一篇的朋友能夠移步先去看第一篇:新手學習 react 迷惑的點(一)javascript

第一篇反響也還不錯,不少新手都以爲頗有幫助,解答了他們好久以來的疑惑,其實第一篇裏面的還算基礎的,主要是 ES6 語法和 JSX 沒有深入理解。php

這第二篇稍微要難一點,有的須要瞭解 React 的原理才能搞明白的,不過你放心,我都用了最簡單最簡單的語言,即便你是個新手,若是產生了這些疑問,你也能看懂。html

下面開始吧!前端

爲何調用方法要 bind this

前提知識: 深入的理解 JavaScript 中的 thisvue

相信剛寫 React 的時候,不少朋友可能會寫相似這樣的代碼:java

class Foo extends React.Component {
  handleClick () {
    this.setState({ xxx: aaa })
  }

  render() {
    return (
      <button onClick={this.handleClick}> Click me </button>
    )
  }
}
複製代碼

發現會報 thisundefined 的錯,而後可能對事件處理比較疑惑,而後去看官網的事件處理有下面一段話:react

你必須謹慎對待 JSX 回調函數中的 this,在 JavaScript 中,class 的方法默認不會綁定this。若是你忘記綁定 this.handleClick 並把它傳入了 onClick,當你調用這個函數的時候 this 的值爲 undefinedgit

這並非 React 特有的行爲;這其實與 JavaScript 函數工做原理有關。一般狀況下,若是你沒有在方法後面添加 (),例如 onClick={this.handleClick},你應該爲這個方法綁定 thisgithub

而後你看了官網的例子和建議以後,知道須要爲事件處理函數綁定 this就能解決,想下面這樣:後端

class Foo extends React.Component {
  handleClick () {
    this.setState({ xxx: aaa })
  }

  render() {
    return (
      <button onClick={this.handleClick.bind(this)}> Click me </button>
    )
  }
}
複製代碼

可是可能你沒有去思考過爲何須要 bind this?若是你不能理解的話,仍是 js 的基礎沒有打好。

React 是如何處理事件的?

我們先來了解一下 React 是如何處理事件的。

React 的事件是合成事件, 內部原理很是複雜,我這裏只把關鍵性,能夠用來解答這個問題的原理部分進行介紹便可(後面應該會寫一篇 react 的事件原理,敬請期待)。

上篇文章已經說過,jsx 其實是 React.createElement(component, props, …children) 函數提供的語法糖,那麼這段 jsx 代碼:

<button onClick={this.handleClick}>
     Click me
 </button>
複製代碼

會被轉化爲:

React.createElement("button", {
     onClick: this.handleClick
}, "Click me")
複製代碼

瞭解了上面的,而後簡單的理解 react 如何處理事件的,React 在組件加載(mount)和更新(update)時,將事件經過 addEventListener 統一註冊到 document 上,而後會有一個事件池存儲了全部的事件,當事件觸發的時候,經過 dispatchEvent 進行事件分發。

因此你能夠簡單的理解爲,最終 this.handleClick 會做爲一個回調函數調用。

理解了這個,而後再來看看回調函數爲何就會丟失 this

this 簡單回顧

在函數內部,this的值取決於函數被調用的方式。

若是你不能理解上面那句話,那麼你可能須要停下來閱讀文章,去查一下相關資料,不然你可能看不懂下面的,若是你懶的話,就看爲你準備好的 MDN 吧。

經過上面對事件處理的介紹,來模擬一下在類組件的 render 函數中, 有點相似於作了這樣的操做:

class Foo {
  sayThis () {
    console.log(this); // 這裏的 `this` 指向誰?
  }
  
  exec (cb) {
    cb();
  }
  
  render () {
    this.exec(this.sayThis);
  }
}

var foo = new Foo();
foo.render(); // 輸出結果是什麼?
複製代碼

你會發現最終結果輸出的是 undefined,若是你不理解爲何輸出的是 undefined,那麼仍是上面說的,須要去深入的理解 this 的原理。若是你能理解輸出的是 undefined,那麼我以爲你就能夠理解爲何須要 bind this 了。

那麼你可能會問:**爲何React沒有自動的把 bind 集成到 render 方法中呢?**在 exec 調用回調的時候綁定進去,像這樣:

class Foo {
  sayThis () {
    console.log(this); // 這裏的 `this` 指向誰?
  }

 exec (cb) {
   cb.bind(this)();
 }

 render () {
   this.exec(this.sayThis);
 }
}

var foo = new Foo();
foo.render(); // 輸出結果是什麼?
複製代碼

由於 render 屢次調用每次都要 bind 會影響性能,因此官方建議你本身在 constructor 中手動 bind 達到性能優化。

四種事件處理對比

對於事件處理的寫法也有好幾種,我們來進行對比一下:

1. 直接 bind this 型

就是像文章開始的那樣,直接在事件那裏 bind this

class Foo extends React.Component {
  handleClick () {
    this.setState({ xxx: aaa })
  }

  render() {
    return (
      <button onClick={this.handleClick.bind(this)}> Click me </button>
    )
  }
}
複製代碼

優勢:寫起來順手,一口氣就能把這個邏輯寫完,不用移動光標到其餘地方。

缺點:性能不太好,這種方式跟 react 內部幫你 bind 同樣的,每次 render 都會進行 bind,並且若是有兩個元素的事件處理函數式同一個,也仍是要進行 bind,這樣會多寫點代碼,並且進行兩次 bind,性能不是太好。(其實這點性能每每不會是性能瓶頸的地方,若是你以爲順手,這樣寫徹底沒問題)

2. constuctor 手動 bind 型

class Foo extends React.Component {
  constuctor(props) {
    super(props)
    this.handleClick = this.handleClick.bind(this)
  }
  handleClick () {
    this.setState({ xxx: aaa })
  }

  render() {
    return (
      <button onClick={this.handleClick}> Click me </button>
    )
  }
}
複製代碼

優勢: 相比於第一種性能更好,由於構造函數只執行一次,那麼只會 bind 一次,並且若是有多個元素都須要調用這個函數,也不須要重複 bind,基本上解決了第一種的兩個缺點。

缺點: 沒有明顯缺點,硬要說的話就是太醜了,而後不順手(我以爲醜,你以爲不醜就這麼寫就好了)。

3. 箭頭函數型

class Foo extends React.Component {
  handleClick () {
    this.setState({ xxx: aaa })
  }

  render() {
    return (
      <button onClick={(e) => this.handleClick(e)}> Click me </button>
    )
  }
}
複製代碼

優勢: 順手,好看。

缺點: 每次 render 都會重複建立函數,性能會差一點。

4. public class fields 型

這種 class fields還處於實驗階段,據我所知目前尚未被歸入標準,具體可見這裏

class Foo extends React.Component {
  handleClick = () => {
    this.setState({ xxx: aaa })
  }

  render() {
    return (
      <button onClick={this.handleClick}> Click me </button>
    )
  }
}
複製代碼

優勢: 好看,性能好。

缺點: 沒有明顯缺點,若是硬要說可能就是要多裝一個 babel 插件來支持這種語法。

總結

我平時用的就這四種寫法,我這邊從代碼的美觀性、性能以及是否順手方便對各類寫法作了簡單的對比。其實每種方法在項目裏用都是沒什麼問題的,性能方面基本上能夠忽略,對於美觀性和順手比較主觀,因此整體來講就是看你們的偏好咯,若是硬要推薦的話,我仍是比較推薦第四種寫法,美觀並且不影響性能。

爲何要 setState,而不是直接 this.state.xx = oo

這個問題是咱們公司後端寫 React 的時候提出的問題,爲啥不能直接修改 state,要 setState 一下。我在想,從 vue 轉到 React 可能也會有這種疑問,由於 vue 修改狀態都是直接改的。

若是咱們瞭解 setState 的原理的話,可能就能解答這個問題了,setState 作的事情不只僅只是修改了 this.state 的值,另外最重要的是它會觸發 React 的更新機制,會進行 diff ,而後將 patch 部分更新到真實 dom 裏。

若是你直接 this.state.xx == oo 的話,state 的值確實會改,可是改了不會觸發 UI 的更新,那就不是數據驅動了。

那爲何 Vue 直接修改 data 能夠觸發 UI 的更新呢?由於 Vue 在建立 UI 的時候會把這些 data 給收集起來,而且在這些 data 的訪問器屬性 setter 進行了重寫,在這個重寫的方法裏會去觸發 UI 的更新。若是你想更多的瞭解 vue 的原理,能夠去購買染陌大佬的剖析 Vue.js 內部運行機制

不明白訪問器屬性的能夠看這篇文章:深刻理解JS裏的對象

setState 是同步仍是異步相關問題

1. setState 是同步仍是異步?

個人回答是執行過程代碼同步的,只是合成事件和鉤子函數的調用順序在更新以前,致使在合成事件和鉤子函數中無法立馬拿到更新後的值,形式了所謂的「異步」,因此表現出來有時是同步,有時是「異步」。

2. 什麼時候是同步,什麼時候是異步呢?

只在合成事件和鉤子函數中是「異步」的,在原生事件和 setTimeout/setInterval等原生 API 中都是同步的。簡單的能夠理解爲被 React 控制的函數裏面就會表現出「異步」,反之表現爲同步。

3. 那爲何會出現異步的狀況呢?

爲了作性能優化,將 state 的更新延緩到最後批量合併再去渲染對於應用的性能優化是有極大好處的,若是每次的狀態改變都去從新渲染真實 dom,那麼它將帶來巨大的性能消耗。

4. 那如何在表現出異步的函數裏能夠準確拿到更新後的 state 呢?

經過第二個參數 setState(partialState, callback) 中的 callback 拿到更新後的結果。

或者能夠經過給 setState 傳遞函數來表現出同步的狀況:

this.setState((state) => {
	return { val: newVal }
})
複製代碼

5. 那表現出異步的原理是怎麼樣的呢?

直接講源碼確定篇幅不夠,能夠看這篇文章:你真的理解setState嗎?

我這裏仍是用最簡單的語言讓你理解:在 React 的 setState 函數實現中,會根據 isBatchingUpdates(默認是 false) 變量判斷是否直接更新 this.state 仍是放到隊列中稍後更新。而後有一個 batchedUpdate 函數,能夠修改 isBatchingUpdates 爲 true,當 React 調用事件處理函數以前,或者生命週期函數以前就會調用 batchedUpdate 函數,這樣的話,setState 就不會同步更新 this.state,而是放到更新隊列裏面後續更新。

這樣你就能夠理解爲何原生事件和 setTimeout/setinterval 裏面調用 this.state 會同步更新了吧,由於經過這些函數調用的 React 沒辦法去調用 batchedUpdate 函數將 isBatchingUpdates 設置爲 true,那麼這個時候 setState 的時候默認就是 false,那麼就會同步更新。

最後

setState 是 React 很是重要的一個方法,值得你們好好去研究一下他的原理。

參考文章

後記

上一篇發出以後,有不少小夥伴留言說想看關於 hooks 相關的問題,畢竟 hooks 出來沒多久,有不少疑問很正常,下一篇估計就專門寫 hooks 相關的吧。


我是桃翁,一個愛思考的前端er,想了解關於更多的前端相關的,請關注個人公號:「前端桃園」,若是想加入交流羣關注公衆號後回覆「微信」拉你進羣

相關文章
相關標籤/搜索