3C科技 娛樂遊戲 美食旅遊 時尚美妝 親子育兒 生活休閒 金融理財 健康運動 寰宇綜合

Zi 字媒體

2017-07-25T20:27:27+00:00
加入好友
目錄1、前言2、單純使用 Redux 的問題2.1、問題 1:代碼冗餘2.2、問題2:不必要的渲染3、React-redux 都幹了什麼4、構建自己項目中的 「Provider」 和 「connect」4.1、包裝渲染函數4.2、避免沒有必要的渲染5、總結6、練習1、前言最近在知乎上看到這麼一個問題: 請教 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 的基本語法。2、單純使用 Redux 的問題我們用一個非常簡單的例子來講解一下在非 React 項目中使用 Redux 會遇到什麼問題。假設頁面上有三個部分,header、body、footer,分別由不同模塊進行渲染和控制:<divid='header'div<divid='body'div<divid='footer'div這個三個部分的元素因為有可能會共享和發生數據變化,我們把它存放在 Redux 的 store 裡面,簡單地構建一個 store:const appReducer = (state, action) => { switch (action.type) { case'UPDATE_HEADER': returnObject.assign(state, { header: action.header }) case'UPDATE_BODY': returnObject.assign(state, { body: action.body }) case'UPDATE_FOOTER': returnObject.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來分別進行對頁面數據進行修改。有了 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元素裡面的內容分別為:Header、Body、Footer。我們打算 1s 以後通過store.dispatch更新頁面的數據,模擬 app 數據發生了變化的情況:/* 數據發生變化 */ setTimeout( => { store.dispatch({ type: 'UPDATE_HEADER', header: 'New Header' }) store.dispatch({ type: 'UPDATE_BODY', body: 'New Body''UPDATE_FOOTER', footer: 'New Footer' }) }, 1000)然而 1s 以後頁面沒有發生變化,這是為什麼呢?那是因為數據變化的時候並沒有重新渲染頁面(調用 render 方法),所以需要通過store.subscribe訂閱數據發生變化的事件,然後重新渲染不同的部分:store.subscribe(renderHeder) store.subscribe(renderBody) store.subscribe(renderFooter)好了,現在終於把 jQuery 和 Redux 結合起來了。成功了用 Redux 管理了這個簡單例子裡面可能會發生改變的狀態。但這裡有幾個問題:2.1、問題 1:代碼冗餘編寫完一個渲染的函數以後,需要手動進行第一次渲染初始化;然後手動通過store.subscribe監聽 store 的數據變化,在數據變化的時候進行重新調用渲染函數。這都是重複的代碼和沒有必要的工作,而且還可能提供了忘了subscribe的可能。2.2、問題2:不必要的渲染上面的例子中,程序進行一次初始化渲染,然後數據更新的渲染。3 個渲染函數裡面都有一個 log。兩次渲染最佳的情況應該只有 6 個 log。但是你可以看到出現了 12 個log,那是因為後續修改UPDATE_XXX,除了會導致該數據進行渲染,還會導致其餘兩個數據重新渲染(即使它們其實並沒有變化)。store.subscribe一股腦的調用了全部監聽函數,但其實數據沒有變化就沒有必要重新渲染。以上的兩個缺點在功能較為複雜的時候會越來越凸顯。3、React-redux 都幹了什麼可以看到,單純地使用 Redux 和 jQuery 目測沒有給我們帶來什麼好處和便利。是不是就可以否了 Redux 在非 React 項目中的用處呢?回頭想一下,為什麼 Redux 和 React 結合的時候並沒有出現上面所提到的問題?你會發現,其實 React 和 Redux 並沒有像上面這樣如此暴力地結合在一起。在 React 和 Redux 這兩個庫中間其實隔著第三個庫:React-redux。在 React + Redux 項目當中,我們不需要自己手動進行subscribe,也不需要手動進行過多的性能優化,恰恰就是因為這些臟活累活都由 React-redux 來做了,對外只提供了一個Provider和connect的方法,隱藏了關於 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 ( <Providerstore={store}<Header />Provider> ) } }我們把store傳給了Provider,然後其他組件就可以使用connect進行取數據的操作。connect 的時候傳入了作用很關鍵,它起到了提取數據的作用,可以把這個組件需要的數據按需從 store 中提取出來。實際上,在 React-redux 的內部:Provider接受 store 作為參數,並且通過 context 把 store 傳給所有的子組件;子組件通過connect包裹了一層高階組件,高階組件會通過 context 結合mapStateToPropsstore如果你看不懂上面這段話,可以參考 動手實現 React-redux。說白了就是Provider的基礎上構建的,沒有Provider那麼connectReact 的組件負責渲染工作,相當於我們例子當中的 render 函數。類似 React-redux 圍繞組件,我們圍繞著渲染函數,可以給它們提供不同於、但是功能類似的Provider4、構建自己項目中的Providerconnect4.1、包裝渲染函數參考 React-redux,下面假想出一種類似的provider和connect可以應用在上面的 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 一樣。那麼如何構建provider和connect方法呢?這裡先搭個骨架:const provider = (store) => { return (mapStateToProps) => { // connect 函數return (render) => { /* TODO */ } } }provider接受store作為參數,返回一個connect函數;connect函數接受mapStateToProps作為參數返回一個新的函數;這個返回的函數類似於 React-redux 那樣接受一個組件(渲染函數)作為參數,它的內容就是要接下來要實現的代碼。當然也可以用多個箭頭的表示方法:const provider = (store) => (mapStateToProps) => (render) => { /* TODO */ }store、mapStateToProps、render都有了,剩下就是把 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.2、避免沒有必要的渲染在上面的代碼中,每次store.dispatch都會導致renderWrapper函數執行, 它會把store.getState傳給mapStateToProps來計算新的props實際上可以在這裡做手腳:緩存上次的計算的 props,然後用新的 props 和舊的 props 進行對比,如果兩者相同,就不調用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 }這裡的關鍵點在於。因為mapStateToProps每次都會返回不一樣的對象,所以並不能直接用===來判斷數據是否發生了變化。這裡可以判斷兩個對象的第一層的數據是否全相同,如果相同的話就不需要重新渲染了。例如:const a = { name: 'jerry' } const b = { name: 'jerry' } a === b // false shallowEqual(a, b) // true這時候看看控制台,只有 6 個 log 了。成功地達到了性能優化的目的。這裡shallowEqual的實現留給讀者自己做練習。到這裡,已經完成了類似於 React-redux 的一個 Binding,可以愉快地使用在非 React 項目當中使用了。完整的代碼可以看這個 gist 。5、總結通過本文可以知道,在非 React 項目結合 Redux 不能簡單粗暴地將兩個使用起來。要根據項目需要構建這個場景下需要的工具庫來簡化關於 store 的操作,當然可以直接參照 React-redux 的實現來進行對應的綁定。也可以總結出,其實 React-redux 的 connect 幫助我們隱藏了很多關於store 的操作,包括 store 的數據變化的監聽重新渲染、數據對比和性能優化等。6、練習

本文由yidianzixun提供 原文連結

寫了 5860316篇文章,獲得 23313次喜歡
精彩推薦