是時候理清 React 開發中的一些疑惑了

zhangwang發表於2016-08-22

React其實很好上手,我在最初使用時並未去了解其一些細節性的東西,但是好像在專案中也一直能正常運作。但是那時總會有一種不安感,深感自己對React的使用邏輯並未理解得非常清晰,本文的目的就在於理清這種使用邏輯,當然個人見解定有偏頗,如果你有一些建議,也希望您能在討論區予以指教,如果你到現在還沒有怎麼接觸過React,推薦可以跟著官方文件的例子體會下React再來看本文,也許這樣收穫更大一些,後文的連結裡還有一個更加高階的例子也是非常好的入門教程。

為什麼要使用 React

這是一個老生常談的問題了,可能大家在眾多的教程、文章裡已經瞭解過了React的好處,比如說它的虛擬DOM可以被高效的渲染,比如說它的元件化使得專案結構非常清晰,程式碼複用非常容易,比如說它的資料管理機制也能讓你清晰的知曉資料的狀態,而React本身就是被這種清晰的資料所驅動的。

“We built React to solve one problem: building large applications with data that changes over time.”

詳細談論這些優點前,我想說說React給我帶來的改變。

在使用React之前,我也一直在使用jQuery,它對節點的操作非常方便,如果僅僅只是普通網頁的開發,jQuery無疑是非常好的,但是如果開發的是WebApp,jQuery並不能增強你對全域性的把控能力,在學習使用React沒多久以後突然有一天我感覺以前所做的開發都好像在玩一些小打小鬧的遊戲,而使用React也讓自己明白了為什麼我被稱作前端工程師,這個框架讓我找到了工程師的歸屬感,當我再看自己的專案時和一個建築工程師看自己的設計圖的感覺沒了太多差別,我知道我的這個元件是房子的總體框架,這個元件是大廳,這個元件是椅子,還有與大廳同級的臥室元件,廚房元件,與椅子同級的桌子元件,家電元件。

我可以用優雅的桌子,椅子,床,檯燈來佈置一個溫馨的臥室;
我可以把桌子壓扁拉長變成電視櫃,把椅子拉寬加上軟軟的海綿變成沙發,再把檯燈提高,換一個像樣的遮光罩它就是落地燈了,再加上一些客廳獨有的家電,客廳的感覺也就出來了;
用相同的思路,一個溫馨的家就出來了。

其實想想,如果只考慮客廳和臥室(不考慮裡面的那些桌子椅子之類的元件)那麼除卻它們的長、寬、擺放位置這些引數不同,它們又有什麼區別呢?

回到React,它即帶給我們對整體的把控能力,也讓我們可以通過修改資料(引數)以表現不同的細節達到不同的效果,從最大的房子的框架到每個桌子椅子的樣子,一切都在我們的掌握。下面就慢慢說說React是如何幫我們達到把握全域性,瞭解細節的。

從虛擬 DOM(Virtual DOM)說起

想象這麼一個場景,客廳裡有一把我們不是很喜歡的椅子,想換一把,最合適的做法當然就是改造一下,或者把這把丟了重新買一把新的,為了換一把椅子而重新組裝整個房子一看就是不聰明的做法。Virtual DOM為我們提供了一種高效的渲染機制,使得我們可以只改變我們想改變的地方,而儘量不去影響其它無關的元件。它是React高效能的基礎。

虛擬 DOM 究竟是什麼?

要說明Virtual DOM究竟是什麼,不得不提到React DOM模組的一個方法,ReactDOM.reader()

這個方法就像開啟一道門的鑰匙,門的兩邊就是Virtual DOM和Html DOM,我們在瀏覽器中看到的肯定是Html DOM,Virtual DOM存在於隔著這道門的系統記憶體之中,Html DOM和Virtual DOM之間存在著對映關係。
就像Html DOM由各種節點構成,Virtual DOM也是由一種被稱為React node的節點構成。

每個React元件中還有另外一個render()方法(不同於ReactDOM.reader()),我的理解是這個方法用於將ReactNode構建為Virtual DOM。下面再來詳細看看React node

React node

“a light, stateless, immutable, virtual representation of a DOM node.”

React node其實並非真實的節點,實際上它們可以看做是真實節點在Virtual DOM中的代表,Virtual DOM就是由ReactNodes構成,真實的DOM就是依據它們所構建;

在需要改變真實的DOM時,React其實是先修改虛擬DOM,然後和真實的DOM做比較,在真實DOM中只改變需要改變的地方,這種補丁機制只改變區域性,不改變整體,因此對系統效能的消耗較小,對虛擬DOM的修改會在狀態改變時觸發,後文會詳細說明這種狀態機制。可能大家也已經聽說了二者之間的比較是基於diff演算法,知乎上有一篇詳細解析React的這個演算法的文章,推薦大家閱讀。

一般來說建立React node有兩種方法,如下

// 方法1,使用React 內建的工廠方法建立
var reactNodeLi = React.DOM.li({id:`li1`}, `one`);

//方法2,使用JavaScript建立node的方法
var reactNodeLi = React.createElement(`li`, null, `one`);

最近有一本開源的電子書React Enlightenment裡有一章對React node有詳細的介紹,也推薦大家閱讀。

React提供的另外一種簡潔,直觀的建立React node的方法,那就是JSX,其實提到React,大家好像都會想到JSX,因為它實在是太方便了,其實使用React其實並非必須使用JSX,不過使用它真的能讓我們的工作更加輕鬆。

JSX

var App = React.createClass({
  render: function() {
    return <p>My name is { this.props.name }</p>;
  }
});

上面例子裡return中的那一部分就是JSX了,初看JSX的語法,可能大家會想到前端開發中經常使用到的模板,不過JSX並非模板,它應該算是React對JS語法的擴充,需要編譯後才能正確使用它,JSX的構建是非常簡潔明瞭的,在此就不再贅述。

再說 Babel

剛剛已經提及JSX是需要編譯才能被瀏覽器識別的,它就是被Babel編譯的,具體說來是被babel-preset-react來編譯的。不過Babel的最主要目的其實並非編譯JSX,Babel應該算是一個編譯平臺,其主要目的是轉換你在程式碼中使用了的ES6甚至ES7語法為瀏覽器識別的ES5語法(babel-core,babel-preset-es2015模組),編譯React倒像是其的附加功能。初學者有時候會覺得使用React困難,配置合適的開發環境可是就是原因之一。以前翻譯過一篇基礎的配置webpack的文章,具體可以點這裡

說到元件了(Component)

除卻高效能,元件是另外一個React非常吸引人的地方,元件的可複用性,可組合性以及其對模組化開發的天然適應性,使得我們的專案非常直觀,便於理解和管理。拿到一個專案,最開始要想的就是如何來劃分元件。當然劃分肯定需要一些依據,先來看看React自己對元件的分類。

劃分並建立元件

我在最初使用React時,我的專案裡的所有的元件都是通過React.createClass()建立,所有的元件在裡面可能都擁有getInitialState(),componentDidMount()等方法,當然這樣用其實一點也沒有問題。但是這樣寫,除非對專案非常熟悉,否則我們並不能很容易的就區分元件之間的層級關係。而且隨著專案的複雜化,也不利於資料的管理。

Stateful Component

之前在使用React重構百度新聞webapp前端看到智慧元件和木偶元件二詞,我覺得它們可能可以分別對應到Stateful ComponentStateless Component,在此引用一下該文裡的說法。

智慧元件 它是資料的所有者,它擁有資料、且擁有運算元據的action,但是它不實現任何具體功能。它會將資料和操作action傳遞給子元件,讓子元件來完成UI或者功能。這就是智慧元件,也就是專案中的各個頁面。

這是一個完整的元件,在這種元件裡可能會出現所有的React提供的方法(包括各種生命週期函式life cycle methods,各種事件響應函式等等)

建立:

//ES5 寫法
var App = React.createClass({
getInitialState(){
    return{
    name:"Tom",
    ...
    }
},

componentDidMount(){
    this.setState({
        name:"Jim"
    })
},

  render: function() {
    return <p>My name is { this.state.name }</p>;
  }
});

// ES6 寫法
class SearchBar extends React.Component {
      constructor(props) {//props需要作為引數傳入
            super(props);//需要使用super,如果沒有this就會是undefined
            this.state = {
              searchTerm: ``,
            };
            this.handleInputChange = this.handleInputChange.bind(this);//為事件繫結this,這是ES6語法所要求的,ES5並沒相關要求
      }

      handleInputChange(event) {
        this.setState({
              searchTerm: event.target.value,
        });
      }

      render() {
            return <input onChange={this.handleInputChange} />;
      }
}

兩種寫法其實沒有本質區別,ES6語法也會通過Babel轉換為ES5語法後被執行,但是兩種寫法裡確實存在一些不一樣的地方,比如說使用ES6時需要單獨繫結this,ES6語法裡方法之間不能使用逗號,等等。網上可以查到很多相關資料,在此不做贅述。

Stateless Component

木偶元件:它就是一個工具,不擁有任何資料、及運算元據的action,給它什麼資料它就顯示什麼資料,給它什麼方法,它就呼叫什麼方法,比較傻。這就是木偶元件,即專案中的各個元件。

這種元件裡只會出現,React提供的render()方法,用於構建虛擬DOM,其建立方式除了ES5,ES6的寫法,還可以使用Stateless Functions方法建立。

建立:

//ES5
var HelloMessage = React.createClass({
    render(){
        return <div>Hello {props.name}</div> //多個節點時需要加括號
    }
})

//ES6
class HelloMessage extends React.Component {
        constructor(props) {//props需要作為引數傳入
            super(props);//需要使用super,如果沒有this就會是undefined
            }
            
            render() {
            return <div>Hello {props.name}</div>;
      }
}

//Stateless Functions
function HelloMessage(props) {
  return <div>Hello {props.name}</div>;
}

//ES6 Stateless Functions
const HelloMessage = (props) => <div>Hello {props.name}</div>;

模組和元件

如若需要,所有的React元件都是可以當做模組被匯出的,不過就就我本人看來,一般所匯出的模組都是由一個或者若干個元件組成的功能單元。不過說到這裡更想說明的一點時,React其實是很依賴類似於webpack這樣的模組管理工具的,所以想要用好React,其實也需要對模組的定義,以及模組管理工具有一點的瞭解。

有生命的元件

React裡的元件是活的,元件不僅僅有類似於出生,成長,死亡的過程,還有心臟和血液。

生命週期函式 life cycle methods

元件的生命週期函式可以分為三個階段:

  • Mounting Phase(此階段的函式在一個元件的生命中只會執行一次)(掛載階段)

    - getInitialState()
    - componentWillMount()
    - componentDidMount()
  • Updating Phase(此階段的函式在一個元件的生命中可別多次執行)(更新階段)

    - componentWillReceiveProps()
    - shouldComponentUpdate()
    - componentWillUpdate()
    - componentDidUpdate()
  • Unmount Phase (此階段的函式在一個元件的生命中只會執行一次)(解除安裝階段)

    - componentWillUnmount()

關於各個函式的具體意義,在此不在贅述,一個比較容易出錯的地方是弄明白各個函式的執行順序,下面給出一個參考列表。

- Mounting Phase:
    1. Initialize / Construction
    2. getDefaultProps() (React.createClass) or MyComponent.defaultProps (ES6 class)
    3. getInitialState() (React.createClass) or this.state = ... (ES6 constructor)
    4. componentWillMount()
    5. render()
    6. Children initialization & life cycle kickoff
    7. componentDidMount()

- Updating Phase follows this order:
    1. componentWillReceiveProps()
    2. shouldComponentUpdate()
    3. render()
    4. Children Life cycle methods
    5. componentWillUpdate()

- Unmount Phase follows this order:
    1. componentWillUnmount()
    2. Children Life cycle methods
    3. Instance destroyed for Garbage Collection

元件的生命之源-state

用過React的人都知道,this.setState({})可能算是React裡使用最多的方法了,每次使用都會根據所更新的資料重構Virtual DOM已達到更新元件的目的,使得元件充滿活力,滿足我們的各種要求。

state在getInitialState()階段被初始化,之後通過其它生命週期函式(componentWillUpdate()裡不能使用)或React事件呼叫的函式,可以使用利用this.setState({})更新某一state的值。

我在最初使用React時總覺得,使用了過多的this.setState({})會不會導致React變得效能低下,不過閱讀了這篇文章打消了我的一些疑惑。

元件的血液-props

為什麼把props比作血液呢,因為它本身自己並不會變化,它就像是一個傳輸的中介,把父元件的方法,屬性傳遞給子元件。一般在子元件中它可能有三方面的作用

  • 作為子元件的屬性<div className="this.props.className">作為屬性</div>

  • 作為引數<div>{"我的名字是"+this.props.name}</div>;

  • 傳遞方法<div onClick="this.props.click">傳遞方法</div>;

配合state,props可以用來改變子元件的表現形式,如果用來傳遞方法,props可以在子元件中呼叫父元件的方法。

在開發時,props還可以配合propTypes使用,這樣可以使得props的使用更加準確(如下例),也使得元件更加健壯。

const AlbumList = (props) => {
  const albums = props.albums.map((album) => <li>{album.name}</li>);
 
  return (
    <ul>
      {albums}
    </ul>
  );
};
 
AlbumList.propTypes = {
  albums: React.PropTypes.array.isRequired,
};

一點小結

本文只總結了我對React的最基礎的部分的一些思考,類似於高階元件,Redux這類的目前我並未接觸過多的知識和以及一些類似於React中的事件這類的較容易理解的知識沒做過多的敘述,至於Routing這類構建app的知識,以後有機會一定會再和大家分享。

對於剛剛接觸React的童鞋,可能看完依舊是雲裡霧裡,不過本文實在算不上教程,初學者可能還是得看比較靠譜的教程。

之前看過一個一個比較好的React學習路徑推薦在此也分享給大家,希望對大家的React學習有幫助

  • 學習React的基本知識;

  • 熟悉npm

  • 熟悉JavaScript的打包工具

  • 瞭解ES6

  • 學習Routing和flux(redux)

最後還要做一個小廣告,或者其實也算是對自己的一個激勵和監督,之前和 React Enlightenment這本開源書的作者聯絡,他也非常願意自己的書能讓更多人有收穫,所以就同意我把這本書翻譯為漢語了。這本書目前一共八章,這本書,上週我看就看完了,感覺有很大收穫,對初學者也比較友好,應該好好看一道,React肯定就入門了,我打算是每三四天翻譯一章,然後也釋出在此處,歡迎大家關注,希望和大家一起進步。

參考

相關文章