如何在非 React 項目中使用 Redux
目錄
- 1、前言
- 2、單純使用 Redux 的問題
2.1、問題 1:代碼冗余
2.2、問題2:不必要的渲染
- 3、React-redux 都干了什么
- 4、構(gòu)建自己項目中的 “Provider” 和 “connect”
4.1、包裝渲染函數(shù)
4.2、避免沒有必要的渲染
- 5、總結(jié)
- 6、練習(xí)
1、前言
最近看到這么一個問題: 請教 redux 與 eventEmitter?
最近一個小項目中(沒有使用 react),因為事件、狀態(tài)變化稍多,想用 redux 管理,可是并沒有發(fā)現(xiàn)很方便。..
說起 Redux,我們一般都說 React。似乎 Redux 和 React 已經(jīng)是天經(jīng)地義理所當(dāng)然地應(yīng)該捆綁在一起。而實際上,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 項目中使用 Redux 的思路和方案。這不僅僅對在非 React 的項目中使用 Redux 很有幫助,而且對理解 React-redux 也大有裨益。
本文假設(shè)讀者已經(jīng)熟練掌握 React、Redux、React-redux 的使用以及 ES6 的基本語法。
2、單純使用 Redux 的問題
我們用一個非常簡單的例子來講解一下在非 React 項目中使用 Redux 會遇到什么問題。假設(shè)頁面上有三個部分,header、body、footer,分別由不同模塊進(jìn)行渲染和控制:
- <div id='header'></div>
- <div id='body'></div>
- <div id='footer'></div>
這個三個部分的元素因為有可能會共享和發(fā)生數(shù)據(jù)變化,我們把它存放在 Redux 的 store 里面,簡單地構(gòu)建一個 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'
- })
很簡單,上面定義了一個 reducer,可以通過三個不同的 action:UPDATE_HEADER、UPDATE_BODY、UPDATE_FOOTER 來分別進(jìn)行對頁面數(shù)據(jù)進(jìn)行修改。
有了 store 以后,頁面其實還是空白的,因為沒有把 store 里面的數(shù)據(jù)取出來渲染到頁面。接下來構(gòu)建三個渲染函數(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)在頁面就可以看到三個 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ā)生變化,這是為什么呢?那是因為數(shù)據(jù)變化的時候并沒有重新渲染頁面(調(diào)用 render 方法),所以需要通過 store.subscribe 訂閱數(shù)據(jù)發(fā)生變化的事件,然后重新渲染不同的部分:
- store.subscribe(renderHeder)
- store.subscribe(renderBody)
- store.subscribe(renderFooter)
好了,現(xiàn)在終于把 jQuery 和 Redux 結(jié)合起來了。成功了用 Redux 管理了這個簡單例子里面可能會發(fā)生改變的狀態(tài)。但這里有幾個問題:
2.1、問題 1:代碼冗余
編寫完一個渲染的函數(shù)以后,需要手動進(jìn)行***次渲染初始化;然后手動通過 store.subscribe 監(jiān)聽 store 的數(shù)據(jù)變化,在數(shù)據(jù)變化的時候進(jìn)行重新調(diào)用渲染函數(shù)。這都是重復(fù)的代碼和沒有必要的工作,而且還可能提供了忘了subscribe 的可能。
2.2、問題2:不必要的渲染
上面的例子中,程序進(jìn)行一次初始化渲染,然后數(shù)據(jù)更新的渲染。3 個渲染函數(shù)里面都有一個 log。兩次渲染***的情況應(yīng)該只有 6 個 log。
但是你可以看到出現(xiàn)了 12 個log,那是因為后續(xù)修改 UPDATE_XXX ,除了會導(dǎo)致該數(shù)據(jù)進(jìn)行渲染,還會導(dǎo)致其余兩個數(shù)據(jù)重新渲染(即使它們其實并沒有變化)。store.subscribe 一股腦的調(diào)用了全部監(jiān)聽函數(shù),但其實數(shù)據(jù)沒有變化就沒有必要重新渲染。
以上的兩個缺點(diǎn)在功能較為復(fù)雜的時候會越來越凸顯。
3、React-redux 都干了什么
可以看到,單純地使用 Redux 和 jQuery 目測沒有給我們帶來什么好處和便利。是不是就可以否了 Redux 在非 React 項目中的用處呢?
回頭想一下,為什么 Redux 和 React 結(jié)合的時候并沒有出現(xiàn)上面所提到的問題?你會發(fā)現(xiàn),其實 React 和 Redux 并沒有像上面這樣如此暴力地結(jié)合在一起。在 React 和 Redux 這兩個庫中間其實隔著第三個庫:React-redux。
在 React + Redux 項目當(dāng)中,我們不需要自己手動進(jìn)行 subscribe,也不需要手動進(jìn)行過多的性能優(yōu)化,恰恰就是因為這些臟活累活都由 React-redux 來做了,對外只提供了一個 Provider 和 connect 的方法,隱藏了關(guān)于 store 操作的很多細(xì)節(jié)。
所以,在把 Redux 和普通項目結(jié)合起來的時候,也可以參考 React-redux,構(gòu)建一個工具庫來隱藏細(xì)節(jié)、簡化工作。
這就是接下來需要做的事情。但在構(gòu)建這個簡單的庫之前,我們需要了解一下 React-redux 干了什么工作。 React-redux 給我們提供了什么功能?在 React-redux 項目中我們一般這樣使用:
- 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 的時候傳入了 mapStateToProps,mapStateToProps 作用很關(guān)鍵,它起到了提取數(shù)據(jù)的作用,可以把這個組件需要的數(shù)據(jù)按需從 store 中提取出來。
實際上,在 React-redux 的內(nèi)部:Provider 接受 store 作為參數(shù),并且通過 context 把 store 傳給所有的子組件;子組件通過 connect 包裹了一層高階組件,高階組件會通過 context 結(jié)合 mapStateToProps 和 store 然后把里面數(shù)據(jù)傳給被包裹的組件。
如果你看不懂上面這段話,可以參考 動手實現(xiàn) React-redux。說白了就是 connect 函數(shù)其實是在 Provider 的基礎(chǔ)上構(gòu)建的,沒有 Provider 那么 connect 也沒有效果。
React 的組件負(fù)責(zé)渲染工作,相當(dāng)于我們例子當(dāng)中的 render 函數(shù)。類似 React-redux 圍繞組件,我們圍繞著渲染函數(shù),可以給它們提供不同于、但是功能類似的 Provider 和 connect。
4、構(gòu)建自己項目中的 Provider 和 connect
4.1、包裝渲染函數(shù)
參考 React-redux,下面假想出一種類似的 provider 和 connect 可以應(yīng)用在上面的 jQuery 例子當(dāng)中:
- /* 通過 provider 生成這個 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)
你會看到,其實我們就是把組件換成了 render 方法而已。用起來和 React-redux 一樣。那么如何構(gòu)建 provider 和 connect 方法呢?這里先搭個骨架:
- const provider = (store) => {
- return (mapStateToProps) => { // connect 函數(shù)
- return (render) => {
- /* TODO */
- }
- }
- }
provider 接受 store 作為參數(shù),返回一個 connect 函數(shù);connect 函數(shù)接受 mapStateToProps 作為參數(shù)返回一個新的函數(shù);這個返回的函數(shù)類似于 React-redux 那樣接受一個組件(渲染函數(shù))作為參數(shù),它的內(nèi)容就是要接下來要實現(xiàn)的代碼。當(dāng)然也可以用多個箭頭的表示方法:
- 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 provider = (store) => (mapStateToProps) => (render) => {
- /* 返回新的渲染函數(shù),就像 React-redux 的 connect 返回新組件 */
- const renderWrapper = () => {
- const props = mapStateToProps(store.getState())
- render(props)
- }
- return renderWrapper
- }
這時候通過本節(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)可以渲染了。但是這時候調(diào)用 store.dispatch 是不會導(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 個 log,還是沒有解決無關(guān)數(shù)據(jù)變化導(dǎo)致的重新渲染問題。
4.2、避免沒有必要的渲染
在上面的代碼中,每次 store.dispatch 都會導(dǎo)致 renderWrapper 函數(shù)執(zhí)行, 它會把 store.getState() 傳給 mapStateToProps來計算新的 props 然后傳給 render。
實際上可以在這里做手腳:緩存上次的計算的 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。因為 mapStateToProps 每次都會返回不一樣的對象,所以并不能直接用 === 來判斷數(shù)據(jù)是否發(fā)生了變化。這里可以判斷兩個對象的***層的數(shù)據(jù)是否全相同,如果相同的話就不需要重新渲染了。例如:
- const a = { name: 'jerry' }
- const b = { name: 'jerry' }
- a === b // false
- shallowEqual(a, b) // true
這時候看看控制臺,只有 6 個 log 了。成功地達(dá)到了性能優(yōu)化的目的。這里 shallowEqual 的實現(xiàn)留給讀者自己做練習(xí)。
到這里,已經(jīng)完成了類似于 React-redux 的一個 Binding,可以愉快地使用在非 React 項目當(dāng)中使用了。完整的代碼可以看這個 gist 。
5、總結(jié)
通過本文可以知道,在非 React 項目結(jié)合 Redux 不能簡單粗暴地將兩個使用起來。要根據(jù)項目需要構(gòu)建這個場景下需要的工具庫來簡化關(guān)于 store 的操作,當(dāng)然可以直接參照 React-redux 的實現(xiàn)來進(jìn)行對應(yīng)的綁定。
也可以總結(jié)出,其實 React-redux 的 connect 幫助我們隱藏了很多關(guān)于store 的操作,包括 store 的數(shù)據(jù)變化的監(jiān)聽重新渲染、數(shù)據(jù)對比和性能優(yōu)化等。
點(diǎn)擊《如何在非 React 項目中使用 Redux》閱讀原文。
【本文是51CTO專欄作者“胡子大哈”的原創(chuàng)文章,轉(zhuǎn)載請聯(lián)系作者本人獲取授權(quán)】