React的狀態管理

phpsmarter發表於2019-04-22

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

React和資料的基本互動方式

在MVC程式構架中,React經常被稱為View層,但實際上並不完全是這樣, React實際對MVC模式做了新的構想. 本質上React只是藉助JSX語法實現的UI介面庫,但是UI都需要資料來填充,所以問題就是如何獲取資料,如何靈活的展現資料.

MVC的思想

MVC架構的基本思想:

  • 模型層(Model)就是資料層.
  • 檢視層(View)負責整個應用程式的展示.
  • 控制層(Controller)在應用程式中扶著提供資料處理的邏輯操作.

React處理資料和MVC有微妙的區別. 在由多個子元件組合而成的檢視(父元件)裡, 子元件可以管理自己的資料處理方式,而且也可以從父元件獲取資料,只需要在父元件中提供一個控制器就可以了.

React的思想

在React的元件中有兩種不同的資料型別:

  • props ,在建立元件的時候,props會作為引數傳遞給元件,這個就作為元件頂級的配置項,一旦定義好,元件就不能自行修改了. 在React定的父元件->子元件的資訊傳遞中,只能使用這一種方式.沒有其他的方法. Props是React元件庫的精華, 我們可以定義不同的Props來控制元件的表現形式.

  • state,state是元件內部的資料.React的精華實際就在state上,我們可以在父元件中定義一個state,然後以Props的形式傳遞給子元件, state只是一個JS物件,我們可以定義任何形式的屬性. state的定義多樣性,決定了你的應用的多樣性. 通過定義元件的state,可以實現基本的狀態管理,也可以實現類Redux管理方式, 還可以實現React-Hooks的管理方式. 如果深入一點,你需要知道,Redux其實就是一個只有State邏輯處理而沒有UI的React元件.

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

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

基本實現

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

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的狀態管理

或者我自己畫的

React的狀態管理

資料是單向流動的,從React-Redux元件流向應用的元件. 但是在第一張圖的右側的Actions似乎有流了回去, 這算是單向流動嗎? 這個問題時間用dispatch並不好理解, 用ReSub的觸發似乎要好一點. 後邊我會結合一個自己想的現實生活中的例項來解釋這個問題.

下面我要宣告我自己的一個學習體會, React經過幾年的高速發展, 構架不斷的向函數語言程式設計方向發展, 函式式元件內部的JSX程式碼結合傳入的資料,我們想要的應用就實現了. 所以歸結為兩點一個是函式式元件,另外一個就是資料. 在React中流動的資料僅僅只是JS物件,如果我們給這些物件新增了定義好了Type型別,那麼資料就可以井然有序的唄管理和組織,就是這麼簡潔,注意是簡潔並不是簡單, 要想設計好State也不是一件容易的事情. 下面我們要進入本文的主題了, 通俗的學習Redux.

React的狀態管理的權威-Redux

這裡我不想很正式的講解Redux,Redux文件寫的非常好,可能一開始看覺得很難,但是看過十幾遍之後,你會覺得甘之如飴. 沒有看十遍以上的,是苦的. 所以我想換個方法, 用通俗的方法來解釋一下,這個問題.作為看文件的補充.很多時候看問題要換個角度,或者換個容易理解的模型就比較容易理解了.

Redux的通俗理解

我們就從 這張圖的Store開始

React的狀態管理

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

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

從Store開始.

沃爾瑪的Store

如果你沒去過沃爾瑪,把沃爾瑪換成全家便利店也可以, 規模不同,結構和組織完全相同. 但是如果類比Redux的Store,大型超市的多人管理更類似些

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

Store初始化的貨架

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

Store的貨架擺滿貨物

基本大型超市會分成不同的樓層,然後分成不同的區域,不同的貨架 處理具體貨架的人是不同的,所以儘管很大,但是由於進行了分割槽,分層處理,管理是井井有條的.每個區域,每個分類,每個貨架都有不同的標籤來標識. 每種貨物的具體補貨,出貨,換貨等方法都相應的不同, 但是隻要找到具體每個區的負責員工就可以實現了. 看這個每天超市龐大的貨物吞吐量, 其實進入到超市之後,就像看不見的洋流一樣其實是在各自區域中獨立的流動. 看到這裡你有沒有具體的程式碼結構,資料夾結構如何安排? 我想按照貨品的不同分類主導程式碼的結構是比較很好的. 可以先看看gitpoint的程式碼,

React的狀態管理
.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根據你的訂閱地址把貨物傳送到幾個朋友手中.

React的狀態管理
圖片出處 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方法,我後面也會提到.

未完成,還有一些內容

相關文章