一文速覽React全棧

天道酬勤Lewis發表於2019-06-23

React是Facebook推出的一個JavaScript庫,它的口號就是“用來建立使用者介面的JavaScript庫”,所以它只是和使用者的介面打交道,你可以把它看成MVC中的V(檢視)這一層。現在前端領域各種框架和庫層出不窮,那麼是什麼原因讓React如此流行呢?簡單來說,是它三大顛覆性的特點:元件、JSX、Virtual DOM

本文依次介紹 元件、JSX、Virtual DOM、Flux架構、Redux、react-redux和效能優化。

1. 元件

React的一切都是基於元件的。Web世界的構成是基於各種HTML標籤的組合,這些標籤天生就是語義化元件的表現,還有一些內容是這些標籤的組合,比如說一組幻燈片、一個人物簡介介面、一組側邊欄導航等,可以稱之為自定義元件。React最重要的特性是基於元件的設計流程。使用React,你唯一要關心的就是構建元件。元件有著良好的封裝性,元件讓程式碼的複用、測試和分離都變得更加簡單。各個元件都有各自的狀態,當狀態變更時,便會重新渲染整個元件。元件特性不僅僅是React的專利,也是未來Web的發展趨勢。React順應了時代發展的方向,所以它如此流行也就變得順其自然。

元件是React的基石,所有的React應用程式都是基於元件的。

props屬性

現在新建一個元件,稱為Profile.jsx; 一個元件的例子如下:

// Profile.jsx
import React from 'react' ;
export default Class Profile extends React.Component {
    // render 是這個元件渲染的Vitrual DOM結構
    render() {
        return (
            <div className-"profile-component">
                </*this.props就是傳入的屬性*/>
                <h1>my name is {this.props.name}</h1>
                <h2>my age is {this.props.age}</h2>
            </div>
        )
    }
}
複製程式碼

用這種方式,就實現了一個React的元件,在其他的元件中,可以像HTML標籤一樣引用它。有了元件以後,可以使用React提供的另外一個庫ReactDOM把這個元件掛載到DOM節點上。

// app.jsx
import  { render } from 'react-dom';
import Profile from './profile';
render(<Profile name="lewis" age=26 />, document.getElementById('app'));
// 或者可以使用...屬性擴充
const props = {
    name: 'lewis',
    age: 26
};
render(<Profile {...props} />, document.getElementById('app'));
複製程式碼

state狀態

state是元件內部的屬性。元件本身是一個狀態機,它可以在constructor中通過this.state直接定義它的值,然後根據這此值來渲染不同的UI。當state的值發生改變時,可以通過this.setState方法讓元件再次呼叫render方法來渲染新的UI。 現在改造一下簡單的元件,給它新增一個狀態,一個“點贊”的按鈕,每單擊一次, 就給讚的次數加1。

//Profile.jsx
export default class Profile extends React.Component {
  constructor (props) {
    super (props);
    this.state = {
      liked: 0
    };
    this.likedCallback = this.likedCallback.bind(this);
  }
  likedCallback() {
    let liked = this.state.liked;
    liked++;
    this.setState({
      liked
    });
  }

  render() {
    return (
      <div>
        <h1>我的名字叫{this.props.name}</h1>
        <h2>我今年{this.props.age}</h2>
        <button onClick={this.likedCallback}>點贊</button>
        <h2>總點贊數:{this.state.liked}</h2>
      </div>
    )
  }
}
複製程式碼

和上面描述的一樣,在constructor中新增this.state的定義,每次單擊按鈕以後呼叫回撥函式,給當前liked值加1,然後更新this.setState完成UI的重新渲染。因為在ES6 class 型別的component元件宣告方式中,不會把一些自定義的callback函式繫結到例項上,所以需要手動在constructor裡面繫結。

this.likedCallback = this.likedCallback.bind(this);
複製程式碼

React元件通過props和state的值,使用render方法生成一個元件的例項。

生命週期

一文速覽React全棧
1. 元件首次載入

  • getDefaultProps 只會在裝載之前呼叫一次,在元件中賦值的資料會被設定到this.props中。
  • getInitialState 只會在裝載之前呼叫一次,這個函式的返回值會被設定到this.state中,需要注意的是,在ES6的寫法中,只需寫在constructor中即可,如下:
class MyComponent extends React.Component {
    constructor (props){
        super (props) ;
        //在這裡宣告state
        this.state = {count: 0} ;
    }
}
複製程式碼
  • componentWillMount 在render之前被呼叫,可以在渲染之前做一些準備工作。
  • render 這個方法是元件的一個必要方法。 當這個方法被呼叫的時候,應該返回一個ReactElement物件,render是一個純函式,它的意義就是在給定相同的條件時,它的返回結果應該每次都是完全一致的。不應該有任何修改元件state的程式碼或者是和瀏覽器互動的情況。
  • componentDidMount 只會在裝載完成之後呼叫一次,在render之後呼叫,從這裡開始獲取元件的DOM結構。如果想讓元件載入完畢後做一些額外的操作(比如AJAX請求等),可以在這個方法中新增相應的程式碼。

2.元件props更新

當元件接收到新的props的時候,會依次觸發下列方法。

  • componentWillReceiveProps(nextProps) 在元件接收到新的props的時候被觸發,引數nextProps就是傳入的新的props,你可以用它和this.props比較,來決定是否用this.setState實現UI重新消染;
  • shouldComponentUpdate 在重新render之前被呼叫,可以返回一個布林值來決定一個元件是否要更新,如果返回flse那麼前面的流程都不會被觸發,這個方法預設的返回值都是true。
  • componentWillUpdate 在render之前被呼叫,可以在渲染之前做一些準備工作,和componentWillMount類似。
  • render 和元件首次載入的方法相同。
  • componentDidUpdate 重新渲染完成以後立即呼叫,和componentDidMount類似。

3.元件解除安裝

  • componentWillUnmount 在元件被解除安裝和銷燬之前呼叫的方法,可以在這裡做一些清理的工作。

組合元件

React應用建立在各種元件基礎上,那麼自然的一個元件也可以包含多個其他的元件。

無狀態函式式元件

無狀態函式式元件沒有內部state,不需要元件生命週期函式,那麼可以把這類元件寫成一個純函式的形式,稱為stateless functional component(無狀態函式式元件),它做的事情只是根據輸入生成元件,沒有其他副作用,而且簡單明瞭。

// 用一個純函式表示元件
function Hobby (props) {
    return <li>{props .hobby)</li>;
}
複製程式碼

這種寫法很簡單,直接匯出一個函式,它只有一個引數props,就是傳入的屬性。在實際的專案中,大部分的元件都是無狀態函式式元件,所以這是React推薦的寫法。

state 設計原則

什麼元件應該有state,而且應該遵循最小化state的準則?那就是儘量讓大多數的元件都是無狀態的。為了實現這樣的結構,應該儘量把狀態分離在一些特定的元件中,來降低元件的複雜程度。最常見的做法就是建立儘量多的無狀態元件,這些元件唯一要關心的事情就是渲染資料。而在這些元件的外層,應該有一個包含state的父級別的元件。這個元件用於處理各種事件、交流邏輯、修改state,對應的子元件要關心的只是傳入的屬性而己。

state應該包含什麼資料? state中應該包含元件的事件回撥函式可能引發UI更新的這類資料。在實際的專案中,這些應該是輕量化的JSON資料,應該儘量把資料的表現設計到最小,而更多的資料可以在render方法中通過各種計算來得到。

DOM操作

在大多數情況下,不需要通過操作DOM的方式去更新UI,應該使用setState來重新渲染UI,但是有一些情況確實需要訪問一些DOM結構(例如表單的值),那麼可以採用refs這種方式來獲得DOM節點,它的做法就是在要應用的節點上面設定一個ref屬性,然後通過this.refs.name來獲得對應的DOM結構。

// Profile.jsx
render() {
  return (
    <div>
      <input type="text" ref="hobby" />>
      <button onClick={this.addHobbyCallback}>新增愛好</button>
    </div>
  )
}

addHobbyCallback() {
  //用this.refs.name來取得DOM節點
  let hobbyInput = this.refs.hobby;
  let val = hobbyInput.value;
  if (val) {
    let hobbies = this.state.hobbies;
    //新增值到陣列
    hobbies = [...hobbies, val];
    //更新state,重新整理UI
    this.setState({
      hobbies
    }, ()=>{
      hobbyInput.value = '';
    });
  }
}
複製程式碼

元件是react的核心,一個基於react的專案都是由各種各樣不同的元件所構成的。

2. JSX

通過上面的例子可以看出,在render方法中有一種直接把HTML巢狀在JS中的寫法,它被稱為JSX。這是一種類似XML的寫法,它可以定義類似HTML一樣簡的樹狀結構。這種語法結合了JavaScript和HTML的優點,既可以像平常一樣使用HTML,也可以在裡面巢狀JavaScript語法。這種友好的格式,讓開發者易於閱讀和開發。而且對於元件來說,直接使用類似HTML的格式,也是非常合理的。但是,需要注意的是。JSX和HTML完全不是一回事,JSX只是作為編譯器,把類似HTML的結構編譯成JavaScript。當然,在瀏覽器中不能直接使用這種格式,需要新增JSX編譯器來完成這項工作。

來歷

下面這一段是官方文件中的引用,它可以解釋JSX這種寫法誕生的的初衷。

We strongly believe that components are the right way to separate concerns rather than "templates" and "display logic." We things that markup and the code that generates it are intimately tied together. Additionally, display logic is often very complex and using template languages to express it becomes cumbersome.

多年以來,在傳統的開發中,把模板和功能分離看作是最佳事件的完美例子,翻閱形形色色的框架文件,總有一個模板資料夾裡面放置了對應的模板檔案,然後通過模板引擎處理這些字串,來生成把資料和模板結合起來的字元。而React認為世界是基於元件的,元件自然而然和模板相連,把邏輯和模板分開放置是一種笨重的思路。所以,React 創造了一種名為JSX的語法格式來架起它們之間的橋樑。

語法

  • JSX不是必需的

JSX編譯器把類似HTML的寫法轉換成原生的JavaScript方法,並且會將傳入的屬性轉化為對應的物件。它就類似於一種語法糖,把標籤型別的寫法轉換成React提供的一個用來建立ReactElement的方法。

const MyComponent ;
//input JSX, 在JS中直接寫類似的內容。前所未有的感覺。其實它返回的是一個ReactElement
let app = <h1 title="my title">this is my title</h1>;
//JSX轉換後的結果
let app = React.createElement('hl', {title: 'my title'}, 'this is my tit
le');
複製程式碼
  • HTML標籤與React元件

React可以直接渲染HTML型別的標籤,也可以渲染React的元件。

HTML型別的標籤第一個字母用小寫來表示。

import React from 'react';
//當一個標籤裡面為空的時候,可以直接使用自閉和標籤
注意class是一個JavaScript保留字,所以如果要寫class應該替換成classname
let divElement = <div className="foo" />;
//等同於
let divElement = React.createElement('div", {className: 'foo'});
複製程式碼

React元件標籤第一個字母用大寫來表示。

import React from 'react';
class Headline extends React.component {
    render(){
        //直接returnJSX語法
        return (
            <hl>He1lo React</h1>
        )
    }
}
let headine = <Headline />;
//等同於
let headline = React.createElement(Headline);
複製程式碼

JSX語法使用第一個字母大小寫來區分是一個普通的HTML標籤還是一個React元件。

注意: 因為JSX本身是JavaScript語法,所以一些JavaScript中的保留字要用其他的方式書寫,比如第一個例子中class要寫成className.

  • JavaScript表示式

在給元件傳入屬性的時候,有一大部分的情況是要傳入一個JavaScript物件的,那麼基本的規則就是當遇到{}這個表示式的情況下,裡面的程式碼會被當作JavaScript程式碼處理。

屬性表示式如下。

const MyComponent;
let isLoggedIn = true;
let app = <MyComponent name={isLoggedIn ? 'viking' : 'please login'}/>`
複製程式碼

子元件表示式如下。

const MyComponent, LoginForm, Nav;
let isLoggedIn = true;
let app = <MyComponent>{isLoggedIn ? <Nav/> : <LoginForm/> }</MyComponent>
複製程式碼

由上面兩個例子可以得到一個基本規律。在JSX語法中,當遇到標籤的時候就解釋成元件或者HTML標籤,當遇到{}包裹的時候就當成JavaScript程式碼來執行。布林型別屬性如下。

當省略一個屬性的值的時候,JSX 會自動把它的值認為是true。

let myButton = <input type="button" disabled />;
//等同於
let myButton = <input type-"button" disabled={true}/>;
複製程式碼
  • 註釋

要在JSX中使用註釋,沿用JavaScript的方法,需要注意的是,在子元件位置需要用{}括起來。

let component = (
    <div>
        {/* 這裡是一個註釋! */}
        <Headline />
    </div>
);
複製程式碼
  • JSX屬性擴散

假如一個元件有很多屬性,當然可以如下這樣做。

const Profile;
let name = 'viking', age = 10, gender = 'Male';
let component = <Profile name={name} age={age) gender={gender} />;
複製程式碼

但是,當這樣的屬性特別多的時候,書寫和格式看起來就會變得很複雜,所以JSX有一個很便利的功能--屬性擴散。

const Profile;
let props = {
    name: 'viking',
    age: 10,
    gender: 'Male'
};
//用這種方式可以很方便地完成上一個例子裡面的操作
let component = <Profile {...props) />;
複製程式碼

你可以多次使用這種方法,還可以和別的屬性組合在一起。需要注意的是,順序是重要的,越往後的屬性會覆蓋前面的屬性。

let component = <Profile {...props} name='viking2' />;
console.log (component.props.name) ;
//viking2
複製程式碼

神奇的“..."到底是什麼?“...”操作符(擴散操作符)在ES6的陣列上已經獲得了廣泛的使用,物件的擴散操作符也會在ES7中得到實現,這裡JSX直接實現了未來的JavaScript, 帶來了更多的便利。

  • 編譯JSX

JSX不能直接在瀏覽器中使用,需要一種編譯工具把它編譯成React.createElement方法,現在一般用Babel提供的編譯器來進行JSX程式碼的編譯。

小結

JSX看起來就是HTML,每個前端開發者都可以很快地熟悉上手。但是,請記住它不是真正的HTML,也和DOM沒有關係。它像是一種React.createElement寫法的語法糖。是快速高效書寫這個函式的方法,它返回的是ReactElement,一種JavaScript的資料結構。

3. Virtual DOM

在React的設計中,開發者不太需要操作真正的DOM節點,每個React元件都是用Virtual DOM渲染的,它是一種對於HTML DOM節點的抽象描述,你可以把它看成是一種用JavaScript實現的結構,它不需要瀏覽器的DOM API支援,所以它在Node.js中也可以使用。它和DOM的一大區別就是它採用了更高效的渲染方式,元件的DOM結構對映到Virtual DOM上,當需要重新渲染元件時,React在Virtual DOM上實現了一個Diff演算法,通過這個演算法尋找需要變更的節點,再把裡面的修改更新 到實際需要修改的DOM節點上,這樣就避免了整個渲染DOM帶來的巨大成本。

DOM

在當今的Web程式中,由於SPA型別專案的出現,DOM tree 結構也越來越複雜,它的改變也變得越來越頻繁,有可能有非常多的DOM操作,比如新增、刪除或修改一些節點,還有許多的事件監聽、事件回撥、事件銷燬需要處理,由於DOM tree結構的變化,會導致大量的reflow,從而影響效能。

虛擬元素

首先要說的是,Virtual DOM是獨立React所存在的,只不過React在渲染的時候採用了這個技術來提高效率。前面已經介紹過DOM是笨重而龐大的,它包含非常多的API方法。DOM結構也不過是一些屬性和方法的集合,那麼可不可以用原JavaScript的方法來表述它呢?用輕量級的資料能完全代替龐雜的DOM結構來表述相同的內容嗎?答案是肯定的。

/*一個DOM結構,可以用JavaScript這麼來表示
結構如下
<div id="container">
    <h1>Hello world</h1>
</div>
*/
var element = {
  tagName: 'div',
  attr:{
    props: {
      id: 'container',
    },
    styles: {
      color: 'red',
    },
  },
  children: {
    tagName: 'h1',
    children: 'Hello world',
  }
}
//用建構函式來模擬一下
function Element(tagName, props, children) {
  this.tagName = tagName;
  this.props = props;
  this.children = children;
};

var headline = new Element('hi', null, 'Hello world');
var div = new Element('div', {
  props: {
    id: 'container',
  },
  styles: {
    color: 'red',
  },
}, headline);
複製程式碼

這樣就用個物件表述了一個類似DOM節點的結構,看起來有點眼熟,對吧?

從上面的例子可以看出,JSX是一種創造ReacElement的便捷寫法,而ReactElement是什麼呢?

ReactElement是一種輕量級的、 無狀態的、不可改變的、DOM元素的虛擬表述。其實就是用一個JavaScript物件來表述DOM元素而已。我們自己建立的Element物件和ReactElement看起來是完全一致的。

將ReactElement插入真正的DOM中,可以呼叫ReacDOM的render方法。

import { render } from react-dom';
import App from './app';
render(<App />,document.getElementById('root');
複製程式碼

render 這個方法大體可以這樣寫:建立DOM元素,用屬性列表迴圈新建DOM元素的屬性,可以用Element物件寫一段虛擬碼。

function render(elemet, root) {
  var realDOM = document.createElement(elemet.tagName);
  //迴圈設定屬性和樣式,程式碼簡化了解即可
  var props = elemet.attr.props;
  var styles = elemet.attr.styles;
  for (var i in props) {
    realDOM.setAttribute(i, props[i]);
  }
  for (var j in styles) {
    realDOM.styles[j] = styles[j];
  }
  //迴圈子節點,做同樣的事情
  elemet.children.forEach(child => {
    if (child instanceof Element) {
      //如果是Element物件,遞迴該方法
      render(child, realDOM);
    } else {
      //如果是Element物件,遞迴該方法
      realDOM.appendChild(document.createTextNode(child));
    }
  });
  // 最後插入到真實的DOM中
  root.appendChild(realDOM);
  return realDOM;
}
複製程式碼

注意上面的程式碼是虛擬碼,只是讓大家瞭解一下render 的大體過程,並不能良好執行。

介紹到這裡,感覺沒什麼稀奇的,Virtual DOM只不過就是DOM結構的JavaScript物件描述。那它比DOM更高效、速度更快體現在哪裡呢?下面進行介紹。

比較差異

在瞭解了Virtual DOM的結構後,當發生任何更新的時候,這些變更都會發生在Virtual DOM上面,這樣些修都是對JavaScript物件的操作,速度根快。當一系列更新完成的時候,就會產生一棵新的 Virtual DOM樹。為了比較兩棵樹的異同,引入了一種Diff演算法,該演算法可以計算出新舊兩棵樹之間的差異。到目前為止,沒有做任何的DOM操作,只是對JavaScript的計算和操作而已。最後,這個差異會作用到真正的DOM元素上,通過這種方法,讓DOM操作最小化,做到效率最高。

由於這裡的演算法比較複雜,就不再深入講解下去了,現在用虛擬碼的形式來總結一下 整個流程。

//1.構建Virtual DOM 樹結構
var tree = new Element('div', {props: {id: 'test'}}, Hello there');
//2.將Virtual DOM 樹插入到真正的DOM中
var root = render (tree, document.getElementById('container')) ;
//3. 變化後的新 Virtual DOM樹
var newTree = new Element('div', {props: {id: 'test2'}},'Hello React');
//4.通過Diff演算法計算出兩棵樹的不同
var patches = diff(tree, newTree) ;
//5.在DOM元素中使用變更,這裡引入了patch方法,用來將計算出來的不同作用到DOM上
patch(root, patches) ;
複製程式碼

通過這5個步驟,就完成了整個Virtual DOM的流程。

4. Flux架構

FLux是Facebook官方提出的一套前端應用架構模式,它的核心概念就是單向資料流。它更像是一種軟體開發模式,而不是具體的一個框架,所以基於Flux存在很多的實現方式。其實用FLux架構開發程式不需要引入很多程式碼,關鍵是它內在的思想。

單向資料流

單向資料流是Flux的核心。讀者有可能接觸過MVC這種軟體架構,它的資料流動是雙向的。controller是model和view之間互動的媒介,它要處理view的互動操作,通知model進行更新,同時在操作成功後通知view更新,這種雙向的模式在model和view的對應關係變得越來越複雜的時候,就會遇到很多困難,難以維護和除錯。針對MVC的這個弊端,Flux的單向資料流是怎麼運作的呢?

一文速覽React全棧

  • View: 檢視層
  • Action(動作):檢視層發出的訊息(比如mouseClick)
  • Dispatcher(派發器):用來接收Actions、執行回撥函式
  • Store(資料層):用來存放應用的狀態,一旦發生變動,就提醒Views要更新頁面

dispatcher

事件排程中心,Flux模型的中心樞紐,管理著Flux應用中的所有資料流。它本質上是Store的回撥註冊。每個Store註冊它自己並提供一個回撥函式。當Dispatcher響應Action時,通過已註冊的回撥函式,將Action提供的資料負載傳送給應用中的所有Store。應用層級單例;

store

負責封裝應用的業務邏輯跟資料的互動;Store中包含應用所有的資料;Store是應用中唯一的資料發生變更的地方;Store中沒有賦值介面--所有資料變更都是由dispatcher傳送到store,新的資料隨著Store觸發的change事件傳回view。Store對外只暴露getter,不允許提供setter,禁止在任何地方直接操作Store。

view

controller-view 可以理解成MVC模型中的controller,它一般由應用的頂層容器充當,負責從store中獲取資料並將資料傳遞到子元件中。簡單的應用一般只有一個controller-view,複雜應用中也可以有多個。controller-view是應用中唯一可以操作state的地方(setState()),view(UI元件)職責單一隻允許呼叫action觸發事件,資料從由上層容器通過屬性傳遞過來。

其他

action creators 作為dispatcher的輔助函式,通常可以認為是Flux中的第四部分。ActionCreators是相對獨立的,它作為語法上的輔助函式以action的形式使得dispatcher傳遞資料更為便利。

大致流程

  1. 使用者訪問 View
  2. View 發出使用者的 Action
  3. Dispatcher 收到 Action,要求 Store 進行相應的更新
  4. Store 更新後,發出一個"change"事件
  5. View 收到"change"事件後,更新頁面

5. Redux

Redux是JavaScript的狀態容器,它提供了可預測的狀態管理。Redux可以執行在不同的環境下,不論是客戶端、伺服器端,還是原生應用都可以執行Redux。注意React和Redux之間並沒有特別的關係,不管你使用的是什麼框架,Redux都可以作為一個狀態管理器應用到這些框架上。

三大定律

1. 單一資料來源 整個應用的state儲存在一個JavaScript物件中,Redux用一個稱為store的物件來儲存整個state。

2. state 是隻讀的 不能在state上面直接修改資料,改變state的唯一方法是觸發action。action只是一個資訊載體,一個普通的JavaScript 物件。 這樣確保了其他操作都無法修改state 資料,整個修改都被集中處理, 而且嚴格 按順序執行。

3. 使用純函式執行修改 為了描述action怎樣改變state,需要編寫reducer來規定修改的規則。reducer是純函式,接收先前的state和處理的action,返回新的state。 reducer可以根據應用的大小拆分成多個,分別操縱state的不同部分。純函式的好處是它無副作用,僅僅依賴函式的輸入,當輸入確定時輸出也一定保持一致。

組成

1. action

action是資訊的載體,裡面有action的名稱和要傳遞的資訊,然後可以被傳遞到store中去。傳遞的方法是利用store的dispatch方法,action是store的唯一資訊來源。

和Flux中一樣, action 只是普通的JavaScript Object, action 必須有一個屬性值,它就像這個action的身份證一樣, 來表示這action完成的功能。 type應該被定義成常量,因為它是唯一的,不能被修改的。當應用複雜程度上升的時候,可以把所有action 的type統到一個特定的模組下。

action creator其實就是一個函式, 用來建立不同的acion,這其實就是將一個函式改裝了一下,返回的還是一個物件。

function createPost (data) {
    return {
        type: CREATE POST,
        data: data
    }
}
function deletePost (id) {
    return {
        type: DELETE POST,
        id: id
    }
}
function userLogin (data)
    return {
        type: USER LOGIN,
        data: data
    }
}
複製程式碼

也許讀者在這裡會疑惑,為什麼要用函式包裝建立action的過程呢?看起來完全是多此一舉。 在同步的應用中,看起來沒有什麼特殊之處,但是在非同步的應用中,就可以看出action creator的作用。

2. reducer

action定義了要執行的操作,但是沒有規定state怎樣變化。reducer 的任務就是定義整個程式的state如何響應。

在Redux中,整個程式的所有資料儲存在唯一一個Object中。這是Redux不同於Flux的一個重要特性。Flux 可以有多個store來處理不同型別的資料,而Redux整個應用程式的state都在一個單獨的Object中。完全可以只寫一個reducer來處理所有的action,但是,當資料和action變得越來越複雜的時候,這個唯一的reducer就會變得臃腫不堪,所以最好的方法是將複雜的reducer拆分然後合併。

3. store

在瞭解Redux之前,action和reducer聽起來比較晦澀,其實它們沒什麼難懂的地方,action不過是一個特殊的object,它描述了一個特定的行為;而reducer就是一個函式,接受資料和action,返回唯一的值,它會根據這些不同的action更新對應的state值。

store就是這兩者的黏合劑,它能完成以下這些任務。

  • 儲存整個程式的state。
  • 可以通過getstate()方法訪問state 的值。
  • 可以通過dispatch()方法執行一個action。
  • 還可以通過subscribe(listener)註冊回撥, 監聽state的變化。

資料流

一文速覽React全棧

Redux是嚴格的單向資料流,類似Flux,可以讓程式邏輯更加清晰、資料完全可控。應用中的資料變化都遵循相同的週期,這就是Redux的口號,可以預測的JavaScript狀態容器。

根據上面的例子,可以總結出Redux的資料流分為這樣幾步:

  • 呼叫store.dispatch(action)來執行一個action。
  • store呼叫傳入的reducer 函式,store 的來源就是reducer, const store =createStore(rootReducer)。當前的state 和action 會傳入到reducer這個函 數中。
  • reducer處理action並且返回新的state。在reducer這個純函式中,可以根據傳入的action,來生成新的state並且返回。
  • store儲存reducer返回的完整state。可以根據store.getState()來取得當前的state,也可以通過store.subscribe(listener)來監聽state的變化。

middleware

middlear顧名思義,即中介軟體。如果你開發過基於Express/Koa的Web伺服器,你很可能接觸過這個概念。在Express/Koa這樣的伺服器端框架中,中介軟體扮演著對request/ response統進行特定處理行為的角色,它們可以接觸到request/response以及觸發下一個middleware繼續處理的next方法。

Redux中mdeware的設計也較為相似,它們在action被dispatch時觸發,並提供了呼叫最終reducer之前的打展能力midleware可以同時接觸到action資訊與store的getstate/dispatch方法。middleware可以在原有action 的基礎上建立一個新的action和dispatch ( action轉換,用於可非同步action處理等),也可以觸發一些額外的行為(如日誌記錄)。最後,它也可以通過next觸發後續的middleware與reducer本身的執行。

簡單版本的applyMiddleware方法: 最後需要把middleware和store.dispatch方法結合起來,提供一個叫applyMiddleware的方法來完成這項任務。

//這不是Redux最終的實現,在這裡只是寫出了這個方法的工作原理
function applyMiddleware (store, middlewares){
    //讀入middleware的函式陣列
    middlewares = middlewares.slice();
    middlewares.reverse() ;
    
    //儲存-份副本
    let dispatch = store.dispatch;
    //迴圈middleware,將其依次覆蓋到dispatch方法中,還是一種類似滾雪球的方法
    middlewares.forEach (middleware =>  dispatch = middleware (store)(dispatch))
    
    //到這裡dispatch這個函式已經擁有了多個middleware的魔力
    //返回一份store物件修改過的副本
    return object.assign({}, store, { dispatch }) ;
}
store = applyMiddleware (store, [logger, crashReporter]) ;
store.dispatch (addTodo('Use Redux'));
複製程式碼

注意,這個方法不是Redux的最終實現,這裡僅僅是寫出了工作原理,使用applyMiddleware後返回的是一個增強型的store, store dispatch方法也將兩個中介軟體融合了進去。

6. react-redux

react-redux是Redux官方提供的React繫結,用於輔助在React專案中使用Redux,其特點是效能優異且靈活強大。

它的API相當簡單,包括一個 React Component(Provider)和一個高階方法connect。

1. Provider

顧名思義,Provider的作用主要是“provide"。Provider的角色是store的提供者,一般情況下,把原有的元件樹根節點包裹在Provider中,這樣整個元件樹上的節點都可以通過connect獲取store。

ReactDOM.render (
    <Provider store={store}>
        <MyRootComponent />
    </ Provider>,
)
複製程式碼

2. connect

connect是用來“連線”store與元件的方法,它常見的用法是如下這樣的。

import { add } from ' actions';
function mapStateToProps (state) (
    return {
        num: state.num
    };
}
function mapDispatchToProps (dispatch)
    return {
        onBtnClick() {
            dispatch(add())
        }
    }
}

function Counter(props) {
    return (
        <p>
            {props.num}
            <button onClick={props.onBtnClick}>+1</button>
        </p>
    )
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
複製程式碼

在這個示例中,我們通過connect讓元件Counter得以連線store,從store中取得num資訊並在按鈕單擊的時候觸發store上的add方法(這裡的add是個action creator,執行結果是一個action)。

connet是一個高階函式,接收3個引數mapStateToProps、mapDispatchToProps及mergeProps,並返回enhancer, enhancer的作用已經不必多說,它決定了被返回的容器元件的行為,而enhancer的行為又由connect方法決定。下面,首先簡要說明下在connect被呼叫時,它的3個引數各自的作用。

mapStateToProps

mapStateToProps 要求是一個方法,接收引數state (即store getState()的結果),返回一個普通的JavaScript物件,物件的內容會被合併到最終的展示元件上。簡單地說,mapStateToProps就是從全域性的狀態資料中挑選、計算得到展示元件所需資料的過程,即從state到元件屬性的對映,正如它的引數名所暗示的:“mapstatetoprops”。這個方法會在最初state發生改變時,被呼叫並計算出結果,結果會被作為展示控制元件屬性影響其行為。這部分控制元件屬性被稱為stateProps。

mapDispatchToProps

mapDispatchToProps的命名風格與第一個引數類似,不難推斷它的作用“map dispatch to props",即接收引數dispatch (正是store的dispatch方法),並返回一個普通的JavaScript物件,物件的內容會被合併到最終的展示元件上。對應於mapStateToProps,一般用於生成資料屬性,mapDispatchToProps一般用於於生成行為屬性,即典型的onDoSth這樣的回撥,被稱為dispatchProps.

展示元件與容器元件

首先要引入兩個概念:展示元件(Presentational Component)與容器元件(Container Component)。所有的React元件都可以被被分為這兩種元件,顧名思義,前者專注於介面的展示,而後者為前者提供容器。下面將藉助這些特點來幫助我們更明確地區分這兩個概念。

展示元件

  • 關心應用的外觀。
  • 可能會包含展示元件或容器元件,除此之外常常還會包含屬於元件自身的DOM節點與樣式資訊。
  • 常常允許通過this.props.children 實現巢狀。
  • 對應用的其餘部分( 如Flux action及store)沒有依賴。
  • 不會指定資料如何載入或改變。
  • 只通過props獲取資料與行為(回撥函式)。
  • 極少會包含自身的狀態(state),如果有,一定是介面狀態而非資料。
  • 一般都寫成函式式元件( functional component),除非需要包含狀態、生命週期鉤子或效能優化。
  • 典型的例子: Page、Sidebar、Story、UserInfo、List。

容器元件

  • 關心應用如何工作。
  • 可能會包含展示元件或容器元件,但通常不會包含DOM節點(除包裹用的div外),一定不會包含樣式資訊。
  • 為展示元件或其他容器元件提供資料與行為(回撥函式);
  • 呼叫Flux action井將其作為提供給展示元件的回撥函式。
  • 往往是有狀態的,扮演資料來源的角色。
  • 往往無須手工實現,而是通過高階元件生成,如react-redux提供的connect()、Relay提供的createContainer()及 FluxUtils 提供的Container.create()等。
  • 典型的例子: UserPage、FollowedUserList。

這麼做有什麼好處呢?

一來通過職責將元件明確地區分開了,應用的介面與邏輯都會變得更清晰。

二來這種區分幫助我們更好地複用元件:展示元件具有更好的複用性,它們可以通過包裹不同的資料來源成為不同的容器元件。如Usrli可以被分別包裝成為Followeduserist與FollowingUserList,只需要實現各自獲取userList資料的邏輯即可。

最後,這讓我們展示一個無邏輯的介面成為可能----只需要組裝展示元件,然後給它們提供mock的資料,就足以完成介面的全貌。

經過以上的介紹,不難發現,這裡的容器元件在基於react-redux的專案中正是那些通過connect的結果函式處理得到的元件,而展示元件是被作為引數傳入或組成其他展示元件的那些元件。如何組織專案中的connetc行為這個問題,在這裡等價於如何組織專案中的展示元件與容器元件。

組織不同型別的元件

下面介紹一下如何合理地組織展示元件與容器元件。

首先,儘可能通過純展示元件(除根節點外)來完成應用的搭建,所有元件的資料與行為都通過props從其父節點獲取。然後很快會遇到之前提到的問題:需要將很多內容逐層地傳遞下去,以便葉子節點使用。現在便是時候引入容器元件了。考察那些逐層傳遞屬性的行為,對於一箇中間元件,如果某些資料僅僅用來向下傳遞給它的子節點,則自己並不消費。每次它的子節點所需的資料發生變化,都要相應地修改它的props以適應變化,那麼這些資料往往並不應該由它來提供給它的子節點。通過對子節點進行connect產生一個新的容器元件,由它直接從store中獲取資料並提供給子節點,這樣,中間元件就無須傳遞這些並不是它本身依賴的資料。這是一個不斷迭代優化的過程,重複這樣的步驟可以幫助我們找到一個個應該插入容器元件的地方,讓應用結構變得越來越合理。

7. 效能優化

在開發Web應用的時候,效能一直是一個被關注很多的話題。對於React的專案,大部分時候不需要考慮效能問題,這正是React的Virtual DOM與Diff演算法帶來的好處。但是在應用較為複雜或資料流較大時,僅僅通過所以元件的render方法重新生成Vitrual DOM樹並進行Diff,這一過程變得較為耗時,優化在所難免。

優化原則

  1. 避免過早優化
  2. 著眼瓶頸
  3. 效能分析
  4. 避免不必要的render
  5. 合理拆分元件
  6. 不可變資料
  7. 合理使用state和props
  8. 合理使用社群優秀產物

本文整理於【React全棧】,如有錯誤,敬請雅正?

更多精彩內容歡迎關注我的公眾號【天道酬勤Lewis】

一文速覽React全棧

相關文章