如何在非 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_HEADERUPDATE_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è) Providerconnect 的方法察署,隱藏了關(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é)合 mapStateToPropsstore 然后把里面數(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ù)星爪,可以給它們提供不同于浆西、但是功能類似的 Providerconnect

4顽腾、構(gòu)建自己項(xiàng)目中的 Providerconnect

4.1近零、包裝渲染函數(shù)

參考 React-redux,下面假想出一種類似的 providerconnect 可以應(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)建 providerconnect 方法呢漓摩?這里先搭個(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í):

  1. 實(shí)現(xiàn)一個(gè) shallowEqual
  2. 給 provider 加入 mapDispatchToProps
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末宙地,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子逆皮,更是在濱河造成了極大的恐慌宅粥,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件页屠,死亡現(xiàn)場離奇詭異粹胯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)辰企,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評論 3 385
  • 文/潘曉璐 我一進(jìn)店門风纠,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人牢贸,你說我怎么就攤上這事竹观。” “怎么了潜索?”我有些...
    開封第一講書人閱讀 157,221評論 0 348
  • 文/不壞的土叔 我叫張陵臭增,是天一觀的道長。 經(jīng)常有香客問我竹习,道長誊抛,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,474評論 1 283
  • 正文 為了忘掉前任整陌,我火速辦了婚禮拗窃,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘泌辫。我一直安慰自己随夸,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評論 6 386
  • 文/花漫 我一把揭開白布震放。 她就那樣靜靜地躺著宾毒,像睡著了一般。 火紅的嫁衣襯著肌膚如雪殿遂。 梳的紋絲不亂的頭發(fā)上诈铛,一...
    開封第一講書人閱讀 49,816評論 1 290
  • 那天,我揣著相機(jī)與錄音墨礁,去河邊找鬼癌瘾。 笑死,一個(gè)胖子當(dāng)著我的面吹牛饵溅,可吹牛的內(nèi)容都是我干的妨退。 我是一名探鬼主播,決...
    沈念sama閱讀 38,957評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼蜕企,長吁一口氣:“原來是場噩夢啊……” “哼咬荷!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起轻掩,我...
    開封第一講書人閱讀 37,718評論 0 266
  • 序言:老撾萬榮一對情侶失蹤幸乒,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后唇牧,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體罕扎,經(jīng)...
    沈念sama閱讀 44,176評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡聚唐,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腔召。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片杆查。...
    茶點(diǎn)故事閱讀 38,646評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖臀蛛,靈堂內(nèi)的尸體忽然破棺而出亲桦,到底是詐尸還是另有隱情,我是刑警寧澤浊仆,帶...
    沈念sama閱讀 34,322評論 4 330
  • 正文 年R本政府宣布客峭,位于F島的核電站,受9級特大地震影響抡柿,放射性物質(zhì)發(fā)生泄漏舔琅。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評論 3 313
  • 文/蒙蒙 一洲劣、第九天 我趴在偏房一處隱蔽的房頂上張望搏明。 院中可真熱鬧,春花似錦闪檬、人聲如沸星著。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,755評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽虚循。三九已至,卻和暖如春样傍,著一層夾襖步出監(jiān)牢的瞬間横缔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,987評論 1 266
  • 我被黑心中介騙來泰國打工衫哥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留茎刚,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,358評論 2 360
  • 正文 我出身青樓撤逢,卻偏偏與公主長得像膛锭,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子蚊荣,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評論 2 348

推薦閱讀更多精彩內(nèi)容

  • 做React需要會(huì)什么互例? react的功能其實(shí)很單一奢入,主要負(fù)責(zé)渲染的功能,現(xiàn)有的框架媳叨,比如angular是一個(gè)大而...
    蒼都閱讀 14,747評論 1 139
  • 前言 本文 有配套視頻腥光,可以酌情觀看关顷。 文中內(nèi)容因各人理解不同,可能會(huì)有所偏差武福,歡迎朋友們聯(lián)系我討論议双。 文中所有內(nèi)...
    珍此良辰閱讀 11,898評論 23 111
  • 一、什么情況需要redux艘儒? 1聋伦、用戶的使用方式復(fù)雜 2夫偶、不同身份的用戶有不同的使用方式(比如普通用戶和管...
    初晨的筆記閱讀 2,016評論 0 11
  • 我們已經(jīng)詳細(xì)介紹了Action界睁,Reducer,Store和它們之間的流轉(zhuǎn)關(guān)系兵拢。Redux的基礎(chǔ)知識差不多也介紹完...
    張歆琳閱讀 3,734評論 1 17
  • 有過幾段感情之后開始懂得翻斟,男女之愛不是一個(gè)連續(xù)函數(shù),而是點(diǎn)陣说铃。一個(gè)個(gè)時(shí)刻访惜,甚至是瞬間,我們被愛的感覺籠罩腻扇。mome...
    MINDUAN閱讀 227評論 0 0