文中涉及的React demo
代碼都使用了16.8的新增特性Hooks:
它可以讓你在不編寫(xiě)
class
的情況下使用state
以及其他的React
特性晃跺。
前言
剛立項(xiàng)時(shí)揩局,你的所有代碼可能就只有一個(gè)根組件Root
—— 擼起袖子就是干!
項(xiàng)目慢慢有了起色掀虎,一些哥們就拆分了一些子組件凌盯,必然付枫,它們間將有一些數(shù)據(jù)流動(dòng) —— 問(wèn)題不大,可以讓它們緊密聯(lián)系驰怎。
現(xiàn)在項(xiàng)目進(jìn)展火爆阐滩,業(yè)務(wù)N
倍增長(zhǎng),不得不拆出更多的子孫組件出來(lái)县忌,實(shí)現(xiàn)更多復(fù)雜業(yè)務(wù) —— 但愿邏輯比較簡(jiǎn)單掂榔,數(shù)據(jù)流動(dòng)是一層層往下
不過(guò),現(xiàn)實(shí)總是很殘酷症杏,父子孫組件間關(guān)系往往混亂無(wú)比装获。
怎么辦,怎么辦厉颤?穴豫??
只要思想不滑坡逼友,辦法總比困難多
- 方案1精肃,梳理項(xiàng)目邏輯,重新設(shè)計(jì)組件??
- 方案2帜乞,辭職司抱,換個(gè)公司重開(kāi)???
確實(shí),項(xiàng)目迭代過(guò)程中,不可避免地就會(huì)出現(xiàn)組件間狀態(tài)共享,而導(dǎo)致邏輯交錯(cuò),難以控制。
那我們就會(huì)想:"能不能有一種實(shí)踐規(guī)范茂蚓,將所有可能公用的狀態(tài)、數(shù)據(jù)及能力提取到組件外雅宾,數(shù)據(jù)流自上往下唆迁,哪里需要哪里自己獲取,而不是prop drilling
"后频,大概長(zhǎng)這樣:
于是這樣一種數(shù)據(jù)結(jié)構(gòu)冒了出來(lái):
const store = {
state: {
text: 'Goodbye World!'
},
setAction (text) {
this.text = text
},
clearAction () {
this.text = ''
}
}
存在外部變量store
:
-
state
來(lái)存儲(chǔ)數(shù)據(jù) - 有一堆功能各異的
action
來(lái)控制state
的改變
再加上強(qiáng)制約束:只能通過(guò)調(diào)用action
來(lái)改變state
梳庆,然后我們就可以通過(guò)action
清晰地掌握著state
的動(dòng)向,那么日志卑惜、監(jiān)控膏执、回滾等能力還有啥擔(dān)心的。
其實(shí)露久,這就是Flux
的早早期雛形更米。
Flux
2013年,F(xiàn)acebook亮出React的時(shí)候毫痕,也跟著帶出的Flux征峦。Facebook認(rèn)為兩者相輔相成迟几,結(jié)合在一起才能構(gòu)建大型的JavaScript應(yīng)用。
做一個(gè)容易理解的對(duì)比栏笆,React
是用來(lái)替換jQuery
的类腮,那么Flux
就是以替換Backbone.js
、Ember.js
等MVC
一族框架為目的蛉加。
如上圖蚜枢,數(shù)據(jù)總是“單向流動(dòng)”,相鄰部分不存在互相流動(dòng)數(shù)據(jù)的現(xiàn)象针饥,這也是Flux
一大特點(diǎn)厂抽。
-
View
發(fā)起用戶的Action
-
Dispatcher
作為調(diào)度中心,接收Action
打厘,要求Store
進(jìn)行相應(yīng)更新 -
Store
處理主要邏輯修肠,并提供監(jiān)聽(tīng)能力,當(dāng)數(shù)據(jù)更新后觸發(fā)監(jiān)聽(tīng)事件 -
View
監(jiān)聽(tīng)到Store
的更新事件后觸發(fā)UI
更新
感興趣可以看看每個(gè)部分的具體含義:
Action
plain javascript object户盯,一般使用
type
與payload
描述了該action的具體含義嵌施。
在Flux
中一般定義actions
:一組包含派發(fā)action
對(duì)象的函數(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
硫眨,通過(guò)Flux
提供的Dispatcher
注冊(cè)唯一實(shí)例足淆。
Dispatcher.register
方法用來(lái)登記各種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
動(dòng)作礁阁,就會(huì)執(zhí)行回調(diào)函數(shù)巧号,對(duì)CounterStore
進(jìn)行操作。
Dispatcher只用來(lái)派發(fā)Action姥闭,不應(yīng)該有其他邏輯丹鸿。
Store
應(yīng)用狀態(tài)的處理中心。
Store
中復(fù)雜處理業(yè)務(wù)邏輯棚品,而由于數(shù)據(jù)變更后View
需要更新靠欢,所以它也負(fù)責(zé)提供通知視圖更新的能力。
因?yàn)槠潆S用隨注冊(cè)铜跑,一個(gè)應(yīng)用可以注冊(cè)多個(gè)Store
的能力门怪,更新Data Dlow為
細(xì)心的朋友可以發(fā)現(xiàn)在上一小節(jié)CounterStore
中調(diào)用了emitChange
的方法 —— 對(duì),它就是用來(lái)通知變更的锅纺。
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
通過(guò)繼承EventEmitter.prototype
獲得觸發(fā)emit
與監(jiān)聽(tīng)on
事件能力。
View
Store
中的數(shù)據(jù)的視圖展示
View
需要監(jiān)聽(tīng)視圖中數(shù)據(jù)的變動(dòng)來(lái)保證視圖實(shí)時(shí)更新,即
- 在組件中需要添加
addChangeListerner
- 在組件銷毀時(shí)移除監(jiān)聽(tīng)
removeChangeListener
我們看個(gè)簡(jiǎn)單的Couter例子拣帽,更好的理解下實(shí)際使用疼电。
(手動(dòng)分割)
認(rèn)真體驗(yàn)的朋友可能會(huì)注意到:
- 點(diǎn)擊
reset
后,store
中的couter
被更新(沒(méi)有emitChange
所以沒(méi)實(shí)時(shí)更新視圖)减拭; - 業(yè)務(wù)邏輯與數(shù)據(jù)處理邏輯交錯(cuò)蔽豺,代碼組織混亂;
好拧粪,打住修陡,再看個(gè)新的數(shù)據(jù)流。
Redux
- 用戶與
View
進(jìn)行交互 - 通過(guò)
Action Creator
派發(fā)action
- 到達(dá)
Store
后拿到當(dāng)前的State
可霎,一并交給Reducer
-
Reducer
經(jīng)過(guò)處理后返回全新的State
給Store
-
Store
更新后通知View
魄鸦,完成一次數(shù)據(jù)更新
Flux
的基本原則是“單向數(shù)據(jù)流”,Redux在此基礎(chǔ)上強(qiáng)調(diào):
- 唯一數(shù)據(jù)源(Single Source of Truth):整個(gè)應(yīng)用只保持一個(gè)
Store
癣朗,所有組件的數(shù)據(jù)源就是該Store
的狀態(tài)拾因。 - 保持狀態(tài)只讀(State is read-only):不直接修改狀態(tài),要修改
Store
的狀態(tài)旷余,必須要通過(guò)派發(fā)一個(gè)action
對(duì)象完成绢记。 - 數(shù)據(jù)改變只能通過(guò)純函數(shù)完成(Changes are made with pure funtions):這里所說(shuō)的純函數(shù)指
reducer
。
感興趣可以看看每個(gè)部分的具體含義:
(Redux的源碼及其短小優(yōu)雅正卧,有想嘗試閱讀源碼的朋友可以從它開(kāi)始)
Store
應(yīng)用唯一的數(shù)據(jù)存儲(chǔ)中心
import { createStore } from 'redux'
const store = createStore(fn)
以上代碼蠢熄,使用redux
提供的createStore
函數(shù),接受另一個(gè)函數(shù)fn
(即稍后提到的Reducers
)作為參數(shù)炉旷,生成應(yīng)用唯一的store
签孔。
可以看看簡(jiǎn)單實(shí)現(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 }
}
本人看源碼有個(gè)小技巧,一般先從導(dǎo)出找起窘行,再看return
饥追。
如上,return出去三個(gè)能力:
-
getState
: 獲取state的唯一方法罐盔,state被稱為store的快照 -
dispatch
: view派發(fā)action的唯一方法 -
subscribe
: 注冊(cè)監(jiān)聽(tīng)函數(shù)(核心但绕,待會(huì)要考),返回解除監(jiān)聽(tīng)
注意到以上代碼片段最后翘骂,dispatch
了一個(gè)空對(duì)象壁熄,是為了生成初始的state
帚豪,學(xué)習(xí)了reducer
的寫(xiě)法后可以解釋原理碳竟。
當(dāng)然,createStore還可以接收更多的參數(shù)狸臣,如:preloadedState
(默認(rèn)state
)莹桅,enhancer
(store
的超能力蘑菇)等,我們后面會(huì)分析到。
Action
plain javascript object诈泼,一般使用
type
與payload
描述了該action的具體含義懂拾。
在redux
,type
屬性是必須的铐达,表示Action
的名稱岖赋,其他屬性可以自由設(shè)置,參照規(guī)范瓮孙。
const actions = {
type: 'ADD_TODO',
payload: 'Learn Redux'
}
可以用Action Creator
批量來(lái)生成一些Action
唐断,如下addTodo
就是一個(gè)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ù)簽名,接收來(lái)自view
的action
偏灿,并從store
上拿到最新state
丹诀,經(jīng)過(guò)處理后返回一個(gè)全新的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é)果一定要是一個(gè)全新的state
,尤其是涉及到引用數(shù)據(jù)類型的操作時(shí)沮峡,因?yàn)?code>react對(duì)數(shù)據(jù)更新的判斷都是淺比較疚脐,如果更新前后是同一個(gè)引用,那么react
將會(huì)忽略這一次更新邢疙。
理想狀態(tài)state
結(jié)構(gòu)層級(jí)可能比較簡(jiǎn)單棍弄,那么如果state
樹(shù)枝葉后代比較復(fù)雜時(shí)怎么辦(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
}
}
先不討論以上寫(xiě)法風(fēng)險(xiǎn)如何疟游,就這一層層看著都吐呼畸。
既然這樣,我們?cè)傧胂朕k法颁虐。
前面提到蛮原,Redux
中store
唯一,所以我們只要能保證在reducer
中返回的state
是一個(gè)完整的結(jié)構(gòu)就行另绩,那是不是可以:
const reducers = (state = {}, action) => {
return {
A: reducer1(state.A, action),
B: reducer2(state.B, action),
C: reducer3(state.C, action)
}
}
以上儒陨,我們曲線救國(guó),將復(fù)雜的數(shù)據(jù)結(jié)構(gòu)拆分笋籽,每個(gè)reducer
管理state
樹(shù)不同枝干蹦漠,最后再將所有reducer
合并后給createStore
,這正是combineReducer
的設(shè)計(jì)思路车海。
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é)果合并成一個(gè)大的state
對(duì)象。
可以看下簡(jiǎn)單實(shí)現(xiàn):
const combineReducers = reducers => (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key] = reducers[key](state[key], action)
return nextState
}, {})
}
以上介紹了Redux
的基本能力研铆,再看個(gè)Demo加深加深印象埋同。
(再次手動(dòng)分割)
可以注意到一個(gè)痛點(diǎn):
-
component
得主動(dòng)去訂閱store.subscribe``state
的變更,讓代碼顯得很蠢棵红,不太“雅”凶赁。
Flux vs Redux
好,redux
的基本面都覆蓋了逆甜,它是基于Flux
的核心思想實(shí)現(xiàn)的一套解決方案哟冬,從以上分析我們可以感受到區(qū)別:
以上,從store
與dispatcher
兩個(gè)本質(zhì)區(qū)別比對(duì)了二者忆绰,相信你們英文一定比我好浩峡,就不翻譯了。
(不要問(wèn)我為什么要麻將牌+英文排列错敢,問(wèn)就是“中西合璧”)
Redux
和Flux
類似翰灾,只是一種思想或者規(guī)范,它和React
之間沒(méi)有關(guān)系稚茅。Redux
支持React
纸淮、Angular
、Ember
亚享、jQuery
甚至純JavaScript
咽块。
因?yàn)?code>React包含函數(shù)式的思想,也是單向數(shù)據(jù)流欺税,和Redux
很搭侈沪,所以一般都用Redux
來(lái)進(jìn)行狀態(tài)管理。
當(dāng)然晚凿,不是所有項(xiàng)目都無(wú)腦推薦redux
亭罪,Dan Abramov很早前也提到“You Might Not Need Redux”,只有遇到react
不好解決的問(wèn)題我們才考慮使用redux
歼秽,比如:
- 用戶的使用方式復(fù)雜
- 不同身份的用戶有不同的使用方式(比如普通用戶和管理員)
- 多個(gè)用戶之間可以協(xié)作/與服務(wù)器大量交互应役,或者使用了WebSocket
- View要從多個(gè)來(lái)源獲取數(shù)據(jù)
- ...
(再再次手動(dòng)分割)
好,我們繼續(xù)來(lái)聊Redux
燥筷。
以上箩祥,我們處理的都是同步且邏輯簡(jiǎn)單的Redux
使用場(chǎng)景,真正的業(yè)務(wù)開(kāi)發(fā)場(chǎng)景遠(yuǎn)比這復(fù)雜肆氓,各種異步任務(wù)不可避免袍祖,這時(shí)候怎么辦?
一起跟著Redux
的Data Flow分析一下:
-
View
:state
的視覺(jué)層做院,與之一一對(duì)應(yīng)盲泛,不合適承擔(dān)其他功能; -
Action
:描述一個(gè)動(dòng)作的具體內(nèi)容键耕,只能被操作寺滚,自己不能進(jìn)行任何操作 -
Reducer
:純函數(shù),只承擔(dān)計(jì)算state
的功能屈雄,不合適承擔(dān)其他功能
看來(lái)如果想要在action
發(fā)出后做一些額外復(fù)雜的同步/異步操作村视,只有在派發(fā)action
,即dispatch
時(shí)可以做點(diǎn)手腳酒奶,我們稱負(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寫(xiě)法
const Logger = store => next => action => {
console.log('dispatching', action)
next(action)
console.log('next state', store.getState())
}
先補(bǔ)充個(gè)前置知識(shí),前面說(shuō)過(guò)createStore
可以接收除了reducers
之外更多的參數(shù)另伍,其中一個(gè)參數(shù)enhancer
就是表示你要注冊(cè)的中間件們鼻百,再看看createStore
怎么用它?
// https://github.com/reduxjs/redux/blob/v4.0.4/src/createStore.js#L53
...
enhancer(createStore)(reducer, preloadedState)
...
了解了以上代碼后摆尝,我們來(lái)看看redux
源碼是如何實(shí)現(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
即簡(jiǎn)易版的store
勺爱,它保證每個(gè)中間件都能拿到當(dāng)前的同一個(gè)store
,拿到的chain
是[next => action => {}, ...]
這樣一個(gè)數(shù)組讯检。
而后琐鲁,使用compose
(函數(shù)組合),將以上得到的chain
串起來(lái):
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)))
}
簡(jiǎn)單明了人灼,compose
的能力就是將[a, b, c]
組合成(...args) => a(b(c(...args)))
回到上面绣否,將中間件鏈組合后,再接收store.dispatch
(可以理解挡毅,這里就是我們需要的next
)蒜撮,增強(qiáng)后的dispatch
即
dispatch = middleware1(middleware2(middleware3(store.dispatch)))
結(jié)合我們中間件的范式:next => action => next(action)
,store.dispatch
作為middleware3
的next
跪呈,...段磨,middleware2(middleware3(store.dispatch))作為middleware1
的next
,豁然開(kāi)朗耗绿,就這樣dispatch
得到了升華苹支,不過(guò)如此♂?。
(你看看误阻,你看看债蜜,核心代碼晴埂,就這短短幾行,卻韻味十足寻定,還有天理嗎儒洛?心動(dòng)了嗎?心動(dòng)了還不打開(kāi)gayhub
操作起來(lái)?)
當(dāng)然講到這里,如果對(duì)React
生態(tài)有些許了解的同學(xué)可能會(huì)說(shuō)咙俩,“React
里面不是有種概念叫 Context
,而且隨著版本迭代恼蓬,功能越來(lái)越強(qiáng)大,我可以不用Redux
嗎僵芹?处硬??”
Context
React
文檔官網(wǎng)并未對(duì)Context
給出明確定義拇派,更多是描述使用場(chǎng)景郁油,以及如何使用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.
簡(jiǎn)單說(shuō)就是攀痊,當(dāng)你不想在組件樹(shù)中通過(guò)逐層傳遞props
或者state
的方式來(lái)傳遞數(shù)據(jù)時(shí)桐腌,可以使用Context api
來(lái)實(shí)現(xiàn)跨層級(jí)的組件數(shù)據(jù)傳遞。
import { createContext } from "react";
export const CounterContext = createContext(null);
我們聲明一個(gè)CounterContext
簡(jiǎn)單講解使用方法苟径,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
的一些運(yùn)算。
Comsumer
消費(fèi)
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
新寫(xiě)法遭殉,注意確定您的React
版本>=16.8后再做以上嘗試)
App
的任意子孫組件都可以隨地使用useContext
取到Prodider
上的值石挂。
以上就是Context
的全部?jī)?nèi)容了,我們老規(guī)矩险污,簡(jiǎn)單看個(gè)Counter后于Redux
做個(gè)比較痹愚。
Context vs Redux
其實(shí)吧,這二者沒(méi)太多可比較的蛔糯。
Context api
可以說(shuō)是簡(jiǎn)化版的Redux
拯腮,它不能結(jié)合強(qiáng)大的middleware
擴(kuò)展自己的超能力,比如redux-thunk
或redux-saga
等做復(fù)雜的異步任務(wù)蚁飒,也沒(méi)有完善的開(kāi)發(fā)/定位能力动壤,不過(guò)如果你只是想找個(gè)地方存share data
來(lái)避開(kāi)惡心的props drilling
的問(wèn)題,那么Context api
的確值得你為他拍手叫好淮逻。
react-redux
Redux
作為數(shù)據(jù)層琼懊,出色地完成了所有數(shù)據(jù)層面的事物阁簸,而React
作為一個(gè)UI
框架,給我一個(gè)state
我就能給你一個(gè)UI view
哼丈,現(xiàn)在的關(guān)鍵在于需要將Redux
中state
的更新通知到React
启妹,讓其及時(shí)更新UI
。
于是React
團(tuán)隊(duì)出手了削祈,他們動(dòng)手給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')
)
以上就是一個(gè)標(biāo)準(zhǔn)的React
項(xiàng)目入口,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ī)矩碳却,我們來(lái)看看基于react-redux
實(shí)現(xiàn)的Counter队秩。
Redux痛點(diǎn)
回顧一下,我們?cè)谑褂?code>Redux的實(shí)例時(shí)昼浦,分析其痛點(diǎn)馍资,是什么?
對(duì)(雖然沒(méi)人回答关噪,但是我從你們心里聽(tīng)到了)
“ 組件需要主動(dòng)訂閱store
的更新 ”
react-redux
的demo
與之相比鸟蟹,比較直觀的感受就是:不再是哪里需要就哪里subscribe
,而只需要connect
使兔。
那斗膽問(wèn)一句:“以現(xiàn)有的知識(shí)建钥,結(jié)合剛剛分析的用法,你會(huì)怎么實(shí)現(xiàn)react-redux
虐沥?”
源碼分析
沒(méi)錯(cuò)熊经,必然是Context api
啊,一起簡(jiǎn)單看看源碼驗(yàn)證下猜想欲险。
搜索整個(gè)項(xiàng)目奈搜,我們只用到react-redux
提供的唯一兩個(gè)api
,我們可以很快從入口處找到他們的蹤跡盯荤。
Provider
react-redux
汲取了Context api
的的精華 才得以實(shí)現(xiàn)在app
的每個(gè)角落都能拿到store
的state
import React, { useMemo, useEffect } from 'react'
import { ReactReduxContext } from './Context'
// 對(duì)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變化時(shí),通知訂閱者作出響應(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>
}
拋開(kāi)復(fù)雜的nested context
與re-render
的優(yōu)化處理秋秤,Provider
無(wú)非就是將接受的store
通過(guò)Context api
傳遞到每個(gè)組件宏粤。
connect
首先脚翘,我們明確一點(diǎn):connect
的目的是從store
取得想要的props
給到component
。
所以我們知道只要從provider
上拿到store
绍哎,然后在connect
中使用一個(gè)組件在mounted
時(shí)添加對(duì)指定值的subscribe
来农,此后它的更新都會(huì)引起被connected
的后代組件的re-render
,就達(dá)到目的了崇堰。
以上分析其實(shí)就是connect
的實(shí)現(xiàn)原理沃于,但是我們知道在React
中,props
變化的成本很高海诲,它的每次變更都將一起所有后代組件跟隨著它re-render
繁莹,所以以下絕大部分代碼都是為了優(yōu)化這一巨大的re-render
開(kāi)銷。
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)部實(shí)現(xiàn),接受的四個(gè)來(lái)自用戶的參數(shù)拍嵌,然后使用match
給前三個(gè)初始化了一下
match
很簡(jiǎn)單遭赂,接受一個(gè)工廠函數(shù),以及每次需要初始化的key横辆,從后往前遍歷工廠嵌牺,任何一個(gè)response
不為空,則返回(其實(shí)就是為了兼容用戶傳入的參數(shù)龄糊,保證格式與去空)逆粹。
然后是connectHOC
,這是處理核心炫惩,它接收了一個(gè)SelectorFactory
僻弹。
SelectorFactory
根據(jù)傳入的option.pure
(默認(rèn)true
)的值來(lái)決定每次返回props
是否要緩存,這樣將有效的減少不必要的計(jì)算他嚷,優(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
的語(yǔ)法筋蓖,看起來(lái)更加復(fù)雜卸耘,不過(guò)沒(méi)關(guān)系,按老規(guī)矩我們從底往上看粘咖。
可以看到最終return
的是hoistStatics(Connect, WrappedComponent)
蚣抗,這個(gè)方法是把WrappedComponent
掛的靜態(tài)方法屬性拷貝到結(jié)果組件上,于是我們?nèi)フ?code>Connect瓮下。
往上幾行看到connect
根據(jù)pure
做了一層react.memo
來(lái)包裹ConnectFunction
翰铡,我們知道這是為了阻止props
引起的不必要的re-render
钝域。
再來(lái)看ConnectFunction
,這是一個(gè)關(guān)鍵函數(shù)锭魔,return
了renderedChild
例证,而renderedChild
用memo
包裹了renderedWrappedComponent
, 而它接收了actualChildProps
迷捧,看其定義就是我們需要的mapStateToprops
返回的結(jié)果了织咧。
ok,現(xiàn)在我們知道了這個(gè)HOC
的渲染邏輯漠秋,那么它是如何做到store
更新就重新計(jì)算然后觸發(fā)re-render
呢笙蒙?
分析一波:組件要想re-render
,那必須是props
或state
其一膛堤,那這里只能是state
了手趣。
好家伙晌该,我們看到了useReducer
肥荔,看到了forceComponentUpdateDispatch
,這變量名一聽(tīng)就有戲朝群。
checkForUpdates
中通過(guò)newChildProps === lastChildProps.current
的比對(duì)燕耿,如果前后兩次子props
相同,說(shuō)明props
沒(méi)變姜胖,那就不更新誉帅,否則通過(guò)dispatch
,修改state
右莱,強(qiáng)行觸發(fā)組件更新蚜锨,成!
那么問(wèn)題來(lái)了,checkForUpdates
是何方神圣慢蜓,它又怎么感知到store
更新呢亚再?
原來(lái)我們剛一開(kāi)始漏掉了一個(gè)狠角色,useIsomorphicLayoutEffectWithArgs
晨抡。這家伙是兼容ssr
版本的useLayoutEffect
氛悬,在組件每次更新后執(zhí)行,我們看到組件渲染進(jìn)來(lái)耘柱,然后里面通過(guò)subscription.trySubscribe
進(jìn)行了訂閱以及onStatechnage
綁定了checkforUpdate
如捅,所以每次store
有變化這里的subscription
都會(huì)觸發(fā)checkforupdate
。
就這么簡(jiǎn)單5骷濉>登病!
Mobx
不得不注意到士袄,除了Redux
烈涮,社區(qū)里近年來(lái)還有另一產(chǎn)品呼聲很高朴肺,那就是Mobx
。
它是一個(gè)功能強(qiáng)大坚洽,上手非常容易的狀態(tài)管理工具戈稿。就連Redux
的作者也曾經(jīng)向大家推薦過(guò)它,在不少情況下你的確可以使用Mobx
來(lái)替代掉Redux
讶舰。
再次強(qiáng)調(diào)Flux鞍盗、Redux與Mobx
等并不與react
強(qiáng)綁定,你可以在任何框架中使用他們跳昼,所以才會(huì)有react-redux
般甲,mobx-react
等庫(kù)的必要性。
Mobx
比較簡(jiǎn)單鹅颊,相信從Vue轉(zhuǎn)React的朋友應(yīng)該會(huì)很容易上手敷存,它就三個(gè)基本要點(diǎn):
創(chuàng)建可監(jiān)測(cè)的狀態(tài)
一般,我們使用observable
來(lái)創(chuàng)建可被監(jiān)測(cè)的狀態(tài)堪伍,它可以是對(duì)象锚烦,數(shù)組,類等等帝雇。
import { observable } from "mobx"
class Store {
@observable counter = 0
}
const store = new Store()
創(chuàng)建視圖響應(yīng)狀態(tài)變更
state創(chuàng)建后涮俄,如果是開(kāi)發(fā)應(yīng)用我們需要有視圖來(lái)讓感知變更,MobX
會(huì)以一種最小限度的方式來(lái)更新視圖尸闸,并且它有著令人匪夷所思的高效彻亲。
以下我們以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)測(cè)狀態(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兩算法都是通過(guò)調(diào)用store提供的方法,合情合理宦芦。
可怕的是宙址,clear直接就給state的counter賦值,居然也能成功踪旷,而且視圖是及時(shí)更新曼氛,不禁回想起flux章節(jié)中的clear,恐懼更甚令野,讓人望而退步舀患。
其實(shí)大可不必,這就是mobx的魔力气破,其實(shí)跟vue一般聊浅,它也是通過(guò)Proxy注冊(cè)監(jiān)聽(tīng),實(shí)現(xiàn)動(dòng)態(tài)及時(shí)響應(yīng)。
為了滿足React用戶對(duì)于這種狀態(tài)不可控的恐懼低匙,它也提供了api來(lái)限制這種操作旷痕,必須通過(guò)action來(lái)修改store。
enforceAction
規(guī)定只有action才能改store顽冶。
import { configure } from 'mobx'
configure({enforceAction: true})
provider
當(dāng)然欺抗,為了幫助開(kāi)發(fā)者更合理的制定目錄結(jié)構(gòu)與開(kāi)發(fā)規(guī)范,它也提供了同react-redux
相似的Provider
强重,后代組件使用inject
绞呈,接收來(lái)自Provider注入的狀態(tài),再使用observer
連接react組件和 mobx狀態(tài)间景,達(dá)到實(shí)時(shí)相應(yīng)狀態(tài)變化的效果佃声。
還有一些比如autorun
,reaction
倘要,when
computed
等能力能在狀態(tài)滿足特定條件自動(dòng)被觸發(fā)圾亏,有興趣的可以自行做更多了解。
老規(guī)矩封拧,通過(guò)一個(gè)Counter來(lái)看看效果志鹃。
Mobx vs Redux
通過(guò)上面簡(jiǎn)單的介紹以及demo的體驗(yàn),相信你也有了大致的感受哮缺,我們?cè)俸?jiǎn)單的比對(duì)下它與Redux
弄跌。
無(wú)拘無(wú)束甲喝,這既是Mobx
的優(yōu)點(diǎn)也是它的缺點(diǎn)尝苇,當(dāng)項(xiàng)目規(guī)模較大,涉及到多人開(kāi)發(fā)時(shí)埠胖,這種不加管束的自由將是"災(zāi)難"的開(kāi)始糠溜。
咳,點(diǎn)到即可直撤,懂的都懂非竿。
(有疏漏或偏頗的地方感謝指正!D笔:熘)