1. 程式人生 > 實用技巧 >新手學習 react 迷惑的點

新手學習 react 迷惑的點

網上各種言論說react上手比vue難,可能難就難不能深刻理解jsX,或者對 ES6 的一些特性理解得不夠深刻,導致覺得有些點難以理解,然後說react比較難上手,還反人類啥的,所以我打算寫兩篇文章來講新手學習 React 的時候容易迷惑的點寫出來

為什麼要引入 React

在寫 React 的時候,你可能會寫類似這樣的程式碼:

import React from 'react'

function A() {
  // ...other code
  return <h1>前端桃園</h1>
}

你肯定疑惑過,下面的程式碼都沒有用到 React,為什麼要引入 React 呢?

如果你把import React from ‘react’刪掉,還會報下面這樣的錯誤:

7F6E506E-3025-401D-A492-3B501F8081C6

那麼究竟是哪裡用到了這個 React,導致我們引入 React 會報錯呢,不懂這個原因,那麼就是jsX 沒有搞得太明白。

你可以講上面的程式碼(忽略匯入語句)放到線上 babel 裡進行轉化一下,發現 babel 會把上面的程式碼轉化成:

function A() {
  // ...other code
  return React.createElement("h1", null, "前端桃園");
}

因為從本質上講,JSX 只是為React.createElement(component, props, ...children)函式提供的語法糖。

為什麼要用 className 而不用 class

  1. React 一開始的理念是想與瀏覽器的 DOM API 保持一直而不是html,因為 JSX 是 JS 的擴充套件,而不是用來代替html的,這樣會和元素的建立更為接近。在元素上設定class需要使用className這個 API:

    const element = document.createElement("div")
    element.className = "hello" 
    
  2. 瀏覽器問題,ES5 之前,在物件中不能使用保留字。以下程式碼在 IE8 中將會丟擲錯誤:

    const element = {
     attributes: {
       class: "hello"
     }
    } 
    
  3. 解構問題,當你在解構屬性的時候,如果分配一個class變數會出問題:

    const { class } = { class: 'foo' } // Uncaught SyntaxError: Unexpected token }
    const { className } = { className: 'foo' } 
    const { class: className } = { class: 'foo' } 
    

其他討論可見:有趣的話題,為什麼jsx用className而不是class

為什麼屬性要用小駝峰

因為 JSX語法上更接近JavaScript而不是 HTML,所以 React DOM 使用camelCase(小駝峰命名)來定義屬性的名稱,而不使用 HTML 屬性名稱的命名約定。

為什麼 constructor 裡要呼叫 super 和傳遞 props

這是官網的一段程式碼,具體見:狀態(State) 和 生命週期

class Clock extends React.Component {
  constructor(props) {
    super(props);
    this.state = {date: new Date()};
  }

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

而且有這麼一段話,不僅讓我們呼叫super還要把props傳遞進去,但是沒有告訴我們為什麼要這麼做。

image-20190901222456704

不知道你有沒有疑惑過為什麼要呼叫super和傳遞props,接下來我們來解開謎題吧。

為什麼要呼叫 super

其實這不是 React 的限制,這是JavaScript的限制,在建構函式裡如果要呼叫 this,那麼提前就要呼叫 super,在 React 裡,我們常常會在建構函式裡初始化 state,this.state = xxx,所以需要呼叫 super。

為什麼要傳遞 props

你可能以為必須給super傳入props,否則React.Component就沒法初始化this.props:

class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

不過,如果你不小心漏傳了props,直接呼叫了super(),你仍然可以在render和其他方法中訪問this.props(不信的話可以試試嘛)。

為啥這樣也行?因為React 會在建構函式被呼叫之後,會把 props 賦值給剛剛建立的例項物件:

const instance = new YourComponent(props);
instance.props = props;

props不傳也能用,是有原因的。

但這意味著你在使用 React 時,可以用super()代替super(props)了麼?

那還是不行的,不然官網也不會建議你呼叫 props 了,雖然 React 會在建構函式執行之後,為this.props賦值,但在super()呼叫之後與建構函式結束之前,this.props仍然是沒法用的。

// Inside React
class Component {
  constructor(props) {
    this.props = props;
    // ...
  }
}

// Inside your code
class Button extends React.Component {
  constructor(props) {
    super(); // :grimacing: 忘了傳入 props
    console.log(props); // :white_check_mark: {}
    console.log(this.props); // :grimacing: undefined
  }
  // ...
}

要是建構函式中呼叫了某個訪問props的方法,那這個 bug 就更難定位了。因此我強烈建議始終使用super(props),即使這不是必須的:

class Button extends React.Component {
  constructor(props) {
    super(props); // :white_check_mark: We passed props
    console.log(props); // :white_check_mark: {}
    console.log(this.props); // :white_check_mark: {}
  }
  // ...
}

上面的程式碼確保this.props始終是有值的。

如果你想避免以上的問題,你可以通過class 屬性提案 來簡化程式碼:

class Clock extends React.Component {
  state = {date: new Date()};

  render() {
    return (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}

更詳細的內容可見Dan 的部落格

為什麼元件用大寫開頭

前面以及說過了,JSX 是React.createElement(component, props, …children)提供的語法糖,component 的型別是:string/ReactClass type,我們具體看一下在什麼情況下會用到 string 型別,什麼情況下用到 ReactClass type 型別

  • string 型別react會覺得他是一個原生dom節點

  • ReactClass type 型別 自定義元件

例如(string):在 jsx 中我們寫一個

<div></div>

轉換為js的時候就變成了

React.createElement("div", null)

例如(ReactClass type):在jsx中我們寫一個

function MyDiv() {
    return (<div><div>)
}
<MyDiv></MyDiv>

轉換為js的時候就變成了

function MyDiv() {
  return React.createElement("div", null);
}

React.createElement(MyDiv, null);

上邊的例子中如果將MyDiv中的首字母小寫,如下

function myDiv() {
    return (<div><div>)
}
<myDiv></myDiv>

轉換為 js 的時候就變成了

function MyDiv() {
  return React.createElement("div", null);
}

React.createElement("myDiv", null);

由於找不到 myDiv 這個 dom,所以就會報錯。

為什麼呼叫方法要 bind this

前提知識:深刻的理解 JavaScript 中的 this

相信剛寫 React 的時候,很多朋友可能會寫類似這樣的程式碼:

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

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

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

你必須謹慎對待 JSX 回撥函式中的this,在 JavaScript 中,class 的方法預設不會繫結this。如果你忘記繫結this.handleClick並把它傳入了onClick,當你呼叫這個函式的時候this的值為undefined。

這並不是 React 特有的行為;這其實與 JavaScript 函式工作原理有關。通常情況下,如果你沒有在方法後面新增(),例如onClick={this.handleClick},你應該為這個方法繫結this。

然後你看了官網的例子和建議之後,知道需要為事件處理函式繫結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裡的物件

PPT模板下載大全https://www.wode007.com

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 非常重要的一個方法,值得大家好好去研究一下他的原理。