- 本文作者:胡子大哈
- 原文鏈接:https://scriptoj.com/topic/178/如何在非-react-項(xiàng)目中使用-redux
- 轉(zhuǎn)載請注明出處关串,保留原文鏈接和作者信息。
目錄
- 1、前言
- 2顷蟀、單純使用 Redux 的問題
- 2.1、問題 1:代碼冗余
- 2.2、問題2:不必要的渲染
- 3、React-redux 都干了什么
- 4澎怒、構(gòu)建自己項(xiàng)目中的 “Provider” 和 “connect”
- 4.1、包裝渲染函數(shù)
- 4.2、避免沒有必要的渲染
- 5喷面、總結(jié)
- 6星瘾、練習(xí)
1、前言
最近在知乎上看到這么一個(gè)問題: 請教 redux 與 eventEmitter? - 知乎惧辈。
最近一個(gè)小項(xiàng)目中(沒有使用 react)琳状,因?yàn)槭录顟B(tài)變化稍多盒齿,想用 redux 管理念逞,可是并沒有發(fā)現(xiàn)很方便。..
說起 Redux边翁,我們一般都說 React翎承。似乎 Redux 和 React 已經(jīng)是天經(jīng)地義理所當(dāng)然地應(yīng)該捆綁在一起符匾。而實(shí)際上叨咖,Redux 官方給自己的定位卻是:
Redux is a predictable state container for JavaScript apps.
Redux 絕口不提 React啊胶,它給自己的定義是 “給 JavaScript 應(yīng)用程序提供可預(yù)測的狀態(tài)容器”儒恋。也就是說露乏,你可以在任何需要進(jìn)行應(yīng)用狀態(tài)管理的 JavaScript 應(yīng)用程序中使用 Redux碧浊。
但是一旦脫離了 React 的環(huán)境涂邀,Redux 似乎就脫韁了瘟仿,用起來桀驁不馴,難以上手比勉。本文就帶你分析一下問題的原因劳较,并且提供一種在非 React 項(xiàng)目中使用 Redux 的思路和方案。這不僅僅對在非 React 的項(xiàng)目中使用 Redux 很有幫助浩聋,而且對理解 React-redux 也大有裨益观蜗。
本文假設(shè)讀者已經(jīng)熟練掌握 React、Redux衣洁、React-redux 的使用以及 ES6 的基本語法墓捻。
2、單純使用 Redux 的問題
我們用一個(gè)非常簡單的例子來講解一下在非 React 項(xiàng)目中使用 Redux 會(huì)遇到什么問題坊夫。假設(shè)頁面上有三個(gè)部分砖第,header撤卢、body、footer梧兼,分別由不同模塊進(jìn)行渲染和控制:
<div id='header'></div>
<div id='body'></div>
<div id='footer'></div>
這個(gè)三個(gè)部分的元素因?yàn)橛锌赡軙?huì)共享和發(fā)生數(shù)據(jù)變化放吩,我們把它存放在 Redux 的 store 里面,簡單地構(gòu)建一個(gè) store:
const appReducer = (state, action) => {
switch (action.type) {
case 'UPDATE_HEADER':
return Object.assign(state, { header: action.header })
case 'UPDATE_BODY':
return Object.assign(state, { body: action.body })
case 'UPDATE_FOOTER':
return Object.assign(state, { footer: action.footer })
default:
return state
}
}
const store = Redux.createStore(appReducer, {
header: 'Header',
body: 'Body',
footer: 'Footer'
})
很簡單羽杰,上面定義了一個(gè) reducer渡紫,可以通過三個(gè)不同的 action:UPDATE_HEADER
、UPDATE_BODY
考赛、UPDATE_FOOTER
來分別進(jìn)行對頁面數(shù)據(jù)進(jìn)行修改惕澎。
有了 store 以后,頁面其實(shí)還是空白的欲虚,因?yàn)闆]有把 store 里面的數(shù)據(jù)取出來渲染到頁面集灌。接下來構(gòu)建三個(gè)渲染函數(shù),這里使用了 jQuery:
/* 渲染 Header */
const renderHeader = () => {
console.log('render header')
$('#header').html(store.getState().header)
}
renderHeader()
/* 渲染 Body */
const renderBody = () => {
console.log('render body')
$('#body').html(store.getState().body)
}
renderBody()
/* 渲染 Footer */
const renderFooter = () => {
console.log('render footer')
$('#footer').html(store.getState().footer)
}
renderFooter()
現(xiàn)在頁面就可以看到三個(gè) div
元素里面的內(nèi)容分別為:Header
复哆、Body
欣喧、Footer
。我們打算 1s 以后通過 store.dispatch
更新頁面的數(shù)據(jù)梯找,模擬 app 數(shù)據(jù)發(fā)生了變化的情況:
/* 數(shù)據(jù)發(fā)生變化 */
setTimeout(() => {
store.dispatch({ type: 'UPDATE_HEADER', header: 'New Header' })
store.dispatch({ type: 'UPDATE_BODY', body: 'New Body' })
store.dispatch({ type: 'UPDATE_FOOTER', footer: 'New Footer' })
}, 1000)
然而 1s 以后頁面沒有發(fā)生變化唆阿,這是為什么呢?那是因?yàn)閿?shù)據(jù)變化的時(shí)候并沒有重新渲染頁面(調(diào)用 render 方法)锈锤,所以需要通過 store.subscribe
訂閱數(shù)據(jù)發(fā)生變化的事件驯鳖,然后重新渲染不同的部分:
store.subscribe(renderHeder)
store.subscribe(renderBody)
store.subscribe(renderFooter)
好了,現(xiàn)在終于把 jQuery 和 Redux 結(jié)合起來了久免。成功了用 Redux 管理了這個(gè)簡單例子里面可能會(huì)發(fā)生改變的狀態(tài)浅辙。但這里有幾個(gè)問題:
2.1、問題 1:代碼冗余
編寫完一個(gè)渲染的函數(shù)以后阎姥,需要手動(dòng)進(jìn)行第一次渲染初始化记舆;然后手動(dòng)通過 store.subscribe
監(jiān)聽 store 的數(shù)據(jù)變化,在數(shù)據(jù)變化的時(shí)候進(jìn)行重新調(diào)用渲染函數(shù)呼巴。這都是重復(fù)的代碼和沒有必要的工作泽腮,而且還可能提供了忘了subscribe
的可能。
2.2衣赶、問題2:不必要的渲染
上面的例子中诊赊,程序進(jìn)行一次初始化渲染,然后數(shù)據(jù)更新的渲染府瞄。3 個(gè)渲染函數(shù)里面都有一個(gè) log碧磅。兩次渲染最佳的情況應(yīng)該只有 6 個(gè) log。
但是你可以看到出現(xiàn)了 12 個(gè)log,那是因?yàn)楹罄m(xù)修改 UPDATE_XXX
鲸郊,除了會(huì)導(dǎo)致該數(shù)據(jù)進(jìn)行渲染敲街,還會(huì)導(dǎo)致其余兩個(gè)數(shù)據(jù)重新渲染(即使它們其實(shí)并沒有變化)。store.subscribe
一股腦的調(diào)用了全部監(jiān)聽函數(shù)严望,但其實(shí)數(shù)據(jù)沒有變化就沒有必要重新渲染多艇。
以上的兩個(gè)缺點(diǎn)在功能較為復(fù)雜的時(shí)候會(huì)越來越凸顯。
3像吻、React-redux 都干了什么
可以看到峻黍,單純地使用 Redux 和 jQuery 目測沒有給我們帶來什么好處和便利。是不是就可以否了 Redux 在非 React 項(xiàng)目中的用處呢拨匆?
回頭想一下姆涩,為什么 Redux 和 React 結(jié)合的時(shí)候并沒有出現(xiàn)上面所提到的問題?你會(huì)發(fā)現(xiàn)惭每,其實(shí) React 和 Redux 并沒有像上面這樣如此暴力地結(jié)合在一起骨饿。在 React 和 Redux 這兩個(gè)庫中間其實(shí)隔著第三個(gè)庫:React-redux。
在 React + Redux 項(xiàng)目當(dāng)中台腥,我們不需要自己手動(dòng)進(jìn)行 subscribe
宏赘,也不需要手動(dòng)進(jìn)行過多的性能優(yōu)化,恰恰就是因?yàn)檫@些臟活累活都由 React-redux 來做了黎侈,對外只提供了一個(gè) Provider
和 connect
的方法察署,隱藏了關(guān)于 store 操作的很多細(xì)節(jié)。
所以峻汉,在把 Redux 和普通項(xiàng)目結(jié)合起來的時(shí)候贴汪,也可以參考 React-redux,構(gòu)建一個(gè)工具庫來隱藏細(xì)節(jié)休吠、簡化工作扳埂。
這就是接下來需要做的事情。但在構(gòu)建這個(gè)簡單的庫之前瘤礁,我們需要了解一下 React-redux 干了什么工作阳懂。 React-redux 給我們提供了什么功能?在 React-redux 項(xiàng)目中我們一般這樣使用:
import { connect, Provider } from 'react-redux'
/* Header 組件 */
class Header extends Component {
render () {
return (<div>{this.props.header}</div>)
}
}
const mapStateToProps = (state) => {
return { header: state.header }
}
Header = connect(mapStateToProps)(Header)
/* App 組件 */
class App extends Component {
render () {
return (
<Provider store={store}>
<Header />
</Provider>
)
}
}
我們把 store
傳給了 Provider
蔚携,然后其他組件就可以使用 connect
進(jìn)行取數(shù)據(jù)的操作希太。connect 的時(shí)候傳入了 mapStateToProps
克饶,mapStateToProps
作用很關(guān)鍵酝蜒,它起到了提取數(shù)據(jù)的作用,可以把這個(gè)組件需要的數(shù)據(jù)按需從 store 中提取出來矾湃。
實(shí)際上亡脑,在 React-redux 的內(nèi)部:Provider
接受 store 作為參數(shù),并且通過 context 把 store 傳給所有的子組件;子組件通過 connect
包裹了一層高階組件霉咨,高階組件會(huì)通過 context 結(jié)合 mapStateToProps
和 store
然后把里面數(shù)據(jù)傳給被包裹的組件蛙紫。
如果你看不懂上面這段話,可以參考 動(dòng)手實(shí)現(xiàn) React-redux途戒。說白了就是 connect
函數(shù)其實(shí)是在 Provider
的基礎(chǔ)上構(gòu)建的坑傅,沒有 Provider
那么 connect
也沒有效果。
React 的組件負(fù)責(zé)渲染工作喷斋,相當(dāng)于我們例子當(dāng)中的 render 函數(shù)唁毒。類似 React-redux 圍繞組件,我們圍繞著渲染函數(shù)星爪,可以給它們提供不同于浆西、但是功能類似的 Provider
和 connect
。
4顽腾、構(gòu)建自己項(xiàng)目中的 Provider
和 connect
4.1近零、包裝渲染函數(shù)
參考 React-redux,下面假想出一種類似的 provider
和 connect
可以應(yīng)用在上面的 jQuery 例子當(dāng)中:
/* 通過 provider 生成這個(gè) store 對應(yīng)的 connect 函數(shù) */
const connect = provider(store)
/* 普通的 render 方法 */
let renderHeader = (props) => {
console.log('render header')
$('#header').html(props.header)
}
/* 用 connect 取數(shù)據(jù)傳給 render 方法 */
const mapStateToProps = (state) => {
return { header: state.header }
}
renderHeader = connect(mapStateToProps)(renderHeader)
你會(huì)看到抄肖,其實(shí)我們就是把組件換成了 render 方法而已久信。用起來和 React-redux 一樣。那么如何構(gòu)建 provider
和 connect
方法呢漓摩?這里先搭個(gè)骨架:
const provider = (store) => {
return (mapStateToProps) => { // connect 函數(shù)
return (render) => {
/* TODO */
}
}
}
provider
接受 store
作為參數(shù)入篮,返回一個(gè) connect
函數(shù);connect
函數(shù)接受 mapStateToProps
作為參數(shù)返回一個(gè)新的函數(shù)幌甘;這個(gè)返回的函數(shù)類似于 React-redux 那樣接受一個(gè)組件(渲染函數(shù))作為參數(shù)潮售,它的內(nèi)容就是要接下來要實(shí)現(xiàn)的代碼。當(dāng)然也可以用多個(gè)箭頭的表示方法:
const provider = (store) => (mapStateToProps) => (render) => {
/* TODO */
}
store
锅风、mapStateToProps
酥诽、render
都有了,剩下就是把 store 里面的數(shù)據(jù)取出來傳給 mapStateToProps
來獲得 props
皱埠;然后再把 props
傳給 render
函數(shù)肮帐。
const provider = (store) => (mapStateToProps) => (render) => {
/* 返回新的渲染函數(shù),就像 React-redux 的 connect 返回新組件 */
const renderWrapper = () => {
const props = mapStateToProps(store.getState())
render(props)
}
return renderWrapper
}
這時(shí)候通過本節(jié)一開始假想的代碼已經(jīng)可以正常渲染了边器,同樣的方式改寫其他部分的代碼:
/* body */
let renderBody = (props) => {
console.log('render body')
$('#body').html(props.body)
}
mapStateToProps = (state) => {
return { body: state.body }
}
renderBody = connect(mapStateToProps)(renderBody)
/* footer */
let renderFooter = (props) => {
console.log('render footer')
$('#footer').html(props.footer)
}
mapStateToProps = (state) => {
return { footer: state.footer }
}
renderFooter = connect(mapStateToProps)(renderFooter)
雖然頁面已經(jīng)可以渲染了训枢。但是這時(shí)候調(diào)用 store.dispatch
是不會(huì)導(dǎo)致重新渲染的,我們可以順帶在 connect 里面進(jìn)行 subscribe:
const provider = (store) => (mapStateToProps) => (render) => {
/* 返回新的渲染函數(shù)忘巧,就像 React-redux 返回新組件 */
const renderWrapper = () => {
const props = mapStateToProps(store.getState())
render(props)
}
/* 監(jiān)聽數(shù)據(jù)變化重新渲染 */
store.subscribe(renderWrapper)
return renderWrapper
}
贊『憬纾現(xiàn)在 store.dispatch
可以導(dǎo)致頁面重新渲染了,已經(jīng)原來的功能一樣了砚嘴。但是十酣,看看控制臺還是打印了 12 個(gè) log涩拙,還是沒有解決無關(guān)數(shù)據(jù)變化導(dǎo)致的重新渲染問題。
4.2耸采、避免沒有必要的渲染
在上面的代碼中兴泥,每次 store.dispatch
都會(huì)導(dǎo)致 renderWrapper
函數(shù)執(zhí)行, 它會(huì)把 store.getState()
傳給 mapStateToProps
來計(jì)算新的 props
然后傳給 render
虾宇。
實(shí)際上可以在這里做手腳:緩存上次的計(jì)算的 props搓彻,然后用新的 props 和舊的 props 進(jìn)行對比,如果兩者相同嘱朽,就不調(diào)用 render
:
const provider = (store) => (mapStateToProps) => (render) => {
/* 緩存 props */
let props
const renderWrapper = () => {
const newProps = mapStateToProps(store.getState())
/* 如果新的結(jié)果和原來的一樣好唯,就不要重新渲染了 */
if (shallowEqual(props, newProps)) return
props = newProps
render(props)
}
/* 監(jiān)聽數(shù)據(jù)變化重新渲染 */
store.subscribe(renderWrapper)
return renderWrapper
}
這里的關(guān)鍵點(diǎn)在于 shallowEqual
。因?yàn)?mapStateToProps
每次都會(huì)返回不一樣的對象燥翅,所以并不能直接用 ===
來判斷數(shù)據(jù)是否發(fā)生了變化骑篙。這里可以判斷兩個(gè)對象的第一層的數(shù)據(jù)是否全相同,如果相同的話就不需要重新渲染了森书。例如:
const a = { name: 'jerry' }
const b = { name: 'jerry' }
a === b // false
shallowEqual(a, b) // true
這時(shí)候看看控制臺靶端,只有 6 個(gè) log 了。成功地達(dá)到了性能優(yōu)化的目的凛膏。這里 shallowEqual
的實(shí)現(xiàn)留給讀者自己做練習(xí)杨名。
到這里,已經(jīng)完成了類似于 React-redux 的一個(gè) Binding猖毫,可以愉快地使用在非 React 項(xiàng)目當(dāng)中使用了台谍。完整的代碼可以看這個(gè) gist 。
5吁断、總結(jié)
通過本文可以知道趁蕊,在非 React 項(xiàng)目結(jié)合 Redux 不能簡單粗暴地將兩個(gè)使用起來。要根據(jù)項(xiàng)目需要構(gòu)建這個(gè)場景下需要的工具庫來簡化關(guān)于 store 的操作仔役,當(dāng)然可以直接參照 React-redux 的實(shí)現(xiàn)來進(jìn)行對應(yīng)的綁定掷伙。
也可以總結(jié)出,其實(shí) React-redux 的 connect 幫助我們隱藏了很多關(guān)于store 的操作又兵,包括 store 的數(shù)據(jù)變化的監(jiān)聽重新渲染任柜、數(shù)據(jù)對比和性能優(yōu)化等。
6沛厨、練習(xí)
對本文所講內(nèi)容有興趣的朋友可以做一下本文配套的練習(xí):