對React一些原理的理解

莫凡_Tcg發表於2019-03-04

隨著專案開發的深入,不可避免了遇到了一些問題。剛開始出現問題時很懵,不知道該怎麼解決,原因就是對React的原理理解的不夠透徹,不知道問題出在哪。在解決問題的過程中,也逐漸深入瞭解了React的一些原理,這篇文章就來分享一下我對React一些原理的理解。

注意 這篇文章並不是教程,只是我對React原理的一些個人理解,歡迎與我一起討論。文章不對的地方,還請讀者費心指出^-^

概述

本文是《使用React技術棧的一些收穫》系列文章的第二篇(第一篇在這裡,介紹如何開始構建React大型專案),簡單介紹了React一些原理,包括React合成事件系統、元件的生命週期以及setState()

React合成事件系統

React快速的原因之一就是React很少直接操作DOM,瀏覽器事件也是一樣。原因是太多的瀏覽器事件會佔用很大記憶體。

React為此自己實現了一套合成系統,在DOM事件體系基礎上做了很大改進,減少了記憶體消耗,簡化了事件邏輯,最大化解決瀏覽器相容問題。

其基本原理就是,所有在JSX宣告的事件都會被委託在頂層document節點上,並根據事件名和元件名儲存回撥函式(listenerBank)。每次當某個元件觸發事件時,在document節點上繫結的監聽函式(dispatchEvent)就會找到這個元件和它的所有父元件(ancestors),對每個元件建立對應React合成事件(SyntheticEvent)並批處理(runEventQueueInBatch(events)),從而根據事件名和元件名呼叫(invokeGuardedCallback)回撥函式。

因此,如果你採用下面這種寫法,並且這樣的P標籤有很多個:

 listView = list.map((item,index) => {
    return (        
        <p onClick={this.handleClick} key={item.id}>{item.text}</p>
  )})複製程式碼

That's OK,React幫你實現了事件委託。我之前因為不瞭解React合成事件系統,還顯示的使用了事件委託,現在看來是多此一舉的。

由於React合成事件系統模擬事件冒泡的方法是構建一個自己及父元件佇列,因此也帶來一個問題,合成事件不能阻止原生事件,原生事件可以阻止合成事件。用 event.stopPropagation() 並不能停止事件傳播,應該使用 event.preventDefault()

如果你想詳細瞭解React合成事件系統,移步blog.csdn.net/u013510838/…

元件的生命週期(以父子元件為例)

為了搞清楚元件生命週期,構造一個父元件包含子元件並且重寫各生命週期函式的場景:

 class Child extends React.Component {
  constructor() {
    super()
    console.log('Child was created!')
  }
  componentWillMount(){
    console.log('Child componentWillMount!')
  }
  componentDidMount(){
    console.log('Child componentDidMount!')
  }
  componentWillReceiveProps(nextProps){
    console.log('Child componentWillReceiveProps:'+nextProps.data )
  }
  shouldComponentUpdate(nextProps, nextState){
    console.log('Child shouldComponentUpdate:'+ nextProps.data)
    return true
  }
  componentWillUpdate(nextProps, nextState){
    console.log('Child componentWillUpdate:'+ nextProps.data)
  }
  componentDidUpdate(){
    console.log('Child componentDidUpdate')
  }
  render() {
    console.log('render Child!')
    return (      
      <h1>Child recieve props: {this.props.data}</h1>      
    );
  }
}

class Father extends React.Component {
  // ... 前面跟子元件一樣
  handleChangeState(){
    this.setState({randomData: Math.floor(Math.random()*50)})
  }
  render() {
    console.log('render Father!')
    return (
      <div>
        <Child data={this.state.randomData} />
        <h1>Father State: { this.state.randomData}</h1>      
        <button onClick={this.handleChangeState}>切換狀態</button>
      </div>
    );
  }
}

React.render(
  <Father />,
  document.getElementById('root')
);複製程式碼

結果如下:剛開始![Alt text](https://ooo.0o0.ooo/2017/06/28/595398ee54b1a.png)對React一些原理的理解

呼叫父元件的setState後:![Alt text](https://ooo.0o0.ooo/2017/06/28/595398ff8a44a.png)對React一些原理的理解

在Jsbin上試試看

有一張圖能說明這之間的流程(圖片來源):![Alt text](https://ooo.0o0.ooo/2017/06/28/595398b92d076.png)對React一些原理的理解

setState並不奇怪

有一個能反映問題的場景:

 ...
state = {
	count: 0
}
componentDidMount() {
  this.setState({count: this.state.count + 1})
  this.setState({count: this.state.count + 1})
  this.setState({count: this.state.count + 1})
}
...複製程式碼

看起來state.count被增加了三次,但結果是增加了一次。這並不奇怪:

React快的原因之一就是,在執行this.setState()時,React沒有忙著立即更新state,只是把新的state存到一個佇列(batchUpdate)中。上面三次執行setState只是對傳進去的物件進行了合併,然後再統一處理(批處理),觸發重新渲染過程,因此只重新渲染一次,結果只增加了一次。這樣做是非常明智的,因為在一個函式裡呼叫多個setState是常見的,如果每一次呼叫setState都要引發重新渲染,顯然不是最佳實踐。React官方文件裡也說了:

Think of setState() as a request rather than an immediate command to update the component.

setState() 看作是重新render的一次請求而不是立刻更新元件的指令。

那麼呼叫this.setState()後什麼時候this.state才會更新?答案是即將要執行下一次的render函式時。

這之間發生了什麼?setState呼叫後,React會執行一個事務(Transaction),在這個事務中,React將新state放進一個佇列中,當事務完成後,React就會重新整理佇列,然後啟動另一個事務,這個事務包括執行 shouldComponentUpdate 方法來判斷是否重新渲染,如果是,React就會進行state合併(state merge),生成新的state和props;如果不是,React仍然會更新this.state,只不過不會再render了。

開發人員對setState感到奇怪的原因可能就是按照上述寫法並不能產生預期效果,但幸運的是我們改動一下就可以實現上述累加效果:這歸功於setState可以接受函式作為引數:

setState(updater, [callback])

 ...
state = {
	score: 0
}
componentDidMount() {
    this.setState( (prevState) => ({score : prevState.score + 1}) )
    this.setState( (prevState) => ({score : prevState.score + 1}) )
    this.setState( (prevState) => ({score : prevState.score + 1}) )
  } 複製程式碼

}這個updater可以為函式,該函式接受該元件前一刻的 state 以及當前的 props 作為引數,計算和返回下一刻的 state。

你會發現達到增加三次的目的了: 在Jsbin上試試看

這是因為React會把setState裡傳進去的函式放在一個任務佇列裡,React 會依次呼叫佇列中的函式,傳遞給它們前一刻的 state。

另外,不知道你在jsbin上的程式碼上注意到沒有,呼叫setStateconsole.log(this.state.score)輸出仍然為0,也就是this.state並未改變,並且只render了一次。

總結

學習一個框架或者工具,我覺得應該瞭解以下幾點:

  1. 它是什麼?能做什麼?

  2. 它存在的理由是什麼?解決了什麼樣的問題、滿足了什麼樣的需求?

  3. 它的適用場景是什麼?優缺點是什麼?

  4. 它怎麼用?最佳實踐是什麼?

  5. 它的原理是什麼?

  6. ...

通過對React一些原理的簡單瞭解,就懂得了React為什麼這麼快速的原因之一,也會在問題出現時知道錯在什麼地方,知道合理的解決方案。


相關文章