前言html
最近利用業餘時間閱讀了鬍子大哈寫的《React小書》,從基本的原理講解了React,Redux等等受益頗豐。眼過千遍不如手寫一遍,跟着做者的思路以及參考代碼能夠實現基本的Demo,下面根據本身的理解和參考一些資料,用原生JS從零開始實現一個Redux架構。前端
一.Redux基本概念react
常常用React開發的朋友可能很熟悉Redux,React-Redux,這裏告訴你們的是,Redux和React-Redux並非一個東西,Redux是一種架構模式,2015年,Redux出現,將 Flux 與函數式編程結合一塊兒,很短期內就成爲了最熱門的前端架構。它不關心你使用什麼庫,能夠把它和React,Vue或者JQuery結合。git
二.由一個簡單的例子開始github
咱們從一個簡單的例子開始推演,新建一個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),打開頁面:redux
這些寫雖然沒有什麼問題,可是存在一個比較大的隱患,每一個人均可以修改共享狀態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進階之路-徐超