React的狀態管理

主要內容,看看State的狀態管理方式,包括最基本的方式和React-Hooks方式以及Redux方式和ReSub方式 咱們從基本的方式開始javascript

React和數據的基本交互方式

在MVC程序構架中,React常常被稱爲View層,但實際上並不徹底是這樣, React實際對MVC模式作了新的構想. 本質上React只是藉助JSX語法實現的UI界面庫,可是UI都須要數據來填充,因此問題就是如何獲取數據,如何靈活的展示數據.java

MVC的思想

MVC架構的基本思想:react

  • 模型層(Model)就是數據層.
  • 視圖層(View)負責整個應用程序的展現.
  • 控制層(Controller)在應用程序中扶着提供數據處理的邏輯操做.

React處理數據和MVC有微妙的區別. 在由多個子組件組合而成的視圖(父組件)裏, 子組件能夠管理本身的數據處理方式,並且也能夠從父組件獲取數據,只須要在父組件中提供一個控制器就能夠了.git

React的思想

在React的組件中有兩種不一樣的數據類型:程序員

  • props ,在建立組件的時候,props會做爲參數傳遞給組件,這個就做爲組件頂級的配置項,一旦定義好,組件就不能自行修改了. 在React定的父組件->子組件的信息傳遞中,只能使用這一種方式.沒有其餘的方法. Props是React組件庫的精華, 咱們能夠定義不一樣的Props來控制組件的表現形式.github

  • state,state是組件內部的數據.React的精華實際就在state上,咱們能夠在父組件中定義一個state,而後以Props的形式傳遞給子組件, state只是一個JS對象,咱們能夠定義任何形式的屬性. state的定義多樣性,決定了你的應用的多樣性. 經過定義組件的state,能夠實現基本的狀態管理,也能夠實現類Redux管理方式, 還能夠實現React-Hooks的管理方式. 若是深刻一點,你須要知道,Redux其實就是一個只有State邏輯處理而沒有UI的React組件.編程

    進行State修改的方法就只有一個 this.setState({}).在Redux這個特殊的React組件中,也是經過這個方法來修改App的State,只不過咱們看不到實現細節. 後續我會經過一個表單來看看看裏面具體的實現.redux

    以上內容整理自構建 F8 2016 App的介紹瀏覽器

基本實現

State設計是React應用最重要的部分.這個設計,我認爲也是React思想創建的關鍵. 核心是如何思考State的提高, 也就是不斷的把State提高到更高一級的組件中. 可是這個提高也要適可而止, 應該老是以具體的處理流程做爲分割線. 同一個流程的State,最終能夠提高爲一個總的State,例如和登陸,註冊,登出,找回密碼和修改密碼的流程,就能夠提高爲一個大的State. 不相關流程的State,就不要混合在一塊兒.無論你是使用基礎的State管理,Redux管理,Hooks管理,包括ReSub,這一點都是同樣的.bash

React文檔中的基本處理方法

單個字段的‌表單

class SingleFieldForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {value: ''};

    this.handleChange = this.handleChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.value} onChange={this.handleChange} />
        </label>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}
複製代碼

這是一個簡單的表單組件,要實現這個表單,不只要使用state,還有props,同時還要有展現內容的UI組件

在表單組件中定義了state:

//只是一個JS對象,屬性名爲value
this.state = {value: ''};
複製代碼

定義了處理state的方法:

handleChange(event) {
    this.setState({value: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

複製代碼

在建立一個表單組件的時候,須要反饋給輸入用戶到底本身輸入的是什麼,還有如何進行表單提交的方法.上面兩段代碼就定義這兩個內容. 那麼表單組件內部的子組件直接獲取輸入的內容和提交方法就能夠了.從父組件向子組件傳遞數據時,咱們就須要用到props.就是下面的代碼

<form onSubmit={this.handleSubmit}>
        <label> Name: <input type="text" value={this.state.value} onChange={this.handleChange} /> </label> <input type="submit" value="Submit" /> </form> 複製代碼

這裏的form,input[type="text"],input[type="submit"] 都是props的用法. 這裏只要牢記一點, 在return中出現的全部參數都是props, render以外的是state,

render(){
 return(
    ...code
 )
 
複製代碼

在JS中,咱們是傳引用賦值的,因此在子組件就能夠經過引用的方法名來操做父組件定義的State, 那麼這裏就有一個問題, 若是咱們繼續把父組件中定義的State和State處理方法提高的爺爺組件,在繼續提高的太爺爺組件上,應該是同樣的吧? 我能夠確切的說, React的代碼編寫就是這個原則. 只不過state的設計須要稍稍複雜一點.

若是是多個字段的表單,咱們應該如何編寫代碼?

若是按照常規是這樣的 ‌多字段表單常規寫法

class ThreeFieldsForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: ''
                  age: null,
                  email:''
     };

    this.handleNameChange = this.handleNameChange.bind(this);
    this.handleAgeChange = this.handleAgeChange.bind(this);
    this.handleEmailChange = this.handleEmailChange.bind(this);
    this.handleSubmit = this.handleSubmit.bind(this);
  }

  handleNameChange(event) {
    this.setState({name: event.target.value});
  }
  
  handleAgeChange(event) {
    this.setState({age: event.target.value});
  }
  handleEmailChange(event) {
    this.setState({email: event.target.value});
  }

  handleSubmit(event) {
    alert('A name was submitted: ' + this.state.value);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" value={this.state.name} onChange={(event)=>this.handleNameChange(event.target.value)} />
        </label>
        <label>
           Age:
          <input type="text" value={this.state.age} onChange={this.handleAgeChange} />
        </label>

<label>
          Email:
          <input type="text" value={this.state.email} onChange={this.handleEmailChange} />
        </label>

        <input type="submit" value="Submit" />
      </form>
    );
  }
}
複製代碼

這裏是三個字段的表單, 若是是十個字段, 那麼state和處理方法代碼就太多了, 而且你發現這些代碼只有一個地方是不一樣,或許咱們能夠在state處理方法上想一想辦法?

‌把handleChange方法抽象出來

class ThreeFieldsForm extends React.Component {
  constructor(props) {
    super(props);
    this.state = {name: ''
                  age: null,
                  email:''
     };

       
    
    
  }
  //這裏用了ES6的箭頭函數就不須要再綁定啦
  setValue = (text, type) => {
    switch (type) {
      case "setName":
        this.setState({ name: text });
        break;
      case "setAge":
        this.setState({ age: text });
        break;
      case "setEmail":
        this.setState({ email: text });
        break;
      
    }
  };

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" 
             type="setName"
            onChange={(event)=>this.setValue(event.target.value,"setName")}
            value={this.state.name} />
        </label>
        <label>
           Age:
          <input type="text" 
          type="setAge"
           onChange={(event)=>this.setValue(event.target.value,"setAge")}
          value={this.state.age} />
        </label>

<label>
          Email:
          <input type="text" 
          type="setEmail"
          onChange={(event)=>this.setValue(event.target.value,"setEmail")}
          value={this.state.age} />
        </label>

        <input type="submit" value="Submit" />
      </form>
    );
  }
}
複製代碼

這裏有兩個詞,若是你看了Redux和React-Hooks,可能會以爲很眼熟, 一個是type,另外一個是setValue, 沒錯這個地方也是我寫這篇文章的着眼點,上週我想到這個地方的的時候,就以爲常規的State處理,Redux和React-hooks對於State的處理其實並無明確的界限. 如何使用就是React程序員須要考慮的問題.

若是這個表單用React-Hooks處理是這個樣子的

import {useState}  from 'React';

 const  ThreeFieldForm=(props)=>{
    const [name,setName]=useState("");
    const [age,setAge]=useState(null);
    const  [email,setEmail]=useState("")
  return (
      <form onSubmit={this.handleSubmit}>
        <label>
          Name:
          <input type="text" 
             type="setName"
            onChange={(event)=>setName(event.target.value)}
            value={this.state.name} />
        </label>
        <label>
           Age:
          <input type="text" 
          type="setAge"
           onChange={(event)=>setAge(event.target.value)}
          value={this.state.age} />
        </label>

<label>
          Email:
          <input type="text" 
          type="setEmail"
          onChange={(event)=>this.setEmail(event.target.value)}
          value={this.state.age} />
        </label>

        <input type="submit" value="Submit" />
      </form>
    );
}

複製代碼

上面這三個Hooks能夠繼續抽象爲useForm的形式, 爲對象添加type,結合JS的閉包,不少問題變簡潔了.使用Hooks,並返回新的對象和方法也是使用Hooks的一個模式,具體的能夠看看youtube上的一些視頻. 若是咱們在處理的方法中加了type那就能夠用useReducer啦! useReducer能夠看下面這段代碼.

useReducer

const initialState = { count: 0 };

function reducer(state, action) {
  switch (action.type) {
    case "reset":
      return initialState;
    case "increment":
      return { count: state.count + 1 };
    case "decrement":
      return { count: state.count - 1 };
  }
}

function Demo() {
  const [state, dispatch] = useReducer(reducer, initialState);
  return (
    <>
      Count: {state.count}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}
複製代碼

這裏的這段代碼,咱們先不做解釋,若是對Redux比較理解了, useReducer的方法也是比較好理解的. 仍是以前提到的那一句話, state的管理方法不是絕對的, 看看如何思考具體的是用. 通過以前的提高操做, 若是更進一步,把全部的state都提高到一個頂級的組件中, Redux模式就完成了. 這一點我在後面會繼續講到, 其實在不少講解Redux的圖示中,都會提到數據單向流動, 沒錯一旦全部的State都提高到頂級的組件中, 數據就只能經過props的形式傳遞給子組件. 最大的子組件就是Redux的包裝下的那個App組件.

若是你看過Redux的模型圖,例以下面這一張:

或者我本身畫的

數據是單向流動的,從React-Redux組件流向應用的組件. 可是在第一張圖的右側的Actions彷佛有流了回去, 這算是單向流動嗎? 這個問題時間用dispatch並很差理解, 用ReSub的觸發彷佛要好一點. 後邊我會結合一個本身想的現實生活中的實例來解釋這個問題.

下面我要聲明我本身的一個學習體會, React通過幾年的高速發展, 構架不斷的向函數式編程方向發展, 函數式組件內部的JSX代碼結合傳入的數據,咱們想要的應用就實現了. 因此歸結爲兩點一個是函數式組件,另一個就是數據. 在React中流動的數據僅僅只是JS對象,若是咱們給這些對象添加了定義好了Type類型,那麼數據就能夠井井有理的唄管理和組織,就是這麼簡潔,注意是簡潔並非簡單, 要想設計好State也不是一件容易的事情.

來自flutter文檔裏的圖,和React的原理是徹底相同的

上面這張圖了來自flutter文檔,就是我要表達的意思 下面咱們要進入本文的主題了, 通俗的學習Redux.

React的狀態管理的權威-Redux

這裏我不想很正式的講解Redux,Redux文檔寫的很是好,可能一開始看以爲很難,可是看過十幾遍以後,你會以爲甘之如飴. 沒有看十遍以上的,是苦的. 因此我想換個方法, 用通俗的方法來解釋一下,這個問題.做爲看文檔的補充.不少時候看問題要換個角度,或者換個容易理解的模型就比較容易理解了.

Redux的通俗理解

咱們就從 這張圖的Store開始

這一段時間我都在思考Redux Store的通俗理解方法. 結果發現自己這個單詞就是最好的詮釋.

這裏的Store我想用兩個模型解釋,一個是沃爾瑪的Super Store,一個是電商的Store,就拿JD商城作例子吧.

從Store開始.

沃爾瑪的Store

若是你沒去過沃爾瑪,把沃爾瑪換成全家便利店也能夠, 規模不一樣,結構和組織徹底相同. 可是若是類比Redux的Store,大型超市的多人管理更相似些

在Store裏,首先你會看大不少的貨架,一個Store在剛開始初始化的時候是這個樣子的

Store初始化的貨架

開張的時候是這個樣子的:

Store的貨架擺滿貨物

基本大型超市會分紅不一樣的樓層,而後分紅不一樣的區域,不一樣的貨架 處理具體貨架的人是不一樣的,因此儘管很大,可是因爲進行了分區,分層處理,管理是層次分明的.每一個區域,每一個分類,每一個貨架都有不一樣的標籤來標識. 每種貨物的具體補貨,出貨,換貨等方法都相應的不一樣, 可是隻要找到具體每一個區的負責員工就能夠實現了. 看這個天天超市龐大的貨物吞吐量, 其實進入到超市以後,就像看不見的洋流同樣實際上是在各自區域中獨立的流動. 看到這裏你有沒有具體的代碼結構,文件夾結構如何安排? 我想按照貨品的不一樣分類主導代碼的結構是比較很好的. 能夠先看看gitpoint的代碼,

.gitpoint的代碼就是按照不一樣的"洋流"來安排的. 後面咱們再談這個問題. 與之對照, 在Redux中全部的應用State,初看起來也是很是龐大,可是具體到實現,都由JS對象的鍵來區分和管理,各自也包含了本身的State處理方法. 每一個小部分的對象和處理方法就統稱爲 reducer,每一個小分區的State又經過 CombineReducer組合成最大的Reducer,咱們能夠從整個Reducer裏獲取到應用的完整State. 咱們去超市,抽象的是和超市打交道,具體的是和每一個終端在員工和貨架在打交道. 因此儘管超市很大,可是處理問題的方式卻很簡單.

京東的Store

京東的Store,和咱們React裏的Store就很是接近了. 之因此拿電商來作實際的例子,要解決幾個問題,一是如何理解Redux的 dispatch方法,另外一個是如何理解connect. 這裏我先作一個通俗的解釋,而後講解一張我認爲對這個模式解釋最好的圖片.還有就是數據的不可突變性. 前面咱們提到了沃爾瑪超市的貨架,分區. 那麼和這裏的電商的Store有什麼區別? 差異就是咱們瀏覽器或者是手機app看到的分類是虛擬的, 可是實際效果和實體超市同樣,在處理虛擬的Store數據時,也要可以按照分區,分類的方法來管理.這也就是Redux中Store的管理方式.

dispatch

dispatch時,到底有沒有數據從用戶流向Store? 這個單詞翻譯爲中文叫分派,彷佛還不太準確,準確的翻譯應該叫觸發. ReSub這個庫就用了trigger這個詞. 最好的處理就是把state和處理state的方法統一放大一個地方. 因爲JS是傳引用賦值的,咱們能夠把修改State的方法經過props的形式傳遞給子組件, 子組件只須要觸發對應的操做就能夠了.因此這裏用觸發的解釋比較好. 面對一個龐大的電商Store,也沒有什麼擔憂的,只要定義好了不會引起歧義的type就能夠了. 咱們觸發一個操做,就是執行一個Store定義的方法,根據觸發的type對Redux的State作出修改.

咱們在京東購物時,點擊購買,提交的就是商品品名,數量,此外咱們還要提供本身的地址,至關於爲本身的地址綁定了此次購物,等物品從JD的Store出來以後,後按照你的地址進行派送. 整個流程幾乎是徹底相同的.

connect

從JD Store出來的貨物是針對所有買家的,不是每件商品都是你須要的. 因此當Store的貨物返回到社會之後,須要根據買家的地址來進行篩選和分類,而後由快遞員按你提供的地址進行派送. 這個過程是自動, 你不須要本身動手, 由於以前已經進行了訂閱.

用Redux的方法就是使用mapStateToProps把某個組件須要的數據篩選出來. 因爲須要dispatch的Store方法也是從外部傳遞的,全部就有了mapDispatchToProps方法, 傳遞Store對應的方法名. 在重複一下, 組件外部的數據只能經過props傳遞.

好了時候借用別人的殺手鐗了. 下面這張圖嘛,你能夠想象是你有幾個朋友,分別在不一樣的城市,你用他們的地址進行了訂閱,而後你在JD上提交了訂單,觸發了JD Store的一次操做,而後JD根據你的訂閱地址把貨物發送到幾個朋友手中.

圖片出處 when-do-i-know-im-ready-for-redux

你如今能夠進入這張圖中,你的家就在最右邊的這個球中,你觸發了一個訂購操做,好比 在2019年4月22號20點20分20秒195毫秒時 訂購了三隻中華鉛筆,HB的. 而後JD的的文具分部接受根據你觸發的動做的類型作了相應的處理,通知Store出貨,而後鉛筆庫存減掉3, 這時若是還有其餘人想買中華的HB鉛筆,就會顯示無貨. 你的此次訂閱和小米的旗艦店沒有任何的關聯, 儘管從外面看JD的鉛筆和小米的手機是從同一個地方出來的,可是在Store內部是由不一樣的分支來處理的.

上面我專門添加了一個時間,是爲了要解決數據的不可變性這個問題,就是在Redux文檔中提到的時間旅行的問題.

數據的不可突變性對於JavaScript是一個問題,可是對於某些語言就不是問題. JS中的這個問題是因爲JS對於數據存儲的方法引發的.

例如咱們要在JS中定義一個蠟筆的顏色爲紅色:

定義一個紅色蠟筆顏色

而後咱們把對象變爲藍色的對象, JS會爲這個對象從新分配內存地址

修改蠟筆爲藍色顏色對象

可是若是咱們只修改對象的屬性,問題就來了,JS會在原位置對對象做出修改

修改蠟筆的顏色屬性

因爲Redux Store中的state是嵌套對象, 若是對某一部分屬性進行修改, 內存地址不會發生改變, Store可能認爲你沒有作什麼修改工做,由於在Store中使用'==='來決定state是否發生改變的.===符號在JS中就是比較對象的內存地址的. 因此在Redux中須要手動把要修改的State複製到新的內存地址中,而後在作修改,從而讓Store能夠覺察到State的變化.

以上解釋來自[Immutability in React and Redux: The Complete Guide](https://daveceddia.com/react-redux-immutability-guide/). 若是理解有誤差,敬請指出

可是這樣作每次修改都要開闢新的內存地址, 是比較浪費內存的.因此FaceBook提出了 Immutable.js 的方法. 這就是我上面用到的那個時間段的意思. 仍是在JD的Store, 咱們要出貨,管理庫存,當用戶訂購了三隻中華鉛筆,庫存要減掉, 咱們能夠把全部的庫存帳本從新抄一遍,而後把中華鉛筆的庫存減掉3.可是實際的庫存管理不是這樣作的, 咱們有一個總的庫存目錄, 而後單獨在一個地方記載某個時間某個商品的庫存發生了什麼變化, 沒有變化的部分,就無論了. 這就是Immutable的處理方法. 記載變化的位置,共享不變的位置. 若是咱們不爲修改打上時間戳就沒有辦法知道歷史記錄了,由於歷史數據被新的數據給替換掉了. 因此實際的帳目中不只要記錄帳目發生變化的品名還要記錄時間. Redux的時間旅行就是這個意思.

上面的那篇文章對於JS的Immutability操做解釋的很是好, 我也準備翻譯. 尤爲是後面的Immer庫很方便.

微軟的Resub

下面咱們來看看微軟的Resub庫. 這個庫是配合微軟的ReactXP項目的附屬. 我沒看過mobx的文檔,我猜測應該和Mobx是很像的.

主要內容就是使用StoreBase定義數據和數據處理方法,

import { StoreBase, AutoSubscribeStore, autoSubscribe } from 'resub';

@AutoSubscribeStore
class TodosStore extends StoreBase {
    private _todos: string[] = [];

    addTodo(todo: string) {
        // Don't use .push here, we need a new array since the old _todos array was passed to the component by reference value
        this._todos = this._todos.concat(todo);
        this.trigger();
    }

    @autoSubscribe
    getTodos() {
        return this._todos;
    }
}

export = new TodosStore();
複製代碼

在組件中使用數據和方法

import * as React from 'react';
import { ComponentBase } from 'resub';

import TodosStore = require('./TodosStore');

interface TodoListState {
    todos?: string[];
}

class TodoList extends ComponentBase<{}, TodoListState> {
    protected _buildState(props: {}, initialBuild: boolean): TodoListState {
        return {
            todos: TodosStore.getTodos()
        }
    }

    render() {
        return (
            <ul className="todos">
                { this.state.todos.map(todo => <li>{ todo }</li> ) }
            </ul>
        );
    }
}

export = TodoList;
複製代碼

應該也算是很是簡潔的.並且有TS的類型約束, 出錯的機會要少不少. Redux的TS方法,我後面也會提到.

未完成,還有一些內容

相關文章
相關標籤/搜索