前言 最近利用業餘時間閱讀了鬍子大哈寫的《React小書》,從基本的原理講解了React,Redux等等受益頗豐。眼過千遍不如手寫一遍,跟著作者的思路以及參考代碼可以實現基本的Demo,下麵根據自己的理解和參考一些資料,用原生JS從零開始實現一個Redux架構。 一.Redux基本概念 經常用Rea ...
前言
最近利用業餘時間閱讀了鬍子大哈寫的《React小書》,從基本的原理講解了React,Redux等等受益頗豐。眼過千遍不如手寫一遍,跟著作者的思路以及參考代碼可以實現基本的Demo,下麵根據自己的理解和參考一些資料,用原生JS從零開始實現一個Redux架構。
一.Redux基本概念
經常用React開發的朋友可能很熟悉Redux,React-Redux,這裡告訴大家的是,Redux和React-Redux並不是一個東西,Redux是一種架構模式,2015年,Redux出現,將 Flux 與函數式編程結合一起,很短時間內就成為了最熱門的前端架構。它不關心你使用什麼庫,可以把它和React,Vue或者JQuery結合。
二.由一個簡單的例子開始
我們從一個簡單的例子開始推演,新建一個html頁面,代碼如下:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Make-Redux</title> </head> <body> <div id="app"> <div id="title"></div> <div id="content"></div> </div> <script> // 應用的狀態 const appState = { title: { text: '這是一段標題', color: 'Red' }, content: { text: '這是一段內容', color: 'blue' } }; // 渲染函數 function renderApp(appState) { renderTitle(appState.title); renderContent(appState.content); } function renderTitle(title) { const titleDOM = document.getElementById('title'); titleDOM.innerHTML = title.text; titleDOM.style.color = title.color; } function renderContent(content) { const contentDOM = document.getElementById('content'); contentDOM.innerHTML = content.text; contentDOM.style.color = content.color; } // 渲染數據到頁面上 renderApp(appState); </script> </body> </html>
HTML內容很簡單,我們定義了一個appState數據對象,包括title和content屬性,各自都有text和color,然後定義了renderApp,renderTitle,renderContent渲染方法,最後執行renderApp(appState),打開頁面:
這些寫雖然沒有什麼問題,但是存在一個比較大的隱患,每個人都可以修改共用狀態appState,在平時的業務開發中也很常見的一個問題是,定義了一個全局變數,其他同事在不知情的情況下可能會被覆蓋修改刪除掉,帶來的問題是函數執行的結果往往是不可預料的,出現問題的時候調試起來非常困難。
那我們如何解決這個問題呢,我們可以提高修改共用數據的門檻,但是不能直接修改,只能修改我允許的某些修改。於是,定義一個dispatch方法,專門負責數據的修改。
function dispatch (action) { switch (action.type) { case 'UPDATE_TITLE_TEXT': appState.title.text = action.text; break; case 'UPDATE_TITLE_COLOR': appState.title.color = action.color; break; default: break; } }
這樣我們規定,所有歲數據的操作必須通過dispatch方法。它接受一個對象暫且叫它action,規定只能修改title的文字與顏色。這樣要想知道哪個函數修改了數據,我們直接在dispatch方法裡面斷點調試就可以了。大大的提高瞭解決問題的效率。
三.抽離store和實現監控數據變化
上面我們的appStore和dispatch分開的,為了使這種模式更加通用化,我們把他們集中一個地方構建一個函數createStore,用它來生產一個store對象,包含state和dispatch。
function createStore (state, stateChanger) { const getState = () => state; const dispatch = (action) => stateChanger(state, action); return { getState, dispatch } }
我們修改之前的代碼如下:
let appState = { title: { text: '這是一段標題', color: 'red', }, content: { text: '這是一段內容', color: 'blue' } } function stateChanger (state, action) { switch (action.type) { case 'UPDATE_TITLE_TEXT': state.title.text = action.text break case 'UPDATE_TITLE_COLOR': state.title.color = action.color break default: break } } const store = createStore(appState, stateChanger) // 首次渲染頁面 renderApp(store.getState()); // 修改標題文本 store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '換一個標題' }); // 修改標題顏色 store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'grey' }); // 再次把修改後的數據渲染到頁面上 renderApp(store.getState());
上面代碼不難理解:我們用createStore生成了一個store,可以發現,第一個參數state就是我們之前聲明的共用數據,第二個stateChanger方法就是之前聲明的dispatch用於修改數據的方法。
然後我們調用了來兩次store.dispatch方法,最後又重新調用了renderApp再重新獲取新數據渲染了頁面,如下:可以發現title的文字和標題都改變了。
那麼問題來了,我們每次dispatch修改數據的時候,都要手動的調用renderApp方法才能使頁面得以改變。我們可以把renderApp放到dispatch方法最後,這樣的話,我們的createStore不夠通用,因為其他的App不一定要執行renderApp方法,這裡我們通過一種監聽數據變化,然後再重新渲染頁面,術語上講叫做觀察者模式。
我們修改createStore如下。
function createStore (state, stateChanger) { const listeners = []; // 空的方法數組 // store調用一次subscribe就把傳入的listener方法push到方法數組中 const subscribe = (listener) => listeners.push(listener); const getState = () => state; // 當store調用dispatch的改變數據的時候遍歷listeners數組,執行其中每一個方法,到達監聽數據重新渲染頁面的效果 const dispatch = (action) => { stateChanger(state, action); listeners.forEach((listener) => listener()) }; return { getState, dispatch, subscribe } }
再次修改上一部分的代碼如下:
// 首次渲染頁面 renderApp(store.getState()); // 監聽數據變化重新渲染頁面 store.subscribe(()=>{ renderApp(store.getState()); }); // 修改標題文本 store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '換一個標題' }); // 修改標題顏色 store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'grey' });
我們在首次渲染頁面後只需要subscribe一次,後面dispatch修改數據,renderApp方法會被重新調用,實現了監聽數據自動渲染數據的效果。
三.生成一個共用結構的對象來提高頁面的性能
上一節我們每次調用renderApp方法的時候實際上是執行了renderTitle和renderContent方法,我們兩次都是dispatch修改的是title數據,可是renderContent方法也都被一起執行了,這樣執行了不必要的函數,有嚴重的性能問題,我們可以在幾個渲染函數上加上一些Log看看實際上是不是這樣的
function renderApp (appState) { console.log('render app...') ... } function renderTitle (title) { console.log('render title...') ... } function renderContent (content) { console.log('render content...') ... }
瀏覽器控制台列印如下:
解決方案是:我們在每個渲染函數執行之前對其傳入的數據進行一個判斷,判斷傳入的新數據和舊數據是否相同,相同就return不渲染,否則就渲染。
// 渲染函數 function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 沒有傳入,所以加了預設參數 oldAppState = {} if (newAppState === oldAppState) return; // 數據沒有變化就不渲染了 console.log('render app...'); renderTitle(newAppState.title, oldAppState.title); renderContent(newAppState.content, oldAppState.content); } function renderTitle (newTitle, oldTitle = {}) { if (newTitle === oldTitle) return; // 數據沒有變化就不渲染了 console.log('render title...'); const titleDOM = document.getElementById('title'); titleDOM.innerHTML = newTitle.text; titleDOM.style.color = newTitle.color; } function renderContent (newContent, oldContent = {}) { if (newContent === oldContent) return; // 數據沒有變化就不渲染了 console.log('render content...'); const contentDOM = document.getElementById('content') contentDOM.innerHTML = newContent.text; contentDOM.style.color = newContent.color; } ... let oldState = store.getState(); // 緩存舊的 state store.subscribe(() => { const newState = store.getState(); // 數據可能變化,獲取新的 state renderApp(newState, oldState); // 把新舊的 state 傳進去渲染 oldState = newState // 渲染完以後,新的 newState 變成了舊的 oldState,等待下一次數據變化重新渲染 })
...
以上代碼我們在subscribe的時候先用oldState緩存舊的state,在dispatch之後執行裡面的方法再次獲取新的state然後oldState和newState傳入到renderApp中,之後再用oldState保存newState。
好,我們打開瀏覽器看下效果:
控制台只列印了首次渲染的幾行日誌,後面兩次dispatch數據之後渲染函數都沒有執行。這說明oldState和newState相等了。
通過斷點調試,發現newAppState和oldAppState是相等的。
究其原因,因為對象和數組是引用類型,newState,oldState指向同一個state對象地址,在每個渲染函數判斷始終相等,就return了。
解決方法:appState和newState其實是兩個不同的對象,我們利用ES6語法來淺複製appState對象,當執行dispatch方法的時候,用一個新對象覆蓋原來title裡面內容,其餘的屬性值保持不變。形成一個共用數據對象,可以參考以下一個demo:
我們修改stateChanger,
讓它修改數據的時候,並不會直接修改原來的數據 state,而是產生上述的共用結構的對象:
function stateChanger (state, action) { switch (action.type) { case 'UPDATE_TITLE_TEXT': return { // 構建新的對象並且返回 ...state, title: { ...state.title, text: action.text } } case 'UPDATE_TITLE_COLOR': return { // 構建新的對象並且返回 ...state, title: { ...state.title, color: action.color } } default: return state // 沒有修改,返回原來的對象 } }
因為stateChanger不會修改原來的對象了,而是返回一個對象,所以修改createStore裡面的dispatch方法,執行stateChanger(state,action)的返回值來覆蓋原來的state,這樣在subscribe執行傳入的方法在dispatch調用時,newState就是stateChanger()返回的結果。
function createStore (state, stateChanger) { ... const dispatch = (action) => { state=stateChanger(state, action); listeners.forEach((listener) => listener()) }; return { getState, dispatch, subscribe } }
再次運行代碼打開瀏覽器:
發現後兩次store.dispatch導致的content重新渲染不存在了,優化了性能。
四.通用化Reducer
appState是可以合併到一起的
function stateChanger (state, action) { if(state){ return { title: { text: '這是一個標題', color: 'Red' }, content: { text: '這是一段內容', color: 'blue' } } } switch (action.type) { case 'UPDATE_TITLE_TEXT': return { // 構建新的對象並且返回 ...state, title: { ...state.title, text: action.text } } case 'UPDATE_TITLE_COLOR': return { // 構建新的對象並且返回 ...state, title: { ...state.title, color: action.color } } default: return state // 沒有修改,返回原來的對象 } }
再修改createStore方法:
function createStore (stateChanger) { let state = null; const listeners = []; // 空的方法數組 // store調用一次subscribe就把傳入的listener方法push到方法數組中 const subscribe = (listener) => listeners.push(listener); const getState = () => state; // 當store調用dispatch的改變數據的時候遍歷listeners數組,執行其中每一個方法,到達監聽數據重新渲染頁面的效果 const dispatch = (action) => { state=stateChanger(state, action); listeners.forEach((listener) => listener()) }; dispatch({}); //初始化state return { getState, dispatch, subscribe } }
初始化一個局部變數state=null,最後手動調用一次dispatch({})來初始化數據。
stateChanger這個函數也可以叫通用的名字:reducer。為什麼叫reducer? 參考阮一峰的《redux基本用法》裡面對reducder的講解;
五:Redux總結
以上是根據閱讀《React.js小書》再次復盤,通過以上我們由一個簡單的例子引入用原生JS能大概的從零到一完成了Redux,具體的使用步驟如下:
// 定一個 reducer function reducer (state, action) { /* 初始化 state 和 switch case */ } // 生成 store const store = createStore(reducer) // 監聽數據變化重新渲染頁面 store.subscribe(() => renderApp(store.getState())) // 首次渲染頁面 renderApp(store.getState()) // 後面可以隨意 dispatch 了,頁面自動更新 store.dispatch(...)
按照定義reducer->生成store->監聽數據變化->dispatch頁面自動更新。
下麵兩幅圖也能很好表達出Redux的工作流程
使用Redux遵循的三大原則:
1.唯一的數據源store
2.保持狀態的store只讀,不能直接修改應用狀態
3.應用狀態的修改通過純函數Reducer完成
當然不是每個項目都要使用Redux,一些小心共用數據較少的沒必要使用Redux,視項目大小複雜度而定,具體什麼時候使用?引用一句話:當你不確定是否使用Redux的時候,那就不要用Redux。
項目完整代碼地址make-redux
六.寫在最後
每一個工具或框架都是在一定的條件下為瞭解決某種問題產生的,在閱讀幾遍《React.js》小書之後,終於對React,Redux等一些基本原理有了一些瞭解,深感作為一個coder,不能只CV,記憶一些框架API會用就行,知其然不可,更要知其所以然,這樣我們在完成項目才能更好的優化又能,是代碼寫的更加優雅。有什麼錯誤的地方,敬請指正,技術想要有質的飛躍,就要多學習,多思考,多實踐,與君共勉。
參考資料:
2.React進階之路-徐超