顫抖吧!一起手寫一個redux框架!

Colin_Mindset發表於2019-03-06

redux是一個前端架構,經常和react一起使用。你要用react.js基本上都要用到redux和react-redux,但這兩者並不是一個東西!

  • redux是一個前端框架,你可以把它用到react、vue,設定jquery。
  • react-redux是把redux這個前端架構結合到react形成的庫,就是redux架構在react中的體現。

話不多說,我們來從頭手寫一個redux框架。

create-react-app新建一個專案myRedux,修改public/index.html裡的body結構為:

 <body>
    <div id='title'></div>
    <div id='content'></div>
  </body>

src/index.js裡的程式碼替換為如下程式碼,代表我們的應用狀態:

const appState = {
  title: {
    text: 'React.js 小書',
    color: 'red',
  },
  content: {
    text: 'React.js 小書內容',
    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方法會呼叫renderTitlerenderContent,而這兩個方法會把appState的資料渲染到頁面上。


執行截圖貼在這裡


一. 手寫dispatch

這是一個很簡單的頁面,但是存在著嚴重的隱患:我們渲染頁面時,用到了一個共享資料appState,每個人都在哪裡都可以修改它。如果在渲染之前做了一系列其他操作:

loadDataFromServer()
doSomethingUnexpected()
doSomthingMore()
// ...
renderApp(appState)

你根本無法知道這些方法會對renderApp做什麼修改。這種任何地方都可以對共享資料進行修改的寫法,會給debug/開發造成極大的難度。
有過一定開發經驗的朋友看到這裡,一定會忍不住對修改共享資料的操作做一個收口,讓所有對共享資料修改的操作統一收口在一起。
原來各個模組可以直接修改appState,如下圖:
在這裡插入圖片描述
而現在要寫更改appState,必須通過dispatch,如下圖:
在這裡插入圖片描述
我們定義一個收口函式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,這個action是一個普通的JavaScript物件,裡面必須包含一個type指明你想幹什麼。dispatchswitch裡會去識別這個type欄位,然後對appState進行相應的修改。
上面的dispatch只能識別兩種操作:一種是'UPDATE_TITLE_TEXT'會用actiontext欄位去更新appState.title.text;一種是'UPDATE_TITLE_COLOR'會用actioncolor欄位去更新appState.title.color。可以看到,action裡除了type欄位,其他都是可以自定義的。

二. 手寫store

上面我們有了appStatedispatch
現在我們進一步整合,把它們都集中在一個地方,並給這個地方起名叫store,然後建立一個createStore方法來構建store

function createStore (state, stateChanger) {
  const getState = () => state
  const dispatch = (action) => stateChanger(state, action)
  return { getState, dispatch }
}

createStore接受兩個引數,一個表示app的狀態state,一個用來修改state。
現在,我們來修改原來的程式碼:

let appState = {
  title: {
    text: 'React.js 小書',
    color: 'red',
  },
  content: {
    text: 'React.js 小書內容',
    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: '《React.js 小書》' }) // 修改標題文字
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色
renderApp(store.getState()) // 把新的資料渲染到頁面上

針對不同的app,我們給createStore傳入初始狀態appState和用於描述appState變化的stateChanger。需要修改的資料的時候通過store.dispatch,需要獲取資料的時候通過store.getState

監控資料的變化

上面的程式碼有一個問題,在dispatch修改資料的時候,其實只是資料發生了變化,並沒有呼叫renderApp方法,頁面上的內容是不會變化的。然而,我們又不能每次dispatch的時候又renderApp,我們希望用一種通用的監聽資料變化的方式,然後重新渲染頁面。

function createStore (state, stateChanger) {
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    stateChanger(state, action)
    listeners.forEach((listener) => listener())
  }
  return { getState, dispatch, subscribe }
}

我們在createStore中定義一個陣列listeners,並對外提供一個subscribe方法,可以用該方法給陣列push一個渲染函式,每當dispatch的時候,listeners裡的渲染函式都會被呼叫。這樣我們就可以在資料變化的時候重新渲染頁面:

const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState()))

renderApp(store.getState()) // 首次渲染頁面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小書》' }) // 修改標題文字
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色

三. 手寫reducer

細心的朋友會發現,前面的程式碼有著嚴重的效能問題。我們在每個渲染函式前打下log:

function renderApp (appState) {
  console.log('render app...')
  renderTitle(appState.title)
  renderContent(appState.content)
}

function renderTitle (title) {
  console.log('render title...')
  const titleDOM = document.getElementById('title')
  titleDOM.innerHTML = title.text
  titleDOM.style.color = title.color
}

function renderContent (content) {
  console.log('render content...')
  const contentDOM = document.getElementById('content')
  contentDOM.innerHTML = content.text
  contentDOM.style.color = content.color
}

我們接下來dispatch兩個action,來修改title的color和text:

const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState())) // 監聽資料變化

renderApp(store.getState()) // 首次渲染頁面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: '《React.js 小書》' }) // 修改標題文字
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改標題顏色

可以在控制檯看到:
在這裡插入圖片描述
整個頁面都重新整理了三遍!第一次是頁面初始化,後面兩次修改title的狀態的操作,把整個頁面都重新整理了!
但其實,我們只希望讓跟修改的資料(title)有關係的元件重新整理。
這裡給出一個解決方案:
每次渲染之前,先對新資料和舊資料做一下比較,把不一樣的部分進行渲染。

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
}

我們修改下stateChanger,讓它接收到action後不是直接去修改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

function createStore (state, stateChanger) {
  const listeners = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = stateChanger(state, action) // 覆蓋原物件
    listeners.forEach((listener) => listener())
  }
  return { getState, dispatch, subscribe }
}

此時,我們再去執行剛才的程式碼:
在這裡插入圖片描述
我們就這樣成功優化了頁面效能,每次只重新整理了修改過的資料對應的元件。
此時,我們的stateChanger還有沒有優化的空間了?
其實,可以把appState和stateChanger合併到一起:

function stateChanger (state, action) {
  if (!state) {
    return {
      title: {
        text: 'React.js 小書',
        color: 'red',
      },
      content: {
        text: 'React.js 小書內容',
        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 = []
  const subscribe = (listener) => listeners.push(listener)
  const getState = () => state
  const dispatch = (action) => {
    state = stateChanger(state, action)
    listeners.forEach((listener) => listener())
  }
  dispatch({}) // 初始化 state
  return { getState, dispatch, subscribe }
}

此時,這個stateChanger,就是reduxreducer了!

四. 總結

此時,我們已經完整地手寫了一個redux框架。
redux的核心就是發明了store,通過dispatch一個action來更改store裡的值。

五. 參考

https://imweb.io/topic/59f5a7fdb72024f03c7f49bc
http://huziketang.mangojuice.top/books/react/lesson35

相關文章