摘要:第一個功能是普通經(jīng)典類組件,也就是眾所周知的有狀態(tài)組件。我們準(zhǔn)備創(chuàng)建一個上下文環(huán)境來存放全局狀態(tài),然后把它的包裹在一個有狀態(tài)組件中,然后用來管理狀態(tài)。接下來我們需要用有狀態(tài)組件包裹我們的,利用它進行應(yīng)用狀態(tài)的管理。
原文地址對于想要跳過文章直接看結(jié)果的人,我已經(jīng)把我寫的內(nèi)容制作成了一個庫:use-simple-state,無任何依賴(除了依賴 react ),只有3kb,相當(dāng)輕量。
近幾年,應(yīng)我們的 app 增長的需要,web 應(yīng)用數(shù)量增長迅速,隨之而來的還有復(fù)雜性。為了使增加的復(fù)雜性易于處理,應(yīng)用某些新增的技巧和模式使得開發(fā)者可以更簡單的處理以及幫助我們建立更加健壯的應(yīng)用。
其中一個復(fù)雜性增長的主要領(lǐng)域是管理我們應(yīng)用的狀態(tài),因此為了避免這種復(fù)雜性,開發(fā)者使用了包含更新和獲取 app 狀態(tài)的庫。最著名的例子是 redux,它是 flux 模式的一種應(yīng)用。
一旦開發(fā)者開始學(xué)習(xí)使用像 redux 的庫,他們可能不太了解庫的內(nèi)部運行機制,因為一開始這并不明顯,即使很容易了解到這是更新一個全局可用的對象這樣的一個概念。
在這篇文章中,我們將會從零開始為 react 應(yīng)用建立一個我們自己的狀態(tài)管理解決方案。我們的解決方案最初只有幾行代碼,逐漸增加更高級的特性,最終將類似于 redux。
基本概念任何狀態(tài)管理工具只需要兩件東西:對整個應(yīng)用都可用的全局狀態(tài),和讀取以及更新它的能力。只有這些,真的。
這里展示一個狀態(tài)管理的簡單例子:
const state = {}; export const getState = () => state; export const setState = nextState => { state = nextState; };
上面的例子已經(jīng)盡可能的簡單,但它仍然包含了所有的要素:
一個全局可用的用于展現(xiàn)我們應(yīng)用狀態(tài)的值:state;讀取狀態(tài)的能力:getState;
更新狀態(tài)的能力:setState。
上面的例子對于我們真實應(yīng)用來說太過簡單,因此接下來我們將要構(gòu)建一個能讓 react 可用的解決方案。首先我們來重構(gòu)我們的例子,以讓它在 react 中可用。
react 狀態(tài)管理為了制作一個我們之前解決方案的 react 版本,我們需要應(yīng)用兩個 react 功能。第一個功能是普通經(jīng)典類組件,也就是眾所周知的有狀態(tài)組件。
第二個功能是 context API,它可以讓數(shù)據(jù)在整個 react 應(yīng)用可用。context 有兩部分:provider (生產(chǎn)者) 和 consumer (消費者),provider 就像它的名字所說的那樣,給應(yīng)用提供 context (data 數(shù)據(jù)),消費者意指當(dāng)我們讀取 context 時,我們就是消費者。
可以這樣理解 context:如果說 props 是顯式的傳送數(shù)據(jù),那么 context 就是隱式的傳送數(shù)據(jù)。
建造我們自己的狀態(tài)管理器現(xiàn)在我們知道了需要哪些工具,現(xiàn)在只要把它們合在一起就可以了。我們準(zhǔn)備創(chuàng)建一個上下文環(huán)境來存放全局狀態(tài),然后把它的 provider 包裹在一個有狀態(tài)組件中,然后用 provider 來管理狀態(tài)。
首先,我們使用 React.createContext 來創(chuàng)建上下文,它可以給我們提供 provider 和 consumer。
import { createContext } from "react"; const { Provider, Consumer } = createContext();
接下來我們需要用有狀態(tài)組件包裹我們的 provider,利用它進行應(yīng)用狀態(tài)的管理。我們也應(yīng)該把 consumer 導(dǎo)出為一個更加準(zhǔn)確的名稱。
import React, { Component, createContext } from "react"; const { Provider, Consumer } = createContext(); export const StateConsumer = Consumer; export class StateProvider extends Component { static defaultProps = { state: {} }; state = this.props.state; render () { return ({this.props.children} ); } }
在上面的例子中,StateProvider 是接收一個 state 來作為初始狀態(tài)的組件,并且使組件樹中當(dāng)前組件下面的任何組件都可以訪問到這個屬性。如果沒有提供 state,默認(rèn)會有一個空對象代替。
用我們的 StateProvider 包裹住根組件:
import { StateProvider } from "./stateContext"; import MyApp from "./MyApp"; const initialState = { count: 0 }; export default function Root () { return (); }
在我們完成上述代碼之后,就可以作為一個消費者從 MyApp 的任何地方獲得應(yīng)用的狀態(tài)。在這里我們會初始化我們的狀態(tài)為一個有一個 count 屬性的對象,所以無論什么時候我們想要立即獲取應(yīng)用的狀態(tài),我們就可以從這里獲得。
消費者使用 render 屬性 來傳遞上下文數(shù)據(jù),我們可以通過下面的一個函數(shù)作為 StateConsumer 的子組件的例子來查看。state 參數(shù)傳遞給函數(shù)用以展現(xiàn)我們應(yīng)用的當(dāng)前狀態(tài),作為我們的初始狀態(tài),state.count 為 0.
import { StateConsumer } from "./stateContext"; export default function SomeCount () { return ({state => ( ); }Count: {state.count}
)}
關(guān)于 StateConsumer 我們需要知道的很重要一點是在上下文中它會自動訂閱狀態(tài)的改變,因此當(dāng)我們的狀態(tài)改變后會重新渲染以顯示更新。這就是消費者的默認(rèn)行為,我們暫時還沒做能夠用到這個特性的功能。
更新狀態(tài)目前為止我們已經(jīng)可以讀取應(yīng)用的狀態(tài),以及在狀態(tài)改變時自動更新?,F(xiàn)在我們需要一種更新狀態(tài)的方法,為了做到這一點我們僅僅只需要在 StateProvider 里面更新狀態(tài)。
你之前可能已經(jīng)注意到了,我們給 StateProvider 傳遞了一個 state 屬性,也就是之后會傳遞給組件的 state 屬性。我們將使用 react 內(nèi)置的 this.setState 來更新:
export class StateProvider extends Component { static defaultProps = { state: {} }; state = this.props.state; render () { return ({this.props.children} ); }
繼續(xù)保持簡單的風(fēng)格,我們只給上下文傳遞 this.setState,這意味著我們需要稍微改變我們的上下文傳值,不只是傳遞 this.state,我們現(xiàn)在同時傳遞 state 和 setState。
當(dāng)我們用 StateConsumer 時可以用解構(gòu)賦值獲取 state 和 setState,然后我們就可以讀寫我們的狀態(tài)對象了:
export default function SomeCount () { return ({({ state, setState }) => ( <> Count: {state.count}
> )} );
}
* 有一點要注意的是由于我們傳遞了 react 內(nèi)置的 `this.setState` 作為我們的 `setState` 方法,新增的屬性將會和已有的狀態(tài)合并。這意味著如果我們有 `count` 以外的第二個屬性,它也會被自動保存。 * 現(xiàn)在我們的作品已經(jīng)可以用在真實項目中了(盡管還不是很有效率)。對 react 開發(fā)者來說應(yīng)該會覺得 API 很熟悉,由于使用了內(nèi)置的工具因此我們沒有引用任何新的依賴項。假如之前覺得狀態(tài)管理有點神奇,希望現(xiàn)在我們多少能夠了解它內(nèi)部的結(jié)構(gòu)。 ## 華麗的點綴 * 熟悉 redux 的人可能會注意到我們的解決方案缺少一些特性: >* 沒有內(nèi)置的處理副作用的方法,你需要通過 [redux 中間件](https://redux.js.org/advanced/middleware)來做這件事 >* 我們的 `setState` 依賴 react 默認(rèn)的 `this.setState` 來處理我們的狀態(tài)更新邏輯,當(dāng)使用內(nèi)聯(lián)方式更新復(fù)雜狀態(tài)時將可能引發(fā)混亂,同時也沒有內(nèi)置的方法來復(fù)用狀態(tài)更新邏輯,也就是 [redux reducer](https://redux.js.org/basics/reducers) 提供的功能。 >* 也沒有辦法處理異步的操作,通常由 [redux thunk](https://github.com/reduxjs/redux-thunk) 或者 [redux saga](https://github.com/redux-saga/redux-saga)等庫來提供解決辦法。 >* 最關(guān)鍵的是,我們沒辦法讓消費者只訂閱部分狀態(tài),這意味著只要狀態(tài)的任何部分更新都會讓每個消費者更新。 * 為了解決這些問題,我們模仿 redux 來應(yīng)用我們自己的 **actions**,**reducers**,和 **middleware**。我們也會為異步 actions 增加內(nèi)在支持。之后我們將會讓消費者只監(jiān)聽狀態(tài)內(nèi)的子狀態(tài)的改變。最后我們來看看如何重構(gòu)我們的代碼以使用新的 [**hooks api**](https://reactjs.org/docs/hooks-intro.html)。 ## redux 簡介 > 免責(zé)聲明:接下來的內(nèi)容只是為了讓你更容易理解文章,我強烈推薦你閱讀 [redux 官方](https://redux.js.org/introduction/motivation)完整的介紹。 > > 如果你已經(jīng)非常了解 redux,那你可以跳過這部分。 * 下面是一個 redux 應(yīng)用的數(shù)據(jù)流簡化流程圖: ![redux-data-flow](https://cdn-images-1.medium.com/freeze/max/60/1*15Mk9zBAz55PL6ryhH-sUg.png?q=20) * 如你所見,這就是單向數(shù)據(jù)流,從我們的 reducers 接收到狀態(tài)改變之后,觸發(fā) actions,數(shù)據(jù)不會回傳,也不會在應(yīng)用的不同部分來回流動。 * 說的更詳細(xì)一點: * 首先,我們觸發(fā)一個描述改變狀態(tài)的 action,例如 `dispatch({ type: INCREMENT_BY_ONE })` 來加1,同我們之前不同,之前我們是通過 `setState({ count: count + 1 })`來直接改變狀態(tài)。 * action 隨后進入我們的中間件,redux 中間件是可選的,用于處理 action 副作用,并將結(jié)果返回給 action,例如,假如在 action 到達 reducer 之前觸發(fā)一個 `SIGN_OUT` 的 action 用于從本地存儲里刪除所有用戶數(shù)據(jù)。如果你熟悉的話,這有些類似于 [express](https://expressjs.com/) 中間件的概念。 * 最后,我們的 action 到達了接收它的 reducer,伴隨而來的還有數(shù)據(jù),然后利用它和已有的狀態(tài)合并生成一個新的狀態(tài)。讓我們觸發(fā)一個叫做 `ADD` 的 action,同時把我們想發(fā)送過去增加到狀態(tài)的值也發(fā)送過去(叫做 payload )。我們的 reducer 會查找叫做 `ADD` 的 action,當(dāng)它發(fā)現(xiàn)后就會將 payload 里面的值和我們現(xiàn)有的狀態(tài)里的值加到一起并返回新的狀態(tài)。 * reducer 的函數(shù)如下所示: * ```javascript (state, action) => nextState
reducer 應(yīng)當(dāng)只是處理 state 和 action ,雖然簡單卻很強大。關(guān)鍵是要知道 reducer 應(yīng)當(dāng)永遠(yuǎn)是純函數(shù),這樣它們的結(jié)果就永遠(yuǎn)是確定的。
actions + dispatch現(xiàn)在我們已經(jīng)過了幾個 redux app 的關(guān)鍵部分,我們需要修改 app 來模仿一些類似的行為。首先,我們需要一些 actions 和觸發(fā)它們的方法。
我們的 action 會使用 action 創(chuàng)建器來創(chuàng)建,它們其實就是能生成 action 的簡單函數(shù),action 創(chuàng)建器使得測試,復(fù)用,傳遞 payload 數(shù)據(jù)更加簡單,我們也會創(chuàng)建一些 action type,其實就是字符串常量,為了讓他們可以被 reducer 復(fù)用,因此我們把它存儲到變量里:
// Action types const ADD_ONE = "ADD_ONE"; const ADD_N = "ADD_N"; // Actions export const addOne = () => ({ type: ADD_ONE }); export const addN = amount => ({ type: ADD_N, payload: amount });
現(xiàn)在我們來做一個 dispatch 的占位符函數(shù),我們的占位符只是一個空函數(shù),將會被用于替換上下文中的 setState 函數(shù),我們一會再回到這兒,因為我們還沒做接收 action 的 reducer 呢。
export class Provider extends React.PureComponent { static defaultProps = { state: {} }; state = this.props.state; _dispatch = action => {}; render () { return (reducers{this.props.children} ); } }
現(xiàn)在我們已經(jīng)有了一些 action,只需要一些 reducer 來接收就好了。回到之前的 reducer 函數(shù)標(biāo)記,它只是一個關(guān)于 action 和 state 的純函數(shù):
(state, action) => nextState
知道了這個,我們只需要傳遞組件的狀態(tài),然后在 reducer 里觸發(fā) action。對 reducer 來說,我們只想要一個對應(yīng)上面標(biāo)記的函數(shù)數(shù)組。我們之所以使用一個數(shù)組是因為可以使用數(shù)組的 Array.reduce 方法來迭代數(shù)組,最終生成我們的新狀態(tài):
export class Provider extends React.PureComponent { static defaultProps = { state: {}, reducers: [] }; state = this.props.state; _dispatch = action => { const { reducers } = this.props; const nextState = reducers.reduce((state, reducer) => { return reducer(state, action) || state; }, this.state); this.setState(nextState); }; render () { return ({this.props.children} ); } }
如你所見,我們所做的就是使用 reducer 來計算并獲得新狀態(tài),然后就像之前所做的,我們調(diào)用 this.setState 來更新 StateProvider 組件的狀態(tài)。
現(xiàn)在我們只需要一個實際的 reducer:
function countReducer ({ count, ...state }, { type, payload }) { switch (type) { case ADD_N: return { ...state, count: count + payload }; case ADD_ONE: return { ...state, count: count + 1 }; } }
我們的 reducer 只是檢查傳入的 action.type,然后假如匹配到之后將會更新相對應(yīng)的狀態(tài),否則就會在經(jīng)過 switch 判斷語句之后返回函數(shù)默認(rèn)的 undefined。我們的 reducer 和 redux 的 reducer 的一個重要的區(qū)別在當(dāng)我們不想更新狀態(tài)時,一般情況下我們會因為未匹配到 action type 而返回一個falsy 值,而 redux 則會返回未變化的狀態(tài)。
然后把我們的 reducer 傳進 StateProvider:
export default function Root () { return (); }
現(xiàn)在我們終于可以觸發(fā)一些 action,然后就會觀察到相對應(yīng)的狀態(tài)更新:
export default function SomeCount () { return ({({ state, dispatch }) => ( <> Count: {state.count}
> )} );
## 中間件 * 現(xiàn)在我們的作品已經(jīng)跟 redux 比較像了,只需要再增加一個處理副作用的方法就可以。為了達到這個目的,我們需要允許用戶傳遞中間件函數(shù),這樣當(dāng) action 被觸發(fā)時就會被調(diào)用了。 * 我們也想讓中間件函數(shù)幫助我們處理狀態(tài)更新,因此假如返回的 `null` 就不會被 action 傳遞給 reducer。redux 的處理稍微不同,在 redux 中間件你需要手動傳遞 action 到下一個緊鄰的中間件,假如沒有使用 redux 的 `next` 函數(shù)來傳遞,action 將不會到達 reducer,而且狀態(tài)也不會更新。 * 現(xiàn)在讓我們寫一個簡單的中間件,我們想通過它來尋找 `ADD_N` action,如果它找到了那就應(yīng)當(dāng)把 `payload` 和當(dāng)前狀態(tài)里面的 `count` 加和并輸出,但是阻止實際狀態(tài)的更新。
function countMiddleware ({ type, payload }, { count }) {
if (type === ADD_N) {
console.log(`${payload} + ${count} = ${payload + count}`); return null;
}
}
* 跟我們的 reducer 類似,我們會將中間件用數(shù)組傳進我們的 `StateProvider`。 * ```javascript export default function Root () { return (); }
最終我們會調(diào)用所有所有中間件,然后根據(jù)返回的結(jié)果決定是否應(yīng)當(dāng)阻止更新。由于我們傳進了一個數(shù)組,然而我們需要的是一個單個值,因此我們準(zhǔn)備使用 Array.reduce 來獲得我們的值。跟 reducer 類似,我們也會迭代數(shù)組依次調(diào)用每個函數(shù),然后將結(jié)果賦值給一個變量 continueUpdate。
由于中間件被認(rèn)為是一個高級特性,因此我們不想它變成強制性的,因此如果沒有在StateProvider 里面找到 middleware 屬性,我們會將 continueUpdate 置為默認(rèn)的 undefined。我們也會增加一個 middleware 數(shù)組來作默認(rèn)屬性,這樣的話 middleware.reduce 就不會因為沒傳東西而拋出錯誤。
export class StateProvider extends React.PureComponent { static defaultProps = { state: {}, reducers: [], middleware: [] }; state = this.props.state; _dispatch = action => { const { reducers, middleware } = this.props; const continueUpdate = middleware.reduce((result, middleware) => { return result !== null ? middleware(action, this.state) : result; }, undefined); if (continueUpdate !== null) { const nextState = reducers.reduce((state, reducer) => { return reducer(state, action) || state; }, this.state); this.setState(nextState); } }; render () { return ({this.props.children} ); } }
如你所見在第13行,我們會查看中間件函數(shù)的返回值。如果返回 null 我們就會跳過剩下的中間件函數(shù),continueUpdate 將為 null,意味著我們會中斷更新。
異步 action因為我們想讓我們的狀態(tài)管理器對真實生產(chǎn)環(huán)境有用,所以我們需要增加對異步 action 的支持,這意味著我們將可以處理像網(wǎng)絡(luò)請求類似案例的通用任務(wù)。我們借鑒下 Redux Thunk ,因為它的 API 很簡單,直觀而且有效。
我們所要做的就是檢查是否有為被調(diào)用的函數(shù)被傳遞到 dispatch,如果找到的話我們就會在傳遞 dispatch 和 state 時調(diào)用函數(shù),這樣就可以給用戶所寫的異步 action 執(zhí)行的機會。拿這個授權(quán) action 作為例子來看下:
const logIn = (email, password) => async dispatch => { dispatch({ type: "LOG_IN_REQUEST" }); try { const user = api.authenticateUser(email, password); dispatch({ type: "LOG_IN_SUCCESS", payload: user }); catch (error) { dispatch({ type: "LOG_IN_ERROR", payload: error }); } };
在上面的例子中我們寫了一個叫做 logIn 的 action 創(chuàng)建器,不是返回一個對象,它返回一個接收 dispatch 的函數(shù),這可以讓用戶在一個異步 API 請求的前面和后面觸發(fā)異步 action,根據(jù) API 不同的返回結(jié)果觸發(fā)不同的 action,這里我們在發(fā)生錯誤時發(fā)送一個錯誤 action。
做到這一點只需要在 StateProvider 里的 _dispatch 方法里檢查 action 的類型是不是 function:
export class StateProvider extends React.PureComponent { static defaultProps = { state: {}, reducers: [], middleware: [] }; state = this.props.state; _dispatch = action => { if (typeof action === "function") { return action(this._dispatch, this.state); } const { reducers, middleware } = this.props; const continueUpdate = middleware.reduce((result, middleware) => { return result !== null ? middleware(action, this.state) : result; }, undefined); if (continueUpdate !== null) { const nextState = reducers.reduce((state, reducer) => { return reducer(state, action) || state; }, this.state); this.setState(nextState); } }; render () { return ({this.props.children} ); } }
這里需要注意兩點:我們調(diào)用 action 為函數(shù),傳入 this.state,這樣用戶可以訪問異步 action 中已有的狀態(tài),我們也會返回函數(shù)調(diào)用的結(jié)果,允許開發(fā)者從他們的異步 action 中獲得一個返回值從而開啟更多的可能性,例如從 dispatch 觸發(fā)的 promise 鏈。
避免不必要的重新渲染Redux 的一個經(jīng)常被忽視的必要特性是它能在必須時才會對組件重新渲染(或者更準(zhǔn)確的說是 React-Redux? — react 跟 redux 的綁定)。為了做到這一點,它使用了 connect 高階組件,它提供了一個映射函數(shù) — mapStateToProps — 僅僅只在關(guān)聯(lián)的 mapStateToProps 的輸出改變時(只映射從現(xiàn)在開始的狀態(tài))才會觸發(fā)組件的重新渲染。如果不這樣的話,那么每次狀態(tài)更新都會讓組件使用 connect 來訂閱存儲改變?nèi)缓笾匦落秩尽?/p>
想想我們需要做的,我們需要一種方法來存儲 mapState 前面的輸出,這樣我們就可以比較兩者看看有沒有差異來決定我們是否需要繼續(xù)向前和重新渲染我們的組件。為了做到這一點我們需要使用一種叫做記憶化的進程,跟我們這行的許多事情一樣,對于一個想到簡單的進程來說,這是一個重要的詞,尤其是我們可以使用 React.Component 來存儲我們狀態(tài)的子狀態(tài),然后僅在我們檢測到 mapState 的輸出改變之后再更新。
接下來我們需要一種能夠跳過不必要的組件更新的方法。react 提供了一個生命周期方法 shouldComponentUpdate 可以讓我們達到目的。它將接收到的 props 和 state 當(dāng)做參數(shù)來讓我們同現(xiàn)有的 props 和 state 進行比較,如果我們返回 true 那么更新繼續(xù),如果返回 false 那么 react 將會跳過渲染。
class ConnectState extends React.Component { state = this.props.mapState(this.props.state); static getDerivedStateFromProps (nextProps, nextState) {} shouldComponentUpdate (nextProps) { if (!Object.keys(this.state).length) { this.setState(this.props.mapDispatch(this.props.state)); return true; } console.log({ s: this.state, nextState: nextProps.mapState(nextProps.state), state: this.props.mapState(this.state) }); return false; } render () { return this.props.children({ state: this.props.state, dispatch: this.props.dispatch }); } } export function StateConsumer ({ mapState, mapDispatch, children }) { return ({({ state, dispatch }) => ( ); }{children} )}
上面只是對我們接下來要做的事情的概述。它有了所以主要的部分:從我們的 context 接收更新,它使用了 getDerivedStateFromProps 和 shouldComponentUpdate,它也接收一個 render 屬性來作為子組件,就像默認(rèn)的消費者一樣。我們也會通過使用傳遞的 mapState 函數(shù)來初始化我們的消費者初始狀態(tài)。
現(xiàn)在這樣的話,shouldComponentUpdate 將只會在接收到第一次狀態(tài)更新之后渲染一次。之后它會記錄傳進的狀態(tài)和現(xiàn)有的狀態(tài),然后返回 false,阻止任何更新。
上面的解決方案中在 shouldComponentUpdate 內(nèi)部也調(diào)用了 this.setState,而我們都知道 this.setState 總是會觸發(fā)重新渲染。由于我們也會從 shouldComponentUpdate 里返回 true,這會產(chǎn)生一次額外的重新渲染,所以為了解決這個問題,我們將使用生命周期 getDerivedStateFromProps 來獲取我們的狀態(tài),然后我們再使用 shouldComponentUpdate 來決定基于我們獲取的狀態(tài)是否繼續(xù)更新進程。
如果我們檢查控制臺也可以看到全局的狀態(tài)更新,同時我們的組件阻止任何更新到 this.state 對象以至組件跳過更新:
現(xiàn)在我們知道了如何阻止不必要的更新,我們還需要一個可以智能的決定我們的消費者何時應(yīng)當(dāng)更新的方法。如果我們想要遞歸循環(huán)傳進來的 state 對象來查看每個屬性來看狀態(tài)是否有改變,但是這對于幫助我們理解有幫助卻對性能不利。我們沒辦法知道傳進來的 state 對象層級有多深或者多復(fù)雜,如果條件永遠(yuǎn)不滿足,那么遞歸函數(shù)將會無限期的執(zhí)行下去,因此我們準(zhǔn)備限制我們比較的作用域。
跟 redux 類似,我們準(zhǔn)備使用一個淺比較函數(shù)。淺在這里意味著我們在比較我們的對象是否相等的屬性的層級,意味著我們只會比較一層。因此我們將會檢查我們每個新狀態(tài)的頂層屬性是否等于我們現(xiàn)有狀態(tài)的同名屬性,如果同名屬性不存在,或者它們的值不同,我們將會繼續(xù)渲染,否則我們就認(rèn)為我們的狀態(tài)時相同的,然后阻止渲染。
function shallowCompare (state, nextState) { if ((typeof state !== "object" || state === null || typeof nextState !== "object" || nextState === null)) return false; return Object.entries(nextState).reduce((shouldUpdate, [key, value]) => state[key] !== value ? true : shouldUpdate, false); }
首先我們從檢查是否兩個 state 都是對象開始,如果不是那么我們就跳過渲染。在初始檢查之后我們把現(xiàn)有的狀態(tài)轉(zhuǎn)化為一個鍵值對的數(shù)組,并通過將數(shù)組減少為單個布爾值來檢查每個屬性的值與傳進來的 state 對象的值。
這是困難的部分,現(xiàn)在我們想用我們的 shallowCompare 函數(shù),實際上只是調(diào)用并檢查結(jié)果。如果它返回 true,我們就返回 true 來允許組件重新渲染,否則我們就返回 false 來跳過更新(然后我們獲得的狀態(tài)被放棄掉)。我們也想在 mapDispatch 存在的時候調(diào)用它。
class ConnectState extends React.Component { state = {}; static getDerivedStateFromProps ({ state, mapState = s => s }) { return mapState(state); } shouldComponentUpdate (nextProps, nextState) { return shallowCompare(this.state, nextState); } render () { return this.props.children({ state: this.state, dispatch: this.props.mapDispatch ? this.props.mapDispatch(this.props.dispatch) : this.props.dispatch }); } }
最后我們需要傳遞一個 mapState 函數(shù)讓我們消費者以只匹配我們的部分狀態(tài),這樣我們就會將它作為一個屬性傳給我們的 StateConsumer:
return (({ greeting: state.greeting })} mapDispatch={dispatch => dispatch}> // ...
現(xiàn)在我們只訂閱 greeting 里面的改變,因此假如我們更新了組件里的 count 將會被我們的全局狀態(tài)改變所忽略并且避免了一次重新渲染。
快速回顧如果你已經(jīng)做到了這一步,你就已經(jīng)見到了如何開發(fā)一個帶有 reducer 和 action 的 類 redux 的狀態(tài)管理庫。我們也覆蓋了更高級的特性,例如異步 action,中間件,以及如何讓我們只接收我們想要的狀態(tài)更新,從而避免我們的消費者每次全局狀態(tài)更新進而引起的重新渲染。
盡管 redux 其實做的比我們的解決方案要多得多,希望這個方案有助于澄清一些核心概念,而 redux 通常被認(rèn)為是一個更加高級的特性,但它的實現(xiàn)其實相對簡單。
想要對 redux 的內(nèi)部了解更加徹底,我強烈推薦你閱讀它在 github 的源碼。
我們目前為止的解決方案已經(jīng)有了真實項目所必須的工具和特性了。我們可以在一個 react 項目中使用它,不需要使用 redux,除非我們想要接入一些真正高級的功能。
Hooks
如果你還沒聽過它,它正在快速變成 react 的下一個大特性。這里有一段來自官方描述的簡單解釋:
Hooks 是一個讓你不必寫 class 就可以使用 state 和 react 其他特性的新功能。
hooks 提供給我們高階組件的所有能力,以及更加清晰和直觀的 API 來渲染屬性。
我們來看一個使用基本的 useState 鉤子的例子來看看它們是如何工作的:
import React, { useState } from "react";
function Counter () {
const [count, setCount] = useState(0);
return (
<>
Count: {count}
>
);
}
在上面的例子中,我們通過給 useState 傳遞一個 0 來初始化新狀態(tài),它會返回我們的狀態(tài):count,以及一個更新函數(shù) setCount。如果你以前沒見過的話,可能會奇怪 useState 是如何不在每次渲染時初始化,這是因為 react 在內(nèi)部處理了,因此我們無需擔(dān)心這一點。
讓我們暫時先忘掉中間件和異步 action,用 useReducer 鉤子來重新應(yīng)用我們的 provider,就像我們正在做的一樣,除了將 action 觸發(fā)到一個獲得新狀態(tài)的 reducer,它就像 useState 一樣工作。
知道了這個,我們只需要將 reducer 的邏輯從老的 StateProvider 拷貝到新的函數(shù)組件 StateProvider里就可以了:
export function StateProvider ({ state: initialState, reducers, middleware, children }) { const [state, dispatch] = useReducer((state, action) => { return reducers.reduce((state, reducer) => reducer(state, action) || state, state); }, initialState); return ({children} ); }
可以如此的簡單,但是當(dāng)我們想要保持簡單時,我們?nèi)匀粵]有完全掌握 hooks 的所有能力。我們也可以使用 hooks 來把我們的 StateConsumer 換為我們自己的鉤子,我們可以通過包裹 useContext 鉤子來做到:
const StateContent = createContext(); export function useStore () { const { state, dispatch } = useContext(StateContext); return [state, dispatch]; }
盡管之前當(dāng)我們創(chuàng)建我們的上下文時使用了解構(gòu)的 Provider 和 Consumer,但是這次我們會將它存到我們傳遞到 useContext 單個變量從而讓我們可不用 Consumer 就可以接入上下文。我們也將它命名為我們自己的 useStore 鉤子,因為 useState 是一個默認(rèn)的鉤子。
接下來我們來簡單地重構(gòu)下我們消費上下文數(shù)據(jù)的方法:
export default function SomeCount () { const [state, dispatch] = useStore(); return ( <>Count: {state.count}
> );
}
* 希望這些例子能讓你感受到 hooks 是如何的直觀、簡單和有效。我們減少了所需的代碼數(shù)量,并且給了我們一個友好、簡單的 API 供使用。 * 我們也想讓我們的中間件和內(nèi)置的異步 action 重新開始工作。為了做到這一點,我們將我們的 `useReducer` 包裹進一個自定義鉤子,在我們的 `StateProvider` 中被特殊的使用,然后簡單地重用我們老的狀態(tài)組件的邏輯就好了。 * ```javascript export function useStateProvider ({ initialState, reducers, middleware = [] }) { const [state, _dispatch] = useReducer((state, action) => { return reducers.reduce((state, reducer) => reducer(state, action) || state, state); }, initialState); function dispatch (action) { if (typeof action === "function") { return action(dispatch, state); } const continueUpdate = middleware.reduce((result, middleware) => { return result !== null ? middleware(action, state) : result; }, undefined); if (continueUpdate !== null) { _dispatch(action); } } return { state, dispatch }; }
在我們的老的解決方案中,我們想讓中間件是可選的,所以我們添加了一個空數(shù)組作為默認(rèn)值,同樣地這次我們也使用一個默認(rèn)的參數(shù)來替換默認(rèn)屬性。類似于我們老的 dispatch 函數(shù),我們調(diào)用了中間件,然后如果 continueUpdate !== null 我們就繼續(xù)更新狀態(tài)。我們也不會改變處理異步 action 的方式。
最終,我們將 useStateProvider 的結(jié)果和它的參數(shù)到我們的 provider,這我們之前沒怎么考慮:
export function StateProvider ({ state: initialState, reducers, middleware, children }) { return ({children} ); }
完結(jié)!
然而...你可能已經(jīng)注意到的一件事是我們對 hooks 的應(yīng)用可能沒有辦法跳過不必要的更新。這是因為 hooks 是在函數(shù)組件體內(nèi)被調(diào)用的,在這個階段 react 沒法擺脫渲染進程(在不適用一些技巧的前提下)。這沒有必要擔(dān)心,react 團隊已經(jīng)考慮到這一點而且計劃提供一種方法讓我們能夠在函數(shù)組件內(nèi)部終止更新。
在函數(shù)組件內(nèi)一旦我們有官方提供的方法來讓我們擺脫渲染,我就會回到這里來更新這篇文章。與此同時,我用 hooks 實現(xiàn)的這個庫還有消費者,這樣我們就可以訪問此功能。
總結(jié)綜上所述,我們已經(jīng)寫完了狀態(tài)管理的大部分功能,還逐步在它基礎(chǔ)上增加了一些類似 redux 的功能,包括 action,reducer,中間件以及一種比較狀態(tài)差異的方法來提升性能。我們也看到了可以使用新的 hooks API 可以如何簡化我們的代碼。
希望你能從這篇文章中獲得一些有用的東西,能夠?qū)σ恍└呒壐拍钣行┰S了解,同時可以讓我們在使用一些工具時比我們初次見到它時更加簡單。
在開始的時候提到過,我寫了一個庫,Use Simple State,看完這篇文章之后,你可以在我的 github頁面看到,我已經(jīng)使用 hooks 最終實現(xiàn)了,包括幾個新增的功能。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/109478.html
摘要:我現(xiàn)在寫的這些是為了解決和這兩個狀態(tài)管理庫之間的困惑。這甚至是危險的,因為這部分人將無法體驗和這些庫所要解決的問題。這肯定是要第一時間解決的問題。函數(shù)式編程是不斷上升的范式,但對于大部分開發(fā)者來說是新奇的。規(guī)模持續(xù)增長的應(yīng) 原文地址:Redux or MobX: An attempt to dissolve the Confusion 原文作者:rwieruch 我在去年大量的使用...
摘要:我的教程可能也會幫你一把其他的二分法展示型組件和容器型組件這種分類并非十分嚴(yán)格,這是按照它們的目的進行分類。在我看來,展示型組件往往是無狀態(tài)的純函數(shù)組件,容器型組件往往是有狀態(tài)的純類組件。不要把展示型組件和容器型組件的劃分當(dāng)成教條。 本文譯自Presentational and Container Components,文章的作者是Dan Abramov,他同時也是Redux和Crea...
摘要:一般來說,聲明式編程關(guān)注于發(fā)生了啥,而命令式則同時關(guān)注與咋發(fā)生的。聲明式編程可以較好地解決這個問題,剛才提到的比較麻煩的元素選擇這個動作可以交托給框架或者庫區(qū)處理,這樣就能讓開發(fā)者專注于發(fā)生了啥,這里推薦一波與。 本文翻譯自FreeCodeCamp的from-zero-to-front-end-hero-part。 繼續(xù)譯者的廢話,這篇文章是前端攻略-從路人甲到英雄無敵的下半部分,在...
摘要:要求通過要求數(shù)據(jù)變更函數(shù)使用裝飾或放在函數(shù)中,目的就是讓狀態(tài)的變更根據(jù)可預(yù)測性單向數(shù)據(jù)流。同一份數(shù)據(jù)需要響應(yīng)到多個視圖,且被多個視圖進行變更需要維護全局狀態(tài),并在他們變動時響應(yīng)到視圖數(shù)據(jù)流變得復(fù)雜,組件本身已經(jīng)無法駕馭。今天是 520,這是本系列最后一篇文章,主要涵蓋 React 狀態(tài)管理的相關(guān)方案。 前幾篇文章在掘金首發(fā)基本石沉大海, 沒什么閱讀量. 可能是文章篇幅太長了?掘金值太低了? ...
閱讀 2741·2021-11-22 15:22
閱讀 1653·2021-11-22 14:56
閱讀 3629·2021-09-22 15:12
閱讀 2415·2021-09-02 15:41
閱讀 2139·2021-08-27 16:26
閱讀 1126·2019-08-30 15:55
閱讀 2151·2019-08-29 17:30
閱讀 679·2019-08-29 16:26