文中涉及的React demo代碼都使用了16.8的新增特性Hooks:
它可以讓你在不編寫
class的情況下使用state以及其他的React特性。
前言
剛立項時,你的所有代碼可能就只有一個根組件Root —— 擼起袖子就是干!
項目慢慢有了起色,一些哥們就拆分了一些子組件,必然,它們間將有一些數(shù)據(jù)流動 —— 問題不大,可以讓它們緊密聯(lián)系。
現(xiàn)在項目進(jìn)展火爆,業(yè)務(wù)N倍增長,不得不拆出更多的子孫組件出來,實現(xiàn)更多復(fù)雜業(yè)務(wù) —— 但愿邏輯比較簡單,數(shù)據(jù)流動是一層層往下
不過,現(xiàn)實總是很殘酷,父子孫組件間關(guān)系往往混亂無比。
怎么辦,怎么辦???
只要思想不滑坡,辦法總比困難多
- 方案1,梳理項目邏輯,重新設(shè)計組件??
- 方案2,辭職,換個公司重開???
確實,項目迭代過程中,不可避免地就會出現(xiàn)組件間狀態(tài)共享,而導(dǎo)致邏輯交錯,難以控制。
那我們就會想:"能不能有一種實踐規(guī)范,將所有可能公用的狀態(tài)、數(shù)據(jù)及能力提取到組件外,數(shù)據(jù)流自上往下,哪里需要哪里自己獲取,而不是prop drilling",大概長這樣:
于是這樣一種數(shù)據(jù)結(jié)構(gòu)冒了出來:
const store = {
state: {
text: 'Goodbye World!'
},
setAction (text) {
this.text = text
},
clearAction () {
this.text = ''
}
}
存在外部變量store:
-
state來存儲數(shù)據(jù) - 有一堆功能各異的
action來控制state的改變
再加上強制約束:只能通過調(diào)用action來改變state,然后我們就可以通過action清晰地掌握著state的動向,那么日志、監(jiān)控、回滾等能力還有啥擔(dān)心的。
其實,這就是Flux的早早期雛形。
Flux
2013年,F(xiàn)acebook亮出React的時候,也跟著帶出的Flux。Facebook認(rèn)為兩者相輔相成,結(jié)合在一起才能構(gòu)建大型的JavaScript應(yīng)用。
做一個容易理解的對比,React是用來替換jQuery的,那么Flux就是以替換Backbone.js、Ember.js等MVC一族框架為目的。
如上圖,數(shù)據(jù)總是“單向流動”,相鄰部分不存在互相流動數(shù)據(jù)的現(xiàn)象,這也是Flux一大特點。
-
View發(fā)起用戶的Action -
Dispatcher作為調(diào)度中心,接收Action,要求Store進(jìn)行相應(yīng)更新 -
Store處理主要邏輯,并提供監(jiān)聽能力,當(dāng)數(shù)據(jù)更新后觸發(fā)監(jiān)聽事件 -
View監(jiān)聽到Store的更新事件后觸發(fā)UI更新
感興趣可以看看每個部分的具體含義:
Action
plain javascript object,一般使用
type與payload描述了該action的具體含義。
在Flux中一般定義actions:一組包含派發(fā)action對象的函數(shù)。
// actions.js
import AddDispatcher from '@/dispatcher'
export const counterActions = {
increment (number) {
const action = {
type: 'INCREMENT',
payload: number
}
AddDispatcher.dispatch(action)
}
}
以上代碼,使用counterActions.increment,將INCREMENT派發(fā)到Store。
Dispatcher
將
Action派發(fā)到Store,通過Flux提供的Dispatcher注冊唯一實例。
Dispatcher.register方法用來登記各種Action的回調(diào)函數(shù)
import { CounterStore } from '@/store'
import AddDispatcher from '@/dispatcher'
AppDispatcher.register(function (action) {
switch (action.type) {
case INCREMENT:
CounterStore.addHandler();
CounterStore.emitChange();
break;
default:
// no op
}
});
以上代碼,AppDispatcher收到INCREMENT動作,就會執(zhí)行回調(diào)函數(shù),對CounterStore進(jìn)行操作。
Dispatcher只用來派發(fā)Action,不應(yīng)該有其他邏輯。
Store
應(yīng)用狀態(tài)的處理中心。
Store中復(fù)雜處理業(yè)務(wù)邏輯,而由于數(shù)據(jù)變更后View需要更新,所以它也負(fù)責(zé)提供通知視圖更新的能力。
因為其隨用隨注冊,一個應(yīng)用可以注冊多個Store的能力,更新Data Dlow為
細(xì)心的朋友可以發(fā)現(xiàn)在上一小節(jié)CounterStore中調(diào)用了emitChange的方法 —— 對,它就是用來通知變更的。
import { EventEmitter } from "events"
export const CounterStore = Object.assign({}, EventEmitter.prototype, {
counter: 0,
getCounter: function () {
return this.counter
},
addHandler: function () {
this.counter++
},
emitChange: function () {
this.emit("change")
},
addChangeListener: function (callback) {
this.on("change", callback)
},
removeChangeListener: function (callback) {
this.removeListener("change", callback)
}
});
以上代碼,CounterStore通過繼承EventEmitter.prototype獲得觸發(fā)emit與監(jiān)聽on事件能力。
View
Store中的數(shù)據(jù)的視圖展示
View需要監(jiān)聽視圖中數(shù)據(jù)的變動來保證視圖實時更新,即
- 在組件中需要添加
addChangeListerner - 在組件銷毀時移除監(jiān)聽
removeChangeListener
我們看個簡單的Couter例子,更好的理解下實際使用。
(手動分割)
認(rèn)真體驗的朋友可能會注意到:
- 點擊
reset后,store中的couter被更新(沒有emitChange所以沒實時更新視圖); - 業(yè)務(wù)邏輯與數(shù)據(jù)處理邏輯交錯,代碼組織混亂;
好,打住,再看個新的數(shù)據(jù)流。
Redux
- 用戶與
View進(jìn)行交互 - 通過
Action Creator派發(fā)action - 到達(dá)
Store后拿到當(dāng)前的State,一并交給Reducer -
Reducer經(jīng)過處理后返回全新的State給Store -
Store更新后通知View,完成一次數(shù)據(jù)更新
Flux的基本原則是“單向數(shù)據(jù)流”,Redux在此基礎(chǔ)上強調(diào):
- 唯一數(shù)據(jù)源(Single Source of Truth):整個應(yīng)用只保持一個
Store,所有組件的數(shù)據(jù)源就是該Store的狀態(tài)。 - 保持狀態(tài)只讀(State is read-only):不直接修改狀態(tài),要修改
Store的狀態(tài),必須要通過派發(fā)一個action對象完成。 - 數(shù)據(jù)改變只能通過純函數(shù)完成(Changes are made with pure funtions):這里所說的純函數(shù)指
reducer。
感興趣可以看看每個部分的具體含義:
(Redux的源碼及其短小優(yōu)雅,有想嘗試閱讀源碼的朋友可以從它開始)
Store
應(yīng)用唯一的數(shù)據(jù)存儲中心
import { createStore } from 'redux'
const store = createStore(fn)
以上代碼,使用redux提供的createStore函數(shù),接受另一個函數(shù)fn(即稍后提到的Reducers)作為參數(shù),生成應(yīng)用唯一的store。
可以看看簡單實現(xiàn)的createStore函數(shù)
const createStore = (reducer) => {
let state
let listeners = []
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener())
}
const subscribe = (listener) => {
listeners.push(listener)
return () => {
listeners = listeners.filter(l => l !== listener)
}
}
dispatch({})
return { getState, dispatch, subscribe }
}
本人看源碼有個小技巧,一般先從導(dǎo)出找起,再看return。
如上,return出去三個能力:
-
getState: 獲取state的唯一方法,state被稱為store的快照 -
dispatch: view派發(fā)action的唯一方法 -
subscribe: 注冊監(jiān)聽函數(shù)(核心,待會要考),返回解除監(jiān)聽
注意到以上代碼片段最后,dispatch了一個空對象,是為了生成初始的state,學(xué)習(xí)了reducer的寫法后可以解釋原理。
當(dāng)然,createStore還可以接收更多的參數(shù),如:preloadedState(默認(rèn)state),enhancer(store的超能力蘑菇)等,我們后面會分析到。
Action
plain javascript object,一般使用
type與payload描述了該action的具體含義。
在redux,type屬性是必須的,表示Action的名稱,其他屬性可以自由設(shè)置,參照規(guī)范。
const actions = {
type: 'ADD_TODO',
payload: 'Learn Redux'
}
可以用Action Creator批量來生成一些Action,如下addTodo就是一個Action Creator,它接受不同的參數(shù)生成不同的action:
function addTodo(text) {
return {
type: 'ADD_TODO',
payload: text
}
}
const action = addTodo('Learn Redux')
reducer
純函數(shù),根據(jù)action更新store
(previousState, action) => newState
以上,是reducer的函數(shù)簽名,接收來自view的action,并從store上拿到最新state,經(jīng)過處理后返回一個全新的state更新視圖。
const reducers = (state = defaultState, action) => {
const { type, payload } = action
switch (type) {
case 'ADD_TODO':
return {
...state,
counter: state.counter + (+payload)
}
default:
return state
}
}
以上代碼,createStore留下的懸念可以從default分支獲得答案。
reducer返回的結(jié)果一定要是一個全新的state,尤其是涉及到引用數(shù)據(jù)類型的操作時,因為react對數(shù)據(jù)更新的判斷都是淺比較,如果更新前后是同一個引用,那么react將會忽略這一次更新。
理想狀態(tài)state結(jié)構(gòu)層級可能比較簡單,那么如果state樹枝葉后代比較復(fù)雜時怎么辦(state.a.b.c)?
const reducers = (state = {}, action) => {
const { type, payload } = action
switch(type) {
case 'ADD':
return {
...state,
a: {
...state.a,
b: {
...state.a.b,
c: state.a.b.c.concat(payload)
}
}
}
default:
return state
}
}
先不討論以上寫法風(fēng)險如何,就這一層層看著都吐。
既然這樣,我們再想想辦法。
前面提到,Redux中store唯一,所以我們只要能保證在reducer中返回的state是一個完整的結(jié)構(gòu)就行,那是不是可以:
const reducers = (state = {}, action) => {
return {
A: reducer1(state.A, action),
B: reducer2(state.B, action),
C: reducer3(state.C, action)
}
}
以上,我們曲線救國,將復(fù)雜的數(shù)據(jù)結(jié)構(gòu)拆分,每個reducer管理state樹不同枝干,最后再將所有reducer合并后給createStore,這正是combineReducer的設(shè)計思路。
combineReducer
import { combineReducers, createStore } from 'redux'
const reducers = combineReducers({
A: reducer1,
B: reducer2,
C: reducer3
})
const store = createStore(reducers)
以上,根據(jù)state的key去執(zhí)行相應(yīng)的子reducer,并將返回結(jié)果合并成一個大的state對象。
可以看下簡單實現(xiàn):
const combineReducers = reducers => (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key] = reducers[key](state[key], action)
return nextState
}, {})
}
以上介紹了Redux的基本能力,再看個Demo加深加深印象。
(再次手動分割)
可以注意到一個痛點:
-
component得主動去訂閱store.subscribe``state的變更,讓代碼顯得很蠢,不太“雅”。
Flux vs Redux
好,redux的基本面都覆蓋了,它是基于Flux的核心思想實現(xiàn)的一套解決方案,從以上分析我們可以感受到區(qū)別:
以上,從store與dispatcher兩個本質(zhì)區(qū)別比對了二者,相信你們英文一定比我好,就不翻譯了。
(不要問我為什么要麻將牌+英文排列,問就是“中西合璧”)
Redux和Flux類似,只是一種思想或者規(guī)范,它和React之間沒有關(guān)系。Redux支持React、Angular、Ember、jQuery甚至純JavaScript。
因為React包含函數(shù)式的思想,也是單向數(shù)據(jù)流,和Redux很搭,所以一般都用Redux來進(jìn)行狀態(tài)管理。
當(dāng)然,不是所有項目都無腦推薦redux,Dan Abramov很早前也提到“You Might Not Need Redux”,只有遇到react不好解決的問題我們才考慮使用redux,比如:
- 用戶的使用方式復(fù)雜
- 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)
- 多個用戶之間可以協(xié)作/與服務(wù)器大量交互,或者使用了WebSocket
- View要從多個來源獲取數(shù)據(jù)
- ...
(再再次手動分割)
好,我們繼續(xù)來聊Redux。
以上,我們處理的都是同步且邏輯簡單的Redux使用場景,真正的業(yè)務(wù)開發(fā)場景遠(yuǎn)比這復(fù)雜,各種異步任務(wù)不可避免,這時候怎么辦?
一起跟著Redux的Data Flow分析一下:
-
View:state的視覺層,與之一一對應(yīng),不合適承擔(dān)其他功能; -
Action:描述一個動作的具體內(nèi)容,只能被操作,自己不能進(jìn)行任何操作 -
Reducer:純函數(shù),只承擔(dān)計算state的功能,不合適承擔(dān)其他功能
看來如果想要在action發(fā)出后做一些額外復(fù)雜的同步/異步操作,只有在派發(fā)action,即dispatch時可以做點手腳,我們稱負(fù)責(zé)這些復(fù)雜操作:中間件Middleware。
Middleware
It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer.
以上直譯:Middleware提供了第三方的拓展能力,作用于在發(fā)起action與action到達(dá)reducer之間。
比如我們想在發(fā)送action前后添加打印功能,中間件雛形大概就是這樣:
let next = store.dispatch
store.dispatch = function Logger(store, action) {
console.log('dispatching', action)
next(action)
console.log('next state', store.getState())
}
// 遵循middleware規(guī)范的currying寫法
const Logger = store => next => action => {
console.log('dispatching', action)
next(action)
console.log('next state', store.getState())
}
先補充個前置知識,前面說過createStore可以接收除了reducers之外更多的參數(shù),其中一個參數(shù)enhancer就是表示你要注冊的中間件們,再看看createStore怎么用它?
// https://github.com/reduxjs/redux/blob/v4.0.4/src/createStore.js#L53
...
enhancer(createStore)(reducer, preloadedState)
...
了解了以上代碼后,我們來看看redux源碼是如何實現(xiàn)store.dispatch的偷梁換柱的。
// https://github.com/reduxjs/redux/blob/v4.0.4/src/applyMiddleware.js
export default function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = () => {
throw new Error(
'Dispatching while constructing your middleware is not allowed. ' +
'Other middleware would not be applied to this dispatch.'
)
}
const middlewareAPI = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}
可以看到,applyMiddleware接收的所有中間件使用map去了currying最外面的一層,這里的middlewareAPI即簡易版的store,它保證每個中間件都能拿到當(dāng)前的同一個store,拿到的chain是[next => action => {}, ...]這樣一個數(shù)組。
而后,使用compose(函數(shù)組合),將以上得到的chain串起來:
export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
}
if (funcs.length === 1) {
return funcs[0]
}
return funcs.reduce((a, b) => (...args) => a(b(...args)))
}
簡單明了,compose的能力就是將[a, b, c]組合成(...args) => a(b(c(...args)))
回到上面,將中間件鏈組合后,再接收store.dispatch(可以理解,這里就是我們需要的next),增強后的dispatch即
dispatch = middleware1(middleware2(middleware3(store.dispatch)))
結(jié)合我們中間件的范式:next => action => next(action),store.dispatch作為middleware3的next,...,middleware2(middleware3(store.dispatch))作為middleware1的next,豁然開朗,就這樣dispatch得到了升華,不過如此♂?。
(你看看,你看看,核心代碼,就這短短幾行,卻韻味十足,還有天理嗎?心動了嗎?心動了還不打開gayhub操作起來?)
當(dāng)然講到這里,如果對React生態(tài)有些許了解的同學(xué)可能會說,“React里面不是有種概念叫 Context,而且隨著版本迭代,功能越來越強大,我可以不用Redux嗎???”
Context
React文檔官網(wǎng)并未對Context給出明確定義,更多是描述使用場景,以及如何使用Context。
In some cases, you want to pass data through the component tree without having to pass the props down manuallys at every level. you can do this directly in React with the powerful ‘context’ API.
簡單說就是,當(dāng)你不想在組件樹中通過逐層傳遞props或者state的方式來傳遞數(shù)據(jù)時,可以使用Context api來實現(xiàn)跨層級的組件數(shù)據(jù)傳遞。
import { createContext } from "react";
export const CounterContext = createContext(null);
我們聲明一個CounterContext簡單講解使用方法,ceateContext接收默認(rèn)值。
Provider
包裹目標(biāo)組件,聲明
value作為share state
import React, { useState } from "react"
import { CounterContext } from "./context"
import App from "./App"
const Main = () => {
const [counter, setCounter] = useState(0)
return (
<CounterContext.Provider
value={{
counter,
add: () => setCounter(counter + 1),
dec: () => setCounter(counter - 1)
}}
>
<App />
</CounterContext.Provider>
)
}
如上,在App外層包裹Provider,并提供了counter的一些運算。
Comsumer
消費
Provider提供的value
import React, { useContext } from "react";
import { CounterContext } from "./context";
import "./styles.css";
export default function App(props) {
let state = useContext(CounterContext);
return (
<>
...
</>
)
}
(以上使用了Context的hooks新寫法,注意確定您的React版本>=16.8后再做以上嘗試)
App的任意子孫組件都可以隨地使用useContext取到Prodider上的值。
以上就是Context的全部內(nèi)容了,我們老規(guī)矩,簡單看個Counter后于Redux做個比較。
Context vs Redux
其實吧,這二者沒太多可比較的。
Context api可以說是簡化版的Redux,它不能結(jié)合強大的middleware擴展自己的超能力,比如redux-thunk或redux-saga等做復(fù)雜的異步任務(wù),也沒有完善的開發(fā)/定位能力,不過如果你只是想找個地方存share data來避開惡心的props drilling的問題,那么Context api的確值得你為他拍手叫好。
react-redux
Redux作為數(shù)據(jù)層,出色地完成了所有數(shù)據(jù)層面的事物,而React作為一個UI框架,給我一個state我就能給你一個UI view,現(xiàn)在的關(guān)鍵在于需要將Redux中state的更新通知到React,讓其及時更新UI。
于是React團(tuán)隊出手了,他們動手給React做了適配,它的產(chǎn)物就是react-redux。
Provider
包裹目標(biāo)組件,接收
store作為share state
import React from 'react'
import ReactDOM from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './pages'
import reducers from './reducers'
const store = createStore(reducers)
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
以上就是一個標(biāo)準(zhǔn)的React項目入口,Provider接收Redux提供的唯一store。
connect
連接
component與store,賦予component使用state與dispatch action的能力
import { connect } from "react-redux"
const mapStateToProps = (state) => ({
counter: state.counter
});
const mapDispatchToProps = {
add: () => ({ type: 'INCREMENT' }),
dec: () => ({ type: 'DECREMENT' })
};
export default connect(mapStateToProps, mapDispatchToProps)(App)
以上代碼片段,
-
mapStateToProps接收state,獲取component想要的值 -
mapDispatchToProps聲明了一些action creator,并由connect提供dispatch能力,賦予component派發(fā)action的能力 - 它還接收
mergeProps和options等自定義參數(shù)
老規(guī)矩,我們來看看基于react-redux實現(xiàn)的Counter。
Redux痛點
回顧一下,我們在使用Redux的實例時,分析其痛點,是什么?
對(雖然沒人回答,但是我從你們心里聽到了)
“ 組件需要主動訂閱store的更新 ”
react-redux的demo與之相比,比較直觀的感受就是:不再是哪里需要就哪里subscribe,而只需要connect。
那斗膽問一句:“以現(xiàn)有的知識,結(jié)合剛剛分析的用法,你會怎么實現(xiàn)react-redux?”
源碼分析
沒錯,必然是Context api啊,一起簡單看看源碼驗證下猜想。
搜索整個項目,我們只用到react-redux提供的唯一兩個api,我們可以很快從入口處找到他們的蹤跡。
Provider
react-redux汲取了Context api的的精華 才得以實現(xiàn)在app的每個角落都能拿到store的state
import React, { useMemo, useEffect } from 'react'
import { ReactReduxContext } from './Context'
// 對store.subscribe的抽象
import Subscription from '../utils/Subscription'
function Provider({ store, context, children }) {
const contextValue = useMemo(() => {
const subscription = new Subscription(store)
subscription.onStateChange = subscription.notifyNestedSubs
return {
store,
subscription,
}
}, [store])
// 使用userMemo緩存數(shù)據(jù),避免多余的re-render
const previousState = useMemo(() => store.getState(), [store])
// 當(dāng)contectValue, previousState變化時,通知訂閱者作出響應(yīng)
useEffect(() => {
const { subscription } = contextValue
subscription.trySubscribe()
if (previousState !== store.getState()) {
subscription.notifyNestedSubs()
}
return () => {
subscription.tryUnsubscribe()
subscription.onStateChange = null
}
}, [contextValue, previousState])
// context nested
const Context = context || ReactReduxContext
return <Context.Provider value={contextValue}>{children}</Context.Provider>
}
拋開復(fù)雜的nested context與re-render的優(yōu)化處理,Provider無非就是將接受的store通過Context api傳遞到每個組件。
connect
首先,我們明確一點:connect的目的是從store取得想要的props給到component。
所以我們知道只要從provider上拿到store,然后在connect中使用一個組件在mounted時添加對指定值的subscribe,此后它的更新都會引起被connected的后代組件的re-render,就達(dá)到目的了。
以上分析其實就是connect的實現(xiàn)原理,但是我們知道在React中,props變化的成本很高,它的每次變更都將一起所有后代組件跟隨著它re-render,所以以下絕大部分代碼都是為了優(yōu)化這一巨大的re-render開銷。
export function createConnect({
connectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory,
} = {}) {
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
...extraOptions
} = {}
) {
const initMapStateToProps = match(
mapStateToProps,
mapStateToPropsFactories,
'mapStateToProps'
)
const initMapDispatchToProps = match(
mapDispatchToProps,
mapDispatchToPropsFactories,
'mapDispatchToProps'
)
const initMergeProps = match(mergeProps, mergePropsFactories, 'mergeProps')
return connectHOC(selectorFactory, {
// used in error messages
methodName: 'connect',
// used to compute Connect's displayName from the wrapped component's displayName.
getDisplayName: (name) => `Connect(${name})`,
// if mapStateToProps is falsy, the Connect component doesn't subscribe to store state changes
shouldHandleStateChanges: Boolean(mapStateToProps),
// passed through to selectorFactory
initMapStateToProps,
initMapDispatchToProps,
initMergeProps,
pure,
areStatesEqual,
areOwnPropsEqual,
areStatePropsEqual,
areMergedPropsEqual,
// any extra options args can override defaults of connect or connectAdvanced
...extraOptions,
})
}
}
export default /*#__PURE__*/ createConnect()
好奇怪,默認(rèn)導(dǎo)出是createConnect的return func,它接受了一堆默認(rèn)參數(shù),為什么多此一舉?
(認(rèn)真看前面注釋,這些是為了方便更好地做testing case)
然后我們繼續(xù)看其內(nèi)部實現(xiàn),接受的四個來自用戶的參數(shù),然后使用match給前三個初始化了一下
match
很簡單,接受一個工廠函數(shù),以及每次需要初始化的key,從后往前遍歷工廠,任何一個response不為空,則返回(其實就是為了兼容用戶傳入的參數(shù),保證格式與去空)。
然后是connectHOC,這是處理核心,它接收了一個SelectorFactory。
SelectorFactory
根據(jù)傳入的option.pure(默認(rèn)true)的值來決定每次返回props是否要緩存,這樣將有效的減少不必要的計算,優(yōu)化性能。
connectHOC
export default function connectAdvanced(
/*
selectorFactory is a func that is responsible for returning the selector function used to
compute new props from state, props, and dispatch. For example:
export default connectAdvanced((dispatch, options) => (state, props) => ({
thing: state.things[props.thingId],
saveThing: fields => dispatch(actionCreators.saveThing(props.thingId, fields)),
}))(YourComponent)
Access to dispatch is provided to the factory so selectorFactories can bind actionCreators
outside of their selector as an optimization. Options passed to connectAdvanced are passed to
the selectorFactory, along with displayName and WrappedComponent, as the second argument.
Note that selectorFactory is responsible for all caching/memoization of inbound and outbound
props. Do not use connectAdvanced directly without memoizing results between calls to your
selector, otherwise the Connect component will re-render on every state or props change.
*/
selectorFactory,
// options object:
{
// the func used to compute this HOC's displayName from the wrapped component's displayName.
// probably overridden by wrapper functions such as connect()
getDisplayName = (name) => `ConnectAdvanced(${name})`,
// shown in error messages
// probably overridden by wrapper functions such as connect()
methodName = 'connectAdvanced',
// REMOVED: if defined, the name of the property passed to the wrapped element indicating the number of
// calls to render. useful for watching in react devtools for unnecessary re-renders.
renderCountProp = undefined,
// determines whether this HOC subscribes to store changes
shouldHandleStateChanges = true,
// REMOVED: the key of props/context to get the store
storeKey = 'store',
// REMOVED: expose the wrapped component via refs
withRef = false,
forwardRef = false,
// the context consumer to use
context = ReactReduxContext,
// additional options are passed through to the selectorFactory
...connectOptions
} = {}
) {
if (process.env.NODE_ENV !== 'production') {
if (renderCountProp !== undefined) {
throw new Error(
`renderCountProp is removed. render counting is built into the latest React Dev Tools profiling extension`
)
}
if (withRef) {
throw new Error(
'withRef is removed. To access the wrapped instance, use a ref on the connected component'
)
}
const customStoreWarningMessage =
'To use a custom Redux store for specific components, create a custom React context with ' +
"React.createContext(), and pass the context object to React Redux's Provider and specific components" +
' like: <Provider context={MyContext}><ConnectedComponent context={MyContext} /></Provider>. ' +
'You may also pass a {context : MyContext} option to connect'
if (storeKey !== 'store') {
throw new Error(
'storeKey has been removed and does not do anything. ' +
customStoreWarningMessage
)
}
}
const Context = context
return function wrapWithConnect(WrappedComponent) {
if (
process.env.NODE_ENV !== 'production' &&
!isValidElementType(WrappedComponent)
) {
throw new Error(
`You must pass a component to the function returned by ` +
`${methodName}. Instead received ${stringifyComponent(
WrappedComponent
)}`
)
}
const wrappedComponentName =
WrappedComponent.displayName || WrappedComponent.name || 'Component'
const displayName = getDisplayName(wrappedComponentName)
const selectorFactoryOptions = {
...connectOptions,
getDisplayName,
methodName,
renderCountProp,
shouldHandleStateChanges,
storeKey,
displayName,
wrappedComponentName,
WrappedComponent,
}
const { pure } = connectOptions
function createChildSelector(store) {
return selectorFactory(store.dispatch, selectorFactoryOptions)
}
// If we aren't running in "pure" mode, we don't want to memoize values.
// To avoid conditionally calling hooks, we fall back to a tiny wrapper
// that just executes the given callback immediately.
const usePureOnlyMemo = pure ? useMemo : (callback) => callback()
function ConnectFunction(props) {
const [
propsContext,
reactReduxForwardedRef,
wrapperProps,
] = useMemo(() => {
// Distinguish between actual "data" props that were passed to the wrapper component,
// and values needed to control behavior (forwarded refs, alternate context instances).
// To maintain the wrapperProps object reference, memoize this destructuring.
const { reactReduxForwardedRef, ...wrapperProps } = props
return [props.context, reactReduxForwardedRef, wrapperProps]
}, [props])
const ContextToUse = useMemo(() => {
// Users may optionally pass in a custom context instance to use instead of our ReactReduxContext.
// Memoize the check that determines which context instance we should use.
return propsContext &&
propsContext.Consumer &&
isContextConsumer(<propsContext.Consumer />)
? propsContext
: Context
}, [propsContext, Context])
// Retrieve the store and ancestor subscription via context, if available
const contextValue = useContext(ContextToUse)
// The store _must_ exist as either a prop or in context.
// We'll check to see if it _looks_ like a Redux store first.
// This allows us to pass through a `store` prop that is just a plain value.
const didStoreComeFromProps =
Boolean(props.store) &&
Boolean(props.store.getState) &&
Boolean(props.store.dispatch)
const didStoreComeFromContext =
Boolean(contextValue) && Boolean(contextValue.store)
if (
process.env.NODE_ENV !== 'production' &&
!didStoreComeFromProps &&
!didStoreComeFromContext
) {
throw new Error(
`Could not find "store" in the context of ` +
`"${displayName}". Either wrap the root component in a <Provider>, ` +
`or pass a custom React context provider to <Provider> and the corresponding ` +
`React context consumer to ${displayName} in connect options.`
)
}
// Based on the previous check, one of these must be true
const store = didStoreComeFromProps ? props.store : contextValue.store
const childPropsSelector = useMemo(() => {
// The child props selector needs the store reference as an input.
// Re-create this selector whenever the store changes.
return createChildSelector(store)
}, [store])
const [subscription, notifyNestedSubs] = useMemo(() => {
if (!shouldHandleStateChanges) return NO_SUBSCRIPTION_ARRAY
// This Subscription's source should match where store came from: props vs. context. A component
// connected to the store via props shouldn't use subscription from context, or vice versa.
const subscription = new Subscription(
store,
didStoreComeFromProps ? null : contextValue.subscription
)
// `notifyNestedSubs` is duplicated to handle the case where the component is unmounted in
// the middle of the notification loop, where `subscription` will then be null. This can
// probably be avoided if Subscription's listeners logic is changed to not call listeners
// that have been unsubscribed in the middle of the notification loop.
const notifyNestedSubs = subscription.notifyNestedSubs.bind(
subscription
)
return [subscription, notifyNestedSubs]
}, [store, didStoreComeFromProps, contextValue])
// Determine what {store, subscription} value should be put into nested context, if necessary,
// and memoize that value to avoid unnecessary context updates.
const overriddenContextValue = useMemo(() => {
if (didStoreComeFromProps) {
// This component is directly subscribed to a store from props.
// We don't want descendants reading from this store - pass down whatever
// the existing context value is from the nearest connected ancestor.
return contextValue
}
// Otherwise, put this component's subscription instance into context, so that
// connected descendants won't update until after this component is done
return {
...contextValue,
subscription,
}
}, [didStoreComeFromProps, contextValue, subscription])
// We need to force this wrapper component to re-render whenever a Redux store update
// causes a change to the calculated child component props (or we caught an error in mapState)
const [
[previousStateUpdateResult],
forceComponentUpdateDispatch,
] = useReducer(storeStateUpdatesReducer, EMPTY_ARRAY, initStateUpdates)
// Propagate any mapState/mapDispatch errors upwards
if (previousStateUpdateResult && previousStateUpdateResult.error) {
throw previousStateUpdateResult.error
}
// Set up refs to coordinate values between the subscription effect and the render logic
const lastChildProps = useRef()
const lastWrapperProps = useRef(wrapperProps)
const childPropsFromStoreUpdate = useRef()
const renderIsScheduled = useRef(false)
const actualChildProps = usePureOnlyMemo(() => {
// Tricky logic here:
// - This render may have been triggered by a Redux store update that produced new child props
// - However, we may have gotten new wrapper props after that
// If we have new child props, and the same wrapper props, we know we should use the new child props as-is.
// But, if we have new wrapper props, those might change the child props, so we have to recalculate things.
// So, we'll use the child props from store update only if the wrapper props are the same as last time.
if (
childPropsFromStoreUpdate.current &&
wrapperProps === lastWrapperProps.current
) {
return childPropsFromStoreUpdate.current
}
// TODO We're reading the store directly in render() here. Bad idea?
// This will likely cause Bad Things (TM) to happen in Concurrent Mode.
// Note that we do this because on renders _not_ caused by store updates, we need the latest store state
// to determine what the child props should be.
return childPropsSelector(store.getState(), wrapperProps)
}, [store, previousStateUpdateResult, wrapperProps])
// We need this to execute synchronously every time we re-render. However, React warns
// about useLayoutEffect in SSR, so we try to detect environment and fall back to
// just useEffect instead to avoid the warning, since neither will run anyway.
useIsomorphicLayoutEffectWithArgs(captureWrapperProps, [
lastWrapperProps,
lastChildProps,
renderIsScheduled,
wrapperProps,
actualChildProps,
childPropsFromStoreUpdate,
notifyNestedSubs,
])
// Our re-subscribe logic only runs when the store/subscription setup changes
useIsomorphicLayoutEffectWithArgs(
subscribeUpdates,
[
shouldHandleStateChanges,
store,
subscription,
childPropsSelector,
lastWrapperProps,
lastChildProps,
renderIsScheduled,
childPropsFromStoreUpdate,
notifyNestedSubs,
forceComponentUpdateDispatch,
],
[store, subscription, childPropsSelector]
)
// Now that all that's done, we can finally try to actually render the child component.
// We memoize the elements for the rendered child component as an optimization.
const renderedWrappedComponent = useMemo(
() => (
<WrappedComponent
{...actualChildProps}
ref={reactReduxForwardedRef}
/>
),
[reactReduxForwardedRef, WrappedComponent, actualChildProps]
)
// If React sees the exact same element reference as last time, it bails out of re-rendering
// that child, same as if it was wrapped in React.memo() or returned false from shouldComponentUpdate.
const renderedChild = useMemo(() => {
if (shouldHandleStateChanges) {
// If this component is subscribed to store updates, we need to pass its own
// subscription instance down to our descendants. That means rendering the same
// Context instance, and putting a different value into the context.
return (
<ContextToUse.Provider value={overriddenContextValue}>
{renderedWrappedComponent}
</ContextToUse.Provider>
)
}
return renderedWrappedComponent
}, [ContextToUse, renderedWrappedComponent, overriddenContextValue])
return renderedChild
}
// If we're in "pure" mode, ensure our wrapper component only re-renders when incoming props have changed.
const Connect = pure ? React.memo(ConnectFunction) : ConnectFunction
Connect.WrappedComponent = WrappedComponent
Connect.displayName = displayName
if (forwardRef) {
const forwarded = React.forwardRef(function forwardConnectRef(
props,
ref
) {
return <Connect {...props} reactReduxForwardedRef={ref} />
})
forwarded.displayName = displayName
forwarded.WrappedComponent = WrappedComponent
return hoistStatics(forwarded, WrappedComponent)
}
return hoistStatics(Connect, WrappedComponent)
}
}
內(nèi)容很多很多很多,使用了hooks的語法,看起來更加復(fù)雜,不過沒關(guān)系,按老規(guī)矩我們從底往上看。
可以看到最終return的是hoistStatics(Connect, WrappedComponent),這個方法是把WrappedComponent掛的靜態(tài)方法屬性拷貝到結(jié)果組件上,于是我們?nèi)フ?code>Connect。
往上幾行看到connect根據(jù)pure做了一層react.memo來包裹ConnectFunction,我們知道這是為了阻止props引起的不必要的re-render。
再來看ConnectFunction,這是一個關(guān)鍵函數(shù),return了renderedChild,而renderedChild用memo包裹了renderedWrappedComponent, 而它接收了actualChildProps,看其定義就是我們需要的mapStateToprops返回的結(jié)果了。
ok,現(xiàn)在我們知道了這個HOC的渲染邏輯,那么它是如何做到store更新就重新計算然后觸發(fā)re-render呢?
分析一波:組件要想re-render,那必須是props或state其一,那這里只能是state了。
好家伙,我們看到了useReducer,看到了forceComponentUpdateDispatch,這變量名一聽就有戲。
checkForUpdates中通過newChildProps === lastChildProps.current的比對,如果前后兩次子props相同,說明props沒變,那就不更新,否則通過dispatch,修改state,強行觸發(fā)組件更新,成!
那么問題來了,checkForUpdates是何方神圣,它又怎么感知到store更新呢?
原來我們剛一開始漏掉了一個狠角色,useIsomorphicLayoutEffectWithArgs。這家伙是兼容ssr版本的useLayoutEffect,在組件每次更新后執(zhí)行,我們看到組件渲染進(jìn)來,然后里面通過subscription.trySubscribe進(jìn)行了訂閱以及onStatechnage綁定了checkforUpdate ,所以每次store有變化這里的subscription 都會觸發(fā)checkforupdate。
就這么簡單?。?!
Mobx
不得不注意到,除了Redux,社區(qū)里近年來還有另一產(chǎn)品呼聲很高,那就是Mobx。
它是一個功能強大,上手非常容易的狀態(tài)管理工具。就連Redux的作者也曾經(jīng)向大家推薦過它,在不少情況下你的確可以使用Mobx來替代掉Redux。
再次強調(diào)Flux、Redux與Mobx等并不與react強綁定,你可以在任何框架中使用他們,所以才會有react-redux,mobx-react等庫的必要性。

Mobx比較簡單,相信從Vue轉(zhuǎn)React的朋友應(yīng)該會很容易上手,它就三個基本要點:
創(chuàng)建可監(jiān)測的狀態(tài)
一般,我們使用observable來創(chuàng)建可被監(jiān)測的狀態(tài),它可以是對象,數(shù)組,類等等。
import { observable } from "mobx"
class Store {
@observable counter = 0
}
const store = new Store()
創(chuàng)建視圖響應(yīng)狀態(tài)變更
state創(chuàng)建后,如果是開發(fā)應(yīng)用我們需要有視圖來讓感知變更,MobX會以一種最小限度的方式來更新視圖,并且它有著令人匪夷所思的高效。
以下我們以react class component為例。
import React from 'react'
import {observer} from 'mobx-react'
@observer
class Counter extends React.Component {
render() {
return (
<div>
<div>{this.props.state.counter}</div>
<button onClick={this.props.store.add}>Add</button>
<button onClick={this.props.store.dec}>Dec</button>
<button onClick={() => (this.props.store.counter = 0)}>clear</button>
</div>
)
}
}
export default Counter
觸發(fā)狀態(tài)變更
修改第一節(jié)中創(chuàng)建監(jiān)測狀態(tài)的代碼
import { observable, action } from "mobx"
class Store {
@observable counter = 0
@action add = () => {
this.counter++
}
@action dec = () => {
this.counter--
}
}
const store = new Store()
結(jié)合上節(jié)視圖,add、dec兩算法都是通過調(diào)用store提供的方法,合情合理。
可怕的是,clear直接就給state的counter賦值,居然也能成功,而且視圖是及時更新,不禁回想起flux章節(jié)中的clear,恐懼更甚,讓人望而退步。
其實大可不必,這就是mobx的魔力,其實跟vue一般,它也是通過Proxy注冊監(jiān)聽,實現(xiàn)動態(tài)及時響應(yīng)。
為了滿足React用戶對于這種狀態(tài)不可控的恐懼,它也提供了api來限制這種操作,必須通過action來修改store。
enforceAction
規(guī)定只有action才能改store。
import { configure } from 'mobx'
configure({enforceAction: true})
provider
當(dāng)然,為了幫助開發(fā)者更合理的制定目錄結(jié)構(gòu)與開發(fā)規(guī)范,它也提供了同react-redux相似的Provider,后代組件使用inject,接收來自Provider注入的狀態(tài),再使用observer連接react組件和 mobx狀態(tài),達(dá)到實時相應(yīng)狀態(tài)變化的效果。
還有一些比如autorun,reaction,when computed等能力能在狀態(tài)滿足特定條件自動被觸發(fā),有興趣的可以自行做更多了解。
老規(guī)矩,通過一個Counter來看看效果。
Mobx vs Redux
通過上面簡單的介紹以及demo的體驗,相信你也有了大致的感受,我們再簡單的比對下它與Redux。
無拘無束,這既是Mobx的優(yōu)點也是它的缺點,當(dāng)項目規(guī)模較大,涉及到多人開發(fā)時,這種不加管束的自由將是"災(zāi)難"的開始。
咳,點到即可,懂的都懂。
(有疏漏或偏頗的地方感謝指正?。。。?/p>