如何在非 React 項目中使用 Redux

目錄

  • 一、前言react

  • 二、單純使用 Redux 的問題git

    • 2.一、問題 1:代碼冗餘github

    • 2.二、問題2:沒必要要的渲染redux

  • 三、React-redux 都幹了什麼緩存

  • 四、構建本身項目中的 「Provider」 和 「connect」性能優化

    • 4.一、包裝渲染函數app

    • 4.二、避免沒有必要的渲染

  • 五、總結

  • 六、練習

一、前言

最近在知乎上看到這麼一個問題: 請教 redux 與 eventEmitter? - 知乎

最近一個小項目中(沒有使用 react),由於事件、狀態變化稍多,想用 redux 管理,但是並無發現很方便。..

提及 Redux,咱們通常都說 React。彷佛 Redux 和 React 已是天經地義理所固然地應該捆綁在一塊兒。而實際上,Redux 官方給本身的定位倒是:

Redux is a predictable state container for JavaScript apps.

Redux 絕口不提 React,它給本身的定義是 「給 JavaScript 應用程序提供可預測的狀態容器」。也就是說,你能夠在任何須要進行應用狀態管理的 JavaScript 應用程序中使用 Redux。

可是一旦脫離了 React 的環境,Redux 彷佛就脫繮了,用起來桀驁不馴,難以上手。本文就帶你分析一下問題的緣由,而且提供一種在非 React 項目中使用 Redux 的思路和方案。這不只僅對在非 React 的項目中使用 Redux 頗有幫助,並且對理解 React-redux 也大有裨益。

本文假設讀者已經熟練掌握 React、Redux、React-redux 的使用以及 ES6 的基本語法。

二、單純使用 Redux 的問題

咱們用一個很是簡單的例子來說解一下在非 React 項目中使用 Redux 會遇到什麼問題。假設頁面上有三個部分,header、body、footer,分別由不一樣模塊進行渲染和控制:

<div id='header'></div>
<div id='body'></div>
<div id='footer'></div>

這個三個部分的元素由於有可能會共享和發生數據變化,咱們把它存放在 Redux 的 store 裏面,簡單地構建一個 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_HEADERUPDATE_BODYUPDATE_FOOTER 來分別進行對頁面數據進行修改。

有了 store 之後,頁面其實仍是空白的,由於沒有把 store 裏面的數據取出來渲染到頁面。接下來構建三個渲染函數,這裏使用了 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()

如今頁面就能夠看到三個 div 元素裏面的內容分別爲:HeaderBodyFooter。咱們打算 1s 之後經過 store.dispatch 更新頁面的數據,模擬 app 數據發生了變化的狀況:

/* 數據發生變化 */
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 之後頁面沒有發生變化,這是爲何呢?那是由於數據變化的時候並無從新渲染頁面(調用 render 方法),因此須要經過 store.subscribe 訂閱數據發生變化的事件,而後從新渲染不一樣的部分:

store.subscribe(renderHeder)
store.subscribe(renderBody)
store.subscribe(renderFooter)

好了,如今終於把 jQuery 和 Redux 結合起來了。成功了用 Redux 管理了這個簡單例子裏面可能會發生改變的狀態。但這裏有幾個問題:

2.一、問題 1:代碼冗餘

編寫完一個渲染的函數之後,須要手動進行第一次渲染初始化;而後手動經過 store.subscribe 監聽 store 的數據變化,在數據變化的時候進行從新調用渲染函數。這都是重複的代碼和沒有必要的工做,並且還可能提供了忘了subscribe 的可能。

2.二、問題2:沒必要要的渲染

上面的例子中,程序進行一次初始化渲染,而後數據更新的渲染。3 個渲染函數裏面都有一個 log。兩次渲染最佳的狀況應該只有 6 個 log。

可是你能夠看到出現了 12 個log,那是由於後續修改 UPDATE_XXX ,除了會致使該數據進行渲染,還會致使其他兩個數據從新渲染(即便它們其實並無變化)。store.subscribe 一股腦的調用了所有監聽函數,但其實數據沒有變化就沒有必要從新渲染。

以上的兩個缺點在功能較爲複雜的時候會愈來愈凸顯。

三、React-redux 都幹了什麼

能夠看到,單純地使用 Redux 和 jQuery 目測沒有給咱們帶來什麼好處和便利。是否是就能夠否了 Redux 在非 React 項目中的用處呢?

回頭想一下,爲何 Redux 和 React 結合的時候並無出現上面所提到的問題?你會發現,其實 React 和 Redux 並無像上面這樣如此暴力地結合在一塊兒。在 React 和 Redux 這兩個庫中間其實隔着第三個庫:React-redux。

在 React + Redux 項目當中,咱們不須要本身手動進行 subscribe,也不須要手動進行過多的性能優化,偏偏就是由於這些髒活累活都由 React-redux 來作了,對外只提供了一個 Providerconnect 的方法,隱藏了關於 store 操做的不少細節。

因此,在把 Redux 和普通項目結合起來的時候,也能夠參考 React-redux,構建一個工具庫來隱藏細節、簡化工做。

這就是接下來須要作的事情。但在構建這個簡單的庫以前,咱們須要瞭解一下 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 進行取數據的操做。connect 的時候傳入了 mapStateToPropsmapStateToProps 做用很關鍵,它起到了提取數據的做用,能夠把這個組件須要的數據按需從 store 中提取出來。

實際上,在 React-redux 的內部:Provider 接受 store 做爲參數,而且經過 context 把 store 傳給全部的子組件;子組件經過 connect 包裹了一層高階組件,高階組件會經過 context 結合 mapStateToPropsstore 而後把裏面數據傳給被包裹的組件。

若是你看不懂上面這段話,能夠參考 動手實現 React-redux。說白了就是 connect 函數實際上是在 Provider 的基礎上構建的,沒有 Provider 那麼 connect 也沒有效果。

React 的組件負責渲染工做,至關於咱們例子當中的 render 函數。相似 React-redux 圍繞組件,咱們圍繞着渲染函數,能夠給它們提供不一樣於、可是功能相似的 Providerconnect

四、構建本身項目中的 Providerconnect

4.一、包裝渲染函數

參考 React-redux,下面假想出一種相似的 providerconnect 能夠應用在上面的 jQuery 例子當中:

/* 經過 provider 生成這個 store 對應的 connect 函數 */
const connect = provider(store)

/* 普通的 render 方法 */
let renderHeader = (props) => {
  console.log('render header')
  $('#header').html(props.header)
}

/* 用 connect 取數據傳給 render 方法 */
const mapStateToProps = (state) => {
  return { header: state.header }
}
renderHeader = connect(mapStateToProps)(renderHeader)

你會看到,其實咱們就是把組件換成了 render 方法而已。用起來和 React-redux 同樣。那麼如何構建 providerconnect 方法呢?這裏先搭個骨架:

const provider = (store) => {
  return (mapStateToProps) => { // connect 函數
    return (render) => {
      /* TODO */
    }
  }
}

provider 接受 store 做爲參數,返回一個 connect 函數;connect 函數接受 mapStateToProps 做爲參數返回一個新的函數;這個返回的函數相似於 React-redux 那樣接受一個組件(渲染函數)做爲參數,它的內容就是要接下來要實現的代碼。固然也能夠用多個箭頭的表示方法:

const provider = (store) => (mapStateToProps) => (render) => {
  /* TODO */
}

storemapStateToPropsrender 都有了,剩下就是把 store 裏面的數據取出來傳給 mapStateToProps 來得到 props;而後再把 props 傳給 render 函數。

const provider = (store) => (mapStateToProps) => (render) => {
  /* 返回新的渲染函數,就像 React-redux 的 connect 返回新組件 */
  const renderWrapper = () => {
    const props = mapStateToProps(store.getState())
    render(props)
  }
  return renderWrapper
}

這時候經過本節一開始假想的代碼已經能夠正常渲染了,一樣的方式改寫其餘部分的代碼:

/* 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)

雖然頁面已經能夠渲染了。可是這時候調用 store.dispatch 是不會致使從新渲染的,咱們能夠順帶在 connect 裏面進行 subscribe:

const provider = (store) => (mapStateToProps) => (render) => {
  /* 返回新的渲染函數,就像 React-redux 返回新組件 */
  const renderWrapper = () => {
    const props = mapStateToProps(store.getState())
    render(props)
  }
  /* 監聽數據變化從新渲染 */
  store.subscribe(renderWrapper)
  return renderWrapper
}

贊。如今 store.dispatch 能夠致使頁面從新渲染了,已經原來的功能同樣了。可是,看看控制檯仍是打印了 12 個 log,仍是沒有解決無關數據變化致使的從新渲染問題。

4.二、避免沒有必要的渲染

在上面的代碼中,每次 store.dispatch 都會致使 renderWrapper 函數執行, 它會把 store.getState() 傳給 mapStateToProps 來計算新的 props 而後傳給 render

實際上能夠在這裏作手腳:緩存上次的計算的 props,而後用新的 props 和舊的 props 進行對比,若是二者相同,就不調用 render

const provider = (store) => (mapStateToProps) => (render) => {
  /* 緩存 props */
  let props
  const renderWrapper = () => {
    const newProps = mapStateToProps(store.getState())
    /* 若是新的結果和原來的同樣,就不要從新渲染了 */
    if (shallowEqual(props, newProps)) return
    props = newProps
    render(props)
  }
  /* 監聽數據變化從新渲染 */
  store.subscribe(renderWrapper)
  return renderWrapper
}

這裏的關鍵點在於 shallowEqual。由於 mapStateToProps 每次都會返回不同的對象,因此並不能直接用 === 來判斷數據是否發生了變化。這裏能夠判斷兩個對象的第一層的數據是否全相同,若是相同的話就不須要從新渲染了。例如:

const a = { name: 'jerry' }
const b = { name: 'jerry' }

a === b // false
shallowEqual(a, b) // true

這時候看看控制檯,只有 6 個 log 了。成功地達到了性能優化的目的。這裏 shallowEqual 的實現留給讀者本身作練習。

到這裏,已經完成了相似於 React-redux 的一個 Binding,能夠愉快地使用在非 React 項目當中使用了。完整的代碼能夠看這個 gist

五、總結

經過本文能夠知道,在非 React 項目結合 Redux 不能簡單粗暴地將兩個使用起來。要根據項目須要構建這個場景下須要的工具庫來簡化關於 store 的操做,固然能夠直接參照 React-redux 的實現來進行對應的綁定。

也能夠總結出,其實 React-redux 的 connect 幫助咱們隱藏了不少關於store 的操做,包括 store 的數據變化的監聽從新渲染、數據對比和性能優化等。

六、練習

對本文所講內容有興趣的朋友能夠作一下本文配套的練習:

  1. 實現一個 shallowEqual

  2. 給 provider 加入 mapDispatchToProps

相關文章
相關標籤/搜索