用原生JS從零到一實現Redux架構

前言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會用就行,知其然不可,更要知其因此然,這樣咱們在完成項目才能更好的優化又能,是代碼寫的更加優雅。有什麼錯誤的地方,敬請指正,技術想要有質的飛躍,就要多學習,多思考,多實踐,與君共勉。

 


 

參考資料:

1.《React.js小書》-鬍子大哈

2.React進階之路-徐超

3.Redux 入門教程(一):基本用法-阮一峯

相關文章
相關標籤/搜索