一文速覽React全棧

React是Facebook推出的一個JavaScript庫,它的口號就是「用來建立用戶界面的JavaScript庫」,因此它只是和用戶的界面打交道,你能夠把它當作MVC中的V(視圖)這一層。如今前端領域各類框架和庫層出不窮,那麼是什麼緣由讓React如此流行呢?簡單來講,是它三大顛覆性的特色:組件、JSX、Virtual DOM前端

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

1. 組件

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

組件是React的基石,全部的React應用程序都是基於組件的。express

props屬性

如今新建一個組件,稱爲Profile.jsx; 一個組件的例子以下:redux

// 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方法生成一個組件的實例。bash

生命週期

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的單向數據流是怎麼運做的呢?

  • 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的變化。

數據流

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】

相關文章
相關標籤/搜索