雖然 React 的狀態管理是一個老生常談的問題,網上和社區中也能搜到至關多的資料。這裏仍是想梳理下從我接觸 React 開始到如今對狀態管理的一些感想。前端
全部的新技術的出現和流行都是爲了解決特定的場景問題,這裏也會以一個很是簡單的例子做爲咱們故事的開始。react
有這樣一個需求,咱們須要在界面上展現某個商品的信息,可能咱們會這樣實現:程序員
import React, { PureComponent } from 'react'; export default class ProductInfo extends PureComponent { constructor(props) { super(props); this.state = { data: { sku: '', desc: '', }, }; } componentDidMount() { fetch('url', { id: this.props.id }) .then(resp => resp.json()) .then(data => this.setState({ data })); } render() { const { sku } = this.state.data; return ( <div>{sku}</div> ); } }
上述的場景雖然很是簡單,可是在咱們實際的需求開發中很是常見,採用上述的方式也能很好地解決這一類問題。json
咱們把場景變得稍微複雜一點,假如界面上有兩個部分都須要展現商品的信息,只是展現的商品的屬性不一樣而已,怎麼處理了?咱們也能夠像上面那樣再寫一個相似的組件,可是問題是咱們重複獲取了同一個商品的信息,爲了不重複獲取數據,那麼咱們就須要在兩個組件之間共享商品信息。redux
經過 props 解決數據共享問題,本質上是將數據獲取的邏輯放到組件的公共父組件中。代碼多是這樣的:後端
import React, { PureComponent } from 'react'; export default class App extends PureComponent { constructor(props) { super(props); this.state = { data: { sku: '', desc: '', }, }; } componentDidMount() { fetch('url', { id: this.props.id }) .then(resp => resp.json()) .then(data => this.setState({ data })); } render() { return ( <div> <ProductInfoOne data={this.state.data} /> <ProductInfoTwo data={this.state.data} /> </div> ); } } function ProductInfoOne({ data }) { const { sku } = data; return <div>{sku}</div>; } function ProductInfoTwo({ data }) { const { desc } = data; return <div>{desc}</div>; }
對於這種組件嵌套層次只有 一、2 層的場景,經過將數據獲取和存儲的邏輯上移到公共的父組件就能夠很好地解決。數組
可是若是界面呈現更加複雜一點,好比 ProductInfoOne 的子組件中也須要呈現商品的信息,咱們可能會想到繼續經過 props 向下傳遞數據,問題是隨着嵌套的層次愈來愈深,數據須要從最外層一直傳遞到最裏層,整個代碼的可讀性和維護性會變差。咱們但願打破數據「層層傳遞」而子組件也能取到父輩組件中的數據。緩存
React 16.3 的版本引入了新的 Context API,Context API 自己就是爲了解決嵌套層次比較深的場景中數據傳遞的問題,看起來很是適合解決咱們上面提到的問題。咱們嘗試使用 Context API 來解決咱們的問題:性能優化
// context.js const ProductContext = React.createContext({ sku: '', desc: '', }); export default ProductContext; // App.js import React, { PureComponent } from 'react'; import ProductContext from './context'; const Provider = ProductContext.Provider; export default class App extends PureComponent { constructor(props) { super(props); this.state = { data: { sku: '', desc: '', }, }; } componentDidMount() { fetch('url', { id: this.props.id }) .then(resp => resp.json()) .then(data => this.setState({ data })); } render() { return ( <Provider value={this.state.data}> <ProductInfoOne /> <ProductInfoTwo /> </Provider> ); } } // ProductInfoOne.js import React, { PureComponent } from 'react'; import ProductContext from './context'; export default class ProductInfoOne extends PureComponent { static contextType = ProductContext; render() { const { sku } = this.context; return <div>{sku}</div>; } } // ProductInfoTwo.js import React, { PureComponent } from 'react'; import ProductContext from './context'; export default class ProductInfoTwo extends PureComponent { static contextType = ProductContext; render() { const { desc } = this.context; return <div>{desc}</div>; } }
看起來一切都很美好,到目前爲止咱們也只是使用了 React 庫自己的功能,並無引入任何第三方的庫,實際上對於這類比較簡單的場景,使用以上的方式來解決確實是最直接、簡單的方案。服務器
現實中的需求每每要稍微複雜點,上述的幾個場景中咱們偏重於信息的呈現,而真實場景中咱們避免不了一些交互的操做,好比咱們須要在呈現商品信息的同時還須要能夠編輯商品的信息,因爲 ProductInfoOne、ProductInfoTwo 是受控組件,而且數據源在 App 組件中,爲了實現數據的修改,咱們可能經過 Context API 傳遞修改數據的「回調函數」。
上述的幾個場景中咱們偏重於有嵌套關係的組件之間數據的共享,若是場景再複雜一點,假設平行組件之間須要共享數據,例如和 App 沒有父子關係的 App1 組件也須要呈現商品信息,怎麼辦,看起來 Conext API 也是一籌莫展。
終於到了 Redux,相信不少讀者以爲囉裏囉嗦,可是本着技術方案是爲了解決特定問題的原則,仍是以爲有必要作一些鋪墊,若是你的問題場景沒有複雜到 React 自己沒有太好的解決方式的地步,建議也不要引入額外的技術(有更好的解決方案除外),包括 Redux。
Redux 確實是很強大,目前在 React 狀態管理中也仍是最活躍和使用最廣的解決方案。這裏仍是引用一張圖(圖片來源)來簡單說明下 Redux 解決問題的思路:
這裏不想講太多 Redux 的概念和原理,網上也是一大推資料,相信不少人也對 Redux 很是熟悉了。先看看採用 Redux 解決咱們上述問題,代碼大概是這樣的(只列出部分重點代碼):
// store.js import { createStore } from 'redux'; import reducer from './reducer'; const store = createStore(reducer); export default store; // reducer.js import * as actions from './actions'; import { combineReducers } from 'redux'; function ProductInfo(state = {}, action) { switch (action.type) { case actions.SET_SKU: { return { ...state, sku: action.sku }; } case actions.SET_DESC: { return { ...state, desc: action.desc }; } case actions.SET_DATA: { return { ...state, ...action.data }; } default: { return state; } } } const reducer = combineReducers({ ProductInfo, }); export default reducer; // action.js export const SET_SKU = 'SET_SKU'; export const SET_DESC = 'SET_DESC'; export const SET_DATA = 'SET_DATA'; export function setSku(sku) { return { type: SET_SKU, sku, }; } export function setDesc(desc) { return { type: SET_DESC, desc, }; } export function setData(data) { return { type: SET_DESC, data, }; } // App.js import React, { PureComponent } from 'react'; import { Provider } from 'react-redux'; import store from './store'; import * as actions from './actions'; class App extends PureComponent { componentDidMount() { fetch('url', { id: this.props.id }) .then(resp => resp.json()) .then(data => this.props.dispatch(actions.setData(data))); } render() { return ( <Provider store={store}> <ProductInfoOne /> <ProductInfoTwo /> </Provider> ); } } function mapStateToProps() { return { }; } function mapDispatchToProps(dispatch) { return { dispatch, }; } export default connect(mapStateToProps, mapDispatchToProps)(App); // ProductInfoOne.js import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import * as actions from './actions'; class ProductInfoOne extends PureComponent { onEditSku = (sku) => { this.props.dispatch(actions.setSku(sku)); }; render() { const { sku } = this.props.data; return ( <div>{sku}</div> ); } } function mapStateToProps(state) { return { data: state.ProductInfo, }; } function mapDispatchToProps(dispatch) { return { dispatch, }; } export default connect(mapStateToProps, mapDispatchToProps)(ProductInfoOne); // ProductInfoTwo.js import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; import * as actions from './actions'; class ProductInfoTwo extends PureComponent { onEditDesc = (desc) => { this.props.dispatch(actions.setDesc(desc)); }; render() { const { desc } = this.props.data; return ( <div>{desc}</div> ); } } function mapStateToProps(state) { return { data: state.ProductInfo, }; } function mapDispatchToProps(dispatch) { return { dispatch, }; } export default connect(mapStateToProps, mapDispatchToProps)(ProductInfoTwo);
Redux 確實可以解決咱們上面提到的問題,從代碼和 Redux 的原理中咱們也能夠知道,Redux 作了不少概念的抽象和分層,store 專門負責數據的存儲,action 用於描述數據修改的動做,reducer 用於修改數據。咋一看,Redux 使咱們的代碼變得更加複雜了,可是它抽象出來的這些概念和一些強制的規定,會讓數據的共享和修改變得有跡可循,這種約定的規則,在多人協助開發的大型項目中,會讓代碼的邏輯更加清晰、可維護性更好。
可是,Redux 被你們詬病的地方也不少,網上也有愈來愈多對 Redux 批判的聲音,暫且不談技術的學習成本,筆者在使用過程當中以爲有幾點讓人抓狂的地方:
那麼有沒有一些更加輕量級的狀態管理庫了?
Mobx 從 2016 年開始發佈第一個版本,到如今短短兩年多的時間,發展也是很是迅速,受到愈來愈多人的關注。MobX 的實現思路很是簡單直接,相似於 Vue 中的響應式的原理,其實質能夠簡單理解爲觀察者模式,數據是被觀察的對象,「響應」是觀察者,響應能夠是計算值或者函數,當數據發生變化時,就會通知「響應」執行。借用一張網上的圖(圖片來源)描述下原理:
Mobx 我理解的最大的好處是簡單、直接,數據發生變化,那麼界面就從新渲染,在 React 中使用時,咱們甚至不須要關注 React 中的 state,咱們看下用 MobX 怎麼解決咱們上面的問題:
// store.js import { observable } from 'mobx'; const store = observable({ sku: '', desc: '', }); export default store; // App.js import React, { PureComponent } from 'react'; import store from './store.js'; export default class App extends PureComponent { componentDidMount() { fetch('url', { id: this.props.id }) .then(resp => resp.json()) .then(data => Object.assign(store, data)); } render() { return ( <div> <ProductInfoOne /> <ProductInfoTwo /> </div> ); } } // ProductInfoOne.js import React, { PureComponent } from 'react'; import { action } from 'mobx'; import { observer } from 'mobx-react'; import store from './store'; @observer class ProductInfoOne extends PureComponent { @action onEditSku = (sku) => { store.sku = sku; }; render() { const { sku } = store; return ( <div>{sku}</div> ); } } export default ProductInfoOne; // ProductInfoTwo.js import React, { PureComponent } from 'react'; import { action } from 'mobx'; import { observer } from 'mobx-react'; import store from './store'; @observer class ProductInfoTwo extends PureComponent { @action onEditDesc = (desc) => { store.desc = desc; }; render() { const { desc } = store; return ( <div>{desc}</div> ); } } export default ProductInfoTwo;
稍微解釋下用到的新的名詞,observable 或者 @observable
表示聲明一個可被觀察的對象,@observer
標識觀察者,其本質是將組件中的 render 方法用 autorun 包裝了下,@action
描述這是一個修改數據的動做,這個註解是可選的,也就是不用也是能夠的,可是官方建議使用,這樣代碼邏輯更清晰、底層也會作一些性能優化、而且在調試的時候結合調試工具可以提供有用的信息。
咱們能夠對比下 Redux 的方案,使用 MobX 後代碼大大減小,而且數據流動和修改的邏輯更加直接和清晰。聲明一個可被觀察的對象,使用 @observer
將組件中的 render 函數變成觀察者,數據修改直接修改對象的屬性,咱們須要作的就是這些。
可是從中也能夠看到,Mobx 的數據修改說的好聽點是「靈活」,很差聽點是「隨意」,好在社區有一些其餘的庫來優化這個問題,好比 mobx-state-tree 將 action 在模型定義的時候就肯定好,將修改數據的動做集中在一個地方管理。不過相對於 Redux 而言,Mobx 仍是靈活不少,它沒有太多的約束和規則,在少許開發人員或者小型項目中,會很是地自由和高效,可是隨着項目的複雜度和開發人員的增長,這種「無約束」反而可能會帶來後續高昂的維護成本,反之 Redux 的「約束」會確保不一樣的人寫出來的代碼幾乎是一致的,由於你必須按照它約定的規則來開發,代碼的一致性和可維護性也會更好。
前面提到的不論是 Redux 仍是 MobX, 二者都是側重於管理數據,說的更明白點就是怎樣存儲、更新數據,可是數據是從哪裏來的,它們是不關注的。那麼將來有沒有一種新的思路來管理數據了,GraphQL 其實提出了一種新的思路。
咱們開發一個組件或者前端系統的時候,有一部分的數據是來自於後臺的,好比上面場景中的商品信息,有一部分是來自於前臺的,好比對話框是否彈出的狀態。GraphQL 將遠程的數據和本地的數據進行了統一,讓開發者感受到全部的數據都是查詢出來的,至因而從服務端查詢仍是從本地查詢,開發人員不須要關注。
這裏不講解 GraphQL 的具體原理和使用,你們有興趣能夠去查看官網的資料。咱們看看若是採用 GraphQL 來解決咱們上面的問題,代碼會是怎麼樣的?
// client.js import ApolloClient from 'apollo-boost'; const client = new ApolloClient({ uri: 'http://localhost:3011/graphql/productinfo' }); export default client; // app.js import React from 'react'; import { ApolloProvider, Query, Mutation } from 'react-apollo'; import gql from 'graphql-tag'; import client from './index'; import ProductInfoOne from './ProductInfoOne'; import ProductInfoTwo from './ProductInfoTwo'; const GET_PRODUCT_INFO = gql` query ProductInfo($id: Int) { productInfo(id: $id){ id sku desc } } `; export default class App extends React.PureComponent { constructor(props) { super(props); this.state = { id: 1, }; } render() { return ( <ApolloProvider client={client}> <Query query={GET_PRODUCT_INFO} variables={{ id: this.state.id }}> {({ loading, error, data }) => { if (loading) return 'loading...'; if (error) return 'error...'; if (data) { return ( <div> <ProductInfoOne data={data.productInfo} /> <ProductInfoTwo data={data.productInfo} /> </div> ); } return null; }} </Query> </ApolloProvider> ); } } // ProductInfoOne.js import React from 'react'; import { Mutation } from 'react-apollo'; import gql from 'graphql-tag'; const SET_SKU = gql` mutation SetSku($id: Int, $sku: String){ setSku(id: $id, sku: $sku) { id sku desc } } `; export default class ProductInfoOne extends React.PureComponent { render() { const { id, sku } = this.props.data; return ( <div> <div>{sku}</div> <Mutation mutation={SET_SKU}> {(setSku) => ( <button onClick={() => { setSku({ variables: { id: id, sku: 'new sku' } }) }}>修改 sku</button> )} </Mutation> </div> ); } } // ProductInfoTwo.js import React from 'react'; import { Mutation } from 'react-apollo'; import gql from 'graphql-tag'; const SET_DESC = gql` mutation SetDesc($id: Int, $desc: String){ setDesc(id: $id, desc: $desc) { id sku desc } } `; export default class ProductInfoTwo extends React.PureComponent { render() { const { id, desc } = this.props.data; return ( <div> <div>{desc}</div> <Mutation mutation={SET_DESC}> {(setDesc) => ( <button onClick={() => { setDesc({ variables: { id: id, desc: 'new desc' } }) }}>修改 desc</button> )} </Mutation> </div> ); } }
咱們能夠看到,GraphQL 將數據封裝成 Query 的 GraphQL 語句,將數據的更新封裝成了 Mutation 的 GraphQL 語句,對開發者來說,我須要數據,因此我須要一個 Query 的查詢,我須要更新數據,因此我須要一個 Mutation 的動做,數據既能夠來自於遠端服務器也能夠來自於本地。
使用 GraphQL 最大的問題是,須要服務器端支持 GraphQL 的接口,才能真正發揮它的威力,雖然如今主流的幾種 Web 服務器端語言,好比 Java、PHP、Python、JavaScript,均有對應的實現版本,可是將已有的系統整改成支持 GraphQL,成本也是很是大的;而且 GraphQL 的學習成本也不低。
可是 GraphQL 確實相比於傳統的狀態管理方案,提供了新的思路。咱們和後臺人員制定接口時,老是會有一些模糊有爭議的灰色地帶,好比頁面上要展現一個列表,前端程序員的思惟是表格中的一行是一個總體,後臺應該返回一個數組,數組中的每一個元素對應的就是表格中的一行,可是後端程序員可能會從數據模型設計上區分動態數據和靜態數據,前臺應該分別獲取動態數據和靜態數據,而後再拼裝成一行數據。後端程序員的思惟是我有什麼,是生產者的視角;前端程序員的思惟是我須要什麼,是消費者的視角。可是 GraphQL 會強迫後臺人員在開發接口的時候從消費者的視角來制定先後臺交互的數據,由於 GraphQL 中的查詢參數每每是根據界面呈現推導出來的。這樣對前端而言,會減小一部分和後臺制定接口的糾紛,同時也會把一部分的工做「轉嫁」到後臺。
文章可隨意轉載,但請保留此 原文連接。很是歡迎有激情的你加入 ES2049 Studio,簡歷請發送至 caijun.hcj(at)alibaba-inc.com 。