從零一步一步實現一個的Redux

前言

redux是什麼?javascript

redux是一個狀態管理器css

簡單的狀態管理器

redux是一個狀態管理器,那麼狀態是什麼?java

狀態就是數據,狀態管理器也就是數據管理器。那麼咱們來實現一個簡單的狀態管理器吧。css3

1. 狀態管理器得有一個存儲狀態的地方吧,咱們既然叫狀態管理器,那就定義一個state變量吧,做爲存儲狀態的地方。
let state = {
  name: '混沌傳奇',
}
//使用下狀態
console.log(state.name)
複製代碼
2. 有了存儲狀態的地方,那麼得有修改狀態的方法,咱們就起名爲updateState吧。
let state = {
  name: '混沌傳奇',
}
//使用下狀態
console.log(state.name) // 輸出:'混沌傳奇'

const updateState = (newName) => {
  state.name = newName
}
//更新下狀態
updateState('混沌傳奇2')
//再打印下狀態
console.log(state.name) //輸出:'混沌傳奇2'
複製代碼
3. 有了存儲狀態的地方和修改狀態的方法,感受還缺點什麼東西。咱們思考下,狀態修改了後,若是使用狀態的地方不知道狀態已經變化了,那麼使用的狀態的地方實際上使用的仍是舊狀態,咱們目前沒有辦法告知使用狀態的地方狀態已經更新了。

好,問題有了,那麼咱們就解決問題,咱們可使用發佈-訂閱模式來解決這個問題。git

let listeners = []
let state = {
  name: '混沌傳奇',
}
//使用下狀態
console.log(state.name) // 輸出:'混沌傳奇'

const updateState = (newName) => {
  state.name = newName
  listeners.forEach(listerner => listerner())
}

const subscribe = (listener) => {
  listeners.push(listener)
}

//訂閱下狀態
subscribe(() => {
  //在state更新的時候,打印下state.name
  console.log(`最新的name是:${state.name}`) //輸出:'最新的name是:混沌傳奇2'
})

//更新下狀態
updateState('混沌傳奇2')
//再打印下狀態
console.log(state.name) //輸出:'混沌傳奇2'
複製代碼
4. 解決了state數據的訂閱問題,咱們再看下已經實現的這個簡單的狀態管理器,是否是更新狀態的updateState方法只能更新state.name,咱們指望更新狀態的updateState方法能夠更新state中存儲的全部數據,不僅是name。咱們來解決下這個問題。
let listeners = []
let state = {
  name: '',
  like: '',
}
const updateState = (newState) => {
  state = newState
  listeners.forEach(listerner => listerner())
}

const subscribe = (listener) => {
  listeners.push(listener)
}
複製代碼

好了,咱們升級了下updateState方法,咱們試用下咱們新的狀態管理器github

let listeners = []
let state = {
  name: '',
  like: '',
}
const updateState = (newState) => {
  state = newState
  listeners.forEach(listerner => listerner())
}

const subscribe = (listener) => {
  listeners.push(listener)
}

subscribe(() => {
  //在state更新的時候,打印下state
  console.log(state)
})
updateState({
  ...state,
  name: '混沌傳奇'
})
updateState({
  ...state,
  like: '打乒乓球'
})
複製代碼

輸出依次爲:編程

> {name: "混沌傳奇", like: ""}

> {name: "混沌傳奇", like: "打乒乓球"}
複製代碼
5. 升級了updateState方法以後,咱們把咱們的狀態管理器再封裝一下吧,如今的listenersstate都暴露在外面,容易被別人更改
const createStore = (initState) => {
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const updateState = (newState) => {
    state = newState
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  return {
    getState,
    updateState,
    subscribe,
  }
}
複製代碼

咱們來使用這個狀態管理器來管理下更多的數據試試:redux

const createStore = (initState) => {
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const updateState = (newState) => {
    state = newState
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  return {
    getState,
    updateState,
    subscribe,
  }
}


let initState = {
  name: '',
  sex: '',
  age: '',
  like: '',
  friend: '',
}
let { getState, updateState, subscribe } = createStore(initState)

subscribe(() => {
  console.log(getState())
})

updateState({
  ...getState(),
  name: '混沌傳奇',
})

updateState({
  ...getState(),
  sex: '男',
})

updateState({
  ...getState(),
  age: '25',
})

updateState({
  ...getState(),
  like: '打羽毛球',
})

updateState({
  ...getState(),
  friend: '阿龍',
})
複製代碼

運行代碼,輸出依次爲:app

> {name: "混沌傳奇", sex: "", age: "", like: "", friend: ""}

> {name: "混沌傳奇", sex: "男", age: "", like: "", friend: ""}

> {name: "混沌傳奇", sex: "男", age: "25", like: "", friend: ""}

> {name: "混沌傳奇", sex: "男", age: "25", like: "打羽毛球", friend: ""}

> {name: "混沌傳奇", sex: "男", age: "25", like: "打羽毛球", friend: "阿龍"}
複製代碼

到這裏咱們完成了一個簡單的狀態管理器。 固然離真正的redux還差很遠,下面咱們繼續完善咱們的狀態管理器。ide

有計劃的狀態管理器

咱們用上面咱們實現的狀態管理器來實現一個自增自減程序吧,指望count是一個自增|自減的number類型的數值:

const createStore = (initState) => {
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const updateState = (newState) => {
    state = newState
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  return {
    getState,
    updateState,
    subscribe,
  }
}



let initState = {
  count: 0
}
let { getState, updateState, subscribe } = createStore(initState)

console.log(getState())

subscribe(() => {
  console.log(getState())
})

//自增
updateState({
  ...getState(),
  count: getState().count+1,
})

//自減
updateState({
  ...getState(),
  count: getState().count-1,
})

//隨便改下
updateState({
  ...getState(),
  count: '傻了吧',
})
複製代碼

運行程序,輸出依次爲:

> {count: 0}

> {count: 1}

> {count: 0}

> {count: "傻了吧"}
複製代碼

從輸出咱們能夠看出,count被修改爲了字符串類型,咱們指望是countnumber類型,解決這個問題的辦法就是,咱們修改狀態的時候,來按照指望來修改狀態。

怎麼讓修改狀態的時候,按照咱們指望來修改呢?

咱們能夠這樣:

1. 咱們定義一個狀態修改行爲(action
2. 咱們制定一個狀態修改計劃(reducer),reducer會接收上次更新後的stateupdateState傳遞進來的最新的actionreducer根據action來修改state,修改完後,返回最新的stateupdateState
3. updateState方法接收action,把action告訴reducer......

根據這3點,咱們來修改下咱們的狀態管理器

const createStore = (reducer, initState) => {
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const updateState = (action) => {
    state = reducer(state, action)
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  return {
    getState,
    updateState,
    subscribe,
  }
}
複製代碼

咱們來嘗試使用下新的 狀態管理器 來實現自增和自減

const createStore = (reducer, initState) => {
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const updateState = (action) => {
    state = reducer(state, action)
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  return {
    getState,
    updateState,
    subscribe,
  }
}



let initState = {
  count: 0
}
let reducer = (state, action) => {
  switch (action.type) {
    case 'INCREMENT':
      return {
        ...state,
        count: state.count+1,
      }
    case 'DECREMENT':
      return {
        ...state,
        count: state.count-1,
      }
    default:
      return state
  }
}
let { getState, updateState, subscribe } = createStore(reducer, initState)

console.log(getState())

subscribe(() => {
  console.log(getState())
})

//自增
updateState({
  type: 'INCREMENT',
})

//自減
updateState({
  type: 'DECREMENT',
})

//隨便改下
updateState({
  count: '傻了吧',
})
複製代碼

運行程序,輸出依次爲:

> {count: 0}

> {count: 1}

> {count: 0}

> {count: 0}
複製代碼

咱們能夠看到,打印輸出沒有了{count: "傻了吧"},改成了{count: 0}

爲了更加像真正的redux,咱們再改下咱們的狀態管理器,咱們把updateState改成dispatch

const createStore = (reducer, initState) => {
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  return {
    getState,
    dispatch,
    subscribe,
  }
}
複製代碼

到這裏爲止,咱們已經實現了一個有計劃的狀態管理器!

可以支撐大量數據管理的狀態管理器

前面咱們管理的數據都比較少,咱們如今嘗試把管理的數據增長一些:

const createStore = (reducer, initState) => {
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  return {
    getState,
    dispatch,
    subscribe,
  }
}



let initState = {
  studyPerson: {
    name: '',
    age: '',
    like: [],
    sex: '',
    readedBooks: [],
  },
  playfulPerson: {
    name: '',
    age: '',
    like: [],
    sex: '',
    readedBooks: [],
  },
  books: [],
}
let reducer = (state, action) => {
  switch (action.type) {
    case 'SET_STUDY_PERSON':
      return {
        ...state,
        studyPerson: action.data,
      }
    case 'SET_PLAYFUL_PERSON':
      return {
        ...state,
        playfulPerson: action.data,
      }
    case 'SET_BOOKS':
      return {
        ...state,
        books: action.data,
      }
    default:
      return state
  }
}
let { getState, dispatch, subscribe } = createStore(reducer, initState)
//設置圖書館擁有的圖書
dispatch({
  type: 'SET_BOOKS',
  data: [
    {
      id: 1,
      name: 'javascript高級程序設計',
      kind: '計算機編程',
    },
    {
      id: 2,
      name: '圖解css3',
      kind: '計算機編程',
    },
    {
      id: 3,
      name: 'javascript函數式編程',
      kind: '計算機編程',
    },
    {
      id: 4,
      name: '三國演義',
      kind: '小說',
    },
    {
      id: 5,
      name: '籃球投籃技巧',
      kind: '運動類',
    },
  ],
})
//設置學習者信息
dispatch({
  type: 'SET_STUDY_PERSON',
  data: {
    name: '混沌傳奇',
    age: 25,
    like: ['學習計算機知識', '打羽毛球'],
    sex: '男',
    readedBooks: [1, 2, 3],
  },
})
//設置愛玩者信息
dispatch({
  type: 'SET_PLAYFUL_PERSON',
  data: {
    name: '阿龍',
    age: 28,
    like: ['看綜藝', '看小說', '看雜誌'],
    sex: '男',
    readedBooks: [4, 5],
  },
})
console.log(getState())
複製代碼

從這段代碼,咱們能夠看出,隨着管理的數據量的增長,咱們在reducer中寫的邏輯就愈來愈多,並且,每次dispatch執行的時候,傳給dispatchaction內容太多,閱讀起來有點麻煩,看到的是一大坨代碼。

怎麼優化一下呢?

咱們能夠這樣優化:

1. 把reducer拆分紅多個reducer,每一個子state對應一個reducerstudyPersonplayfulPersonbooks都是子state
2. 把每一個子state對應的reducer合併成一個rootReducer,而後把rootReducer傳遞給createStore
3. 每一個action能夠由actionCreater生成,把action歸類到不一樣的js文件中,每一個js文件export出各個action對應的actionCreater
//文件 ./redux.js
const createStore = (reducer, initState) => {
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  return {
    getState,
    dispatch,
    subscribe,
  }
}
export {
  createStore,
}

//文件 ./reducers.js
import {
  SET_STUDY_PERSON, 
  SET_PLAYFUL_PERSON, 
  SET_BOOKS,
} from './actions.js'
let studyPersonReducer = (state, action) => {
  switch (action.type) {
    case SET_STUDY_PERSON:
      return action.data
    default:
      return state
  }
}
let playfulPersonReducer = (state, action) => {
  switch (action.type) {
    case SET_PLAYFUL_PERSON:
      return action.data
    default:
      return state
  }
}
let booksReducer = (state, action) => {
  switch (action.type) {
    case SET_BOOKS:
      return action.data
    default:
      return state
  }
}
export {
  studyPersonReducer,
  playfulPersonReducer,
  booksReducer,
}

//文件 ./actions.js
export const SET_STUDY_PERSON = 'SET_STUDY_PERSON'
export const SET_PLAYFUL_PERSON = 'SET_PLAYFUL_PERSON'
export const SET_BOOKS = 'SET_BOOKS'
export const setStudyPerson = (data) => {
  return {
    type: SET_STUDY_PERSON,
    data,
  }
}
export const setPlayfulPerson = (data) => {
  return {
    type: SET_PLAYFUL_PERSON,
    data,
  }
}
export const setBook = (data) => {
  return {
    type: SET_BOOKS,
    data,
  }
}

//文件 ./index.js
import { createStore } from './redux.js'
import {
  studyPersonReducer,
  playfulPersonReducer,
  booksReducer,
} from './reducers.js'
import {
  setStudyPerson,
  setPlayfulPerson,
  setBook,
} from './actions.js'

let initState = {
  studyPerson: {
    name: '',
    age: '',
    like: [],
    sex: '',
    readedBooks: [],
  },
  playfulPerson: {
    name: '',
    age: '',
    like: [],
    sex: '',
    readedBooks: [],
  },
  books: [],
}
let rootReducer = (state, action) => {
  return {
    studyPerson: studyPersonReducer(state.studyPerson, action),
    playfulPerson: playfulPersonReducer(state.playfulPerson, action),
    books: booksReducer(state.books, action),
  }
}
let { getState, dispatch, subscribe } = createStore(rootReducer, initState)
//設置圖書館擁有的圖書
dispatch(setBook([
  {
    id: 1,
    name: 'javascript高級程序設計',
    kind: '計算機編程',
  },
  {
    id: 2,
    name: '圖解css3',
    kind: '計算機編程',
  },
  {
    id: 3,
    name: 'javascript函數式編程',
    kind: '計算機編程',
  },
  {
    id: 4,
    name: '三國演義',
    kind: '小說',
  },
  {
    id: 5,
    name: '籃球投籃技巧',
    kind: '運動類',
  },
]))
//設置學習者信息
dispatch(setStudyPerson({
  name: '混沌傳奇',
  age: 25,
  like: ['學習計算機知識', '打羽毛球'],
  sex: '男',
  readedBooks: [1, 2, 3],
}))
//設置愛玩者信息
dispatch(setPlayfulPerson({
  name: '阿龍',
  age: 28,
  like: ['看綜藝', '看小說', '看雜誌'],
  sex: '男',
  readedBooks: [4, 5],
}))
console.log(getState())
複製代碼

拆分完reducer,而且把action按類劃分到單獨的js文件中後,咱們的index.js看起來要稍微簡略一些了,可是好像仍是有不少代碼。咱們看看還有能夠封裝的地方嗎?

仔細看咱們剛纔實現的代碼,初始化state的代碼佔用了一大坨區域,合併子reducer的代碼好像也能夠抽離爲工具方法

1. 咱們先來抽離合並子reducer的代碼

前面咱們實現的合併子reducer的代碼以下:

...
let rootReducer = (state, action) => {
  return {
    studyPerson: studyPersonReducer(state.studyPerson, action),
    playfulPerson: playfulPersonReducer(state.playfulPerson, action),
    books: booksReducer(state.books, action)
  }
}
...
複製代碼

能夠看到咱們是定義了一個rootReducer函數,函數返回一個state對象,對象每一個屬性的值對應一個子reducer的執行,子reducer執行完畢返回的就是屬性對於的子state值。

咱們想一想一下,咱們是否能夠實現一個方法(方法就叫combineReducers),這個方法只接收子reducer,返回一個rootReducer,這樣咱們只須要調用這個方法,就能夠生成rootReducer了,好比:

const rootReducer = combineReducers({
    studyPerson: studyPersonReducer,
    playfulPerson: playfulPersonReducer,
    books: booksReducer,
})
複製代碼

咱們嘗試實現下combineReducers函數:

const combineReducers = (reducers) => {
  const reducerKeys = Object.keys(reducers)
  return (state, action) => {
    let newState = {}
    reducerKeys.forEach(key => {
      newState[key] = reducers[key](state[key], action)
    })
    return newState
  }
}
複製代碼
2. 拆分和合並state

咱們把reducer拆分爲一個一個的子reducer,經過 combineReducers合併了起來。可是還有個問題,state 咱們仍是寫在一塊兒的,這樣會形成state樹很龐大,不直觀,很難維護。咱們須要拆分,一個state,一個 reducer寫一塊。

咱們先修改下reducers.js:

//文件 ./reducers.js
import {
  SET_STUDY_PERSON, 
  SET_PLAYFUL_PERSON, 
  SET_BOOKS 
} from './actions.js'

let initStudyPerson = {
  name: '',
  age: '',
  like: [],
  sex: '',
  readedBooks: [],
}
let studyPersonReducer = (state = initStudyPerson, action) => {
  switch (action.type) {
    case SET_STUDY_PERSON:
      return action.data
    default:
      return state
  }
}
let initPlayfulPerson = {
  name: '',
  age: '',
  like: [],
  sex: '',
  readedBooks: [],
}
let playfulPersonReducer = (state = initPlayfulPerson, action) => {
  switch (action.type) {
    case SET_PLAYFUL_PERSON:
      return action.data
    default:
      return state
  }
}
let booksReducer = (state = [], action) => {
  switch (action.type) {
    case SET_BOOKS:
      return action.data
    default:
      return state
  }
}
export {
  studyPersonReducer,
  playfulPersonReducer,
  booksReducer,
}
複製代碼

再修改下redux.jscreateStore中增長一行 dispatch({ type: Symbol() }),把combineReducers方法放在redux.js文件中

//文件 ./redux.js
const createStore = (reducer, initState) => {
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  //注意:Symbol() 不跟任何值相等,因此 type 爲 Symbol(),則 reducer 不匹配任何 type,走 default 邏輯,返回初始化 state 值,這樣就達到了初始化 state 的效果。
  dispatch({ type: Symbol() })

  return {
    getState,
    dispatch,
    subscribe,
  }
}
const combineReducers = (reducers) => {
  const reducerKeys = Object.keys(reducers)
  return (state, action) => {
    let newState = {}
    reducerKeys.forEach(key => {
      newState[key] = reducers[key](state[key], action)
    })
    return newState
  }
}
export {
  createStore,
  combineReducers,
}
複製代碼

本小節完整源碼見 demo-1

到這裏爲止,咱們的redux已經實現的差很少啦!

中間件 middleware

什麼是Redux中間件?

Redux的中間件能夠理解爲是對dispatch方法的包裝,加強dispatch的功能。

1. 嘗試包裝下dispatch

假如咱們須要在每次redux修改state的時候,打印下修改前的state和修改後的state以及觸發修改stateaction

咱們能夠暫時這麼包裝下dispatch

import { createStore, combineReducers } from './redux.js'

const rootReducer = combineReducers({
    count: countReducer
})

const store = createStore(rootReducer);

const next = store.dispatch;

store.dispatch = (action) => {
  console.log('previous state:', store.getState())
  next(action)
  console.log('next state:', store.getState())
  console.log('action:', action)
}

store.dispatch({
  type: 'INCREMENT'
});
複製代碼

咱們來運行下代碼,輸出結果是:

> previous state: {count: 0}

> next state: {count: 0}

> action: {type: "INCREMENT"}
複製代碼

從輸出結果能夠看出,咱們實現了對dispatch的加強。在改變state的時候,正確的打印出了改變state前的state值,改變後的state的值,以及觸發state改變的action

2. 多中間件組合包裝dispatch

上面咱們實現了一個加強dispatch需求,若是如今咱們在原有需求的基礎上,又有了一個新的加強需求。好比:我想在每次state改變的時候,打印下當前時間。

嘗試實現下代碼:

const store = createStore(rootReducer);

const next = store.dispatch;

store.dispatch = (action) => {
  console.log('state change time:', new Date().toLocaleString())
  console.log('previous state:', store.getState())
  next(action)
  console.log('next state:', store.getState())
  console.log('action:', action)
}
複製代碼

若是咱們,如今又有新的加強需求了,要記錄每次改變state時出錯的緣由,咱們再改下加強代碼:

const store = createStore(rootReducer);

const next = store.dispatch;

store.dispatch = (action) => {
  try {
    console.log('state change time:', new Date().toLocaleString())
    console.log('previous state:', store.getState())
    next(action)
    console.log('next state:', store.getState())
    console.log('action:', action)
  } catch (e) {
    console.log('錯誤信息:', e)
  }
}
複製代碼

若是又雙叒叕有新的加強需求了呢。。。。。。

是否是一直這麼該加強代碼,很麻煩,到時候dispatch函數會愈來愈龐大,很難維護。因此這個方式不可取,咱們要考慮如何實現擴展性更強的多中間件包裝dispatch的方法。

  1. 把不一樣的加強需求,抽離成單個的函數:
const store = createStore(rootReducer);

const next = store.dispatch;

const loggerMiddleware = (action) => {
  console.log('previous state:', store.getState())
  next(action)
  console.log('next state:', store.getState())
  console.log('action:', action)
}

const updateTimeMiddleware = (action) => {
  console.log('state change time:', new Date().toLocaleString())
  loggerMiddleware(action)
}

const exceptionMiddleware = (action) => {
  try {
    updateTimeMiddleware(action)
  } catch (e) {
    console.log('錯誤信息:', e)
  }
}

store.dispatch = exceptionMiddleware
複製代碼

這樣,看起來是把各個加強需求都抽離成單個的函數了,可是都不是純函數,對外部數據有依賴,這樣很不利於擴展。

咱們來把這些加強函數都改爲純函數吧。

咱們分兩步來改造加強函數: (1)把加強函數內調用的加強函數,都抽離出來,做爲參數傳遞進加強函數:

const store = createStore(rootReducer);

const next = store.dispatch;

const loggerMiddleware = (next) => (action) => {
  console.log('previous state:', store.getState())
  next(action)
  console.log('next state:', store.getState())
  console.log('action:', action)
}

const updateTimeMiddleware = (next) => (action) => {
  console.log('state change time:', new Date().toLocaleString())
  next(action)
}

const exceptionMiddleware = (next) => (action) => {
  try {
    next(action)
  } catch (e) {
    console.log('錯誤信息:', e)
  }
}

store.dispatch = exceptionMiddleware(updateTimeMiddleware(loggerMiddleware(next)))
複製代碼

(2)把加強函數內對store的調用抽離出來,做爲參數傳遞進加強函數:

const store = createStore(rootReducer);

const next = store.dispatch;

const loggerMiddleware = (store) => (next) => (action) => {
  console.log('previous state:', store.getState())
  next(action)
  console.log('next state:', store.getState())
  console.log('action:', action)
}

const updateTimeMiddleware = (store) => (next) => (action) => {
  console.log('state change time:', new Date().toLocaleString())
  next(action)
}

const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action)
  } catch (e) {
    console.log('錯誤信息:', e)
  }
}

const logger = loggerMiddleware(store)
const updateTime = updateTimeMiddleware(store)
const exception = exceptionMiddleware(store)

store.dispatch = exception(updateTime(logger(next)))
複製代碼

到這裏爲止,咱們真正的實現了可擴展的、獨立的、純的加強函數(加強函數即redux中間件),而且多個加強函數能夠很方便的組合使用。

3. 優化中間件的使用方式

咱們上面已經實現了可擴展、獨立的、純的中間件,可是咱們最後還須要執行

const logger = loggerMiddleware(store)
const updateTime = updateTimeMiddleware(store)
const exception = exceptionMiddleware(store)
複製代碼

來生成loggerupdateTimeexception, 而且執行

store.dispatch = exception(updateTime(logger(next)))
複製代碼

來覆蓋storedispatch方法。

其實咱們不必關心中間件怎麼使用,咱們只須要知道有三個中間件,剩下的細節都封裝起來。

好比,咱們能夠經過包裝createStore來把中間件使用細節都封裝在包裝createStore的代碼內。

指望的用法:

/** * applyMiddleware 接收中間件,把中間件使用細節包裝在了 * applyMiddleware 內部,而後返回一個函數, * 函數接收舊的 createStore,返回新的 createStore */
const newCreateStore = applyMiddleware(exceptionMiddleware, updateTimeMiddleware, loggerMiddleware)(createStore);

// newCreateStore 返回了一個 dispatch 被重寫過的 store
const store = newCreateStore(reducer);
複製代碼

實現applyMiddleware:

const applyMiddleware = (...middlewares) => (oldCreateStore) => (reducer, initState) => {
  // 生成 store
  let store = oldCreateStore(reducer, initState)
  // 給每一個 middleware 傳遞進去 store,至關於 loggerMiddleware(store)
  // 爲了防止中間件修改 store 的其餘方法,咱們只暴露 store 的 getState 方法
  // 執行結果至關於 chain = [logger, updateTime, exception]
  let chain = middlewares.map(middleware => middleware({ getState: store.getState }))
  // 獲取 store 的 dispatch 方法
  let next = store.dispatch
  // 實現 exception(updateTime(logger(next)))
  chain.reverse().map(middleware => {
    next = middleware(next)
  })
  // 替換 store 的 dispatch 方法
  store.dispatch = next
  // 返回新的 store
  return store
}
複製代碼

咱們如今包裝createStore的代碼在createStore函數外面,爲了使用上的方便,咱們稍微修改下createStore函數,指望用法:

const rewriteCreateStoreFunc = applyMiddleware(exceptionMiddleware, updateTimeMiddleware, loggerMiddleware)

const store = createStore(reducer, initState, rewriteCreateStoreFunc)
複製代碼

createStore修改後的代碼以下:

const createStore = (reducer, initState, rewriteCreateStoreFunc) => {
  //若是有 rewriteCreateStoreFunc,那就生成新的 createStore
  if(rewriteCreateStoreFunc){
      const newCreateStore =  rewriteCreateStoreFunc(createStore);
      return newCreateStore(reducer, initState);
  }
  

  // 如下爲舊 createStore 邏輯
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
  }

  //注意:Symbol() 不跟任何值相等,因此 type 爲 Symbol(),則 reducer 不匹配任何 type,走 default 邏輯,返回初始化 state 值,這樣就達到了初始化 state 的效果。
  dispatch({ type: Symbol() })

  return {
    getState,
    dispatch,
    subscribe,
  }
}
複製代碼

本小節完整源碼見 demo-2

到這裏,咱們的redux中間件功能所有實現完了。

redux其餘功能

退訂

store.subscribe是用來訂閱state變化方法,既然有訂閱,那麼就存在取消訂閱的需求,咱們來實現下取消訂閱功能。

修改store.subscribe的實現以下:

const subscribe = (listener) => {
  listeners.push(listener)
  return () => {
    const index = listeners.indexOf(listener)
    listeners.splice(index, 1)
  }
}
複製代碼

使用方法以下:

// 訂閱
const unsubscribe = store.subscribe(() => {
  let state = store.getState();
  console.log(state);
});
// 退訂
unsubscribe();
複製代碼
createStore函數能夠省略initState參數

createStore容許省略initState參數,好比這樣:

let store = createStore(rootReducer, rewriteCreateStoreFunc)
複製代碼

修改下咱們的createStore方法,兼容省略initState參數的用法:

const createStore = (reducer, initState, rewriteCreateStoreFunc) => {
  // 兼容省略參數 initState
  if (typeof initState === 'function') {
    rewriteCreateStoreFunc = initState
    initState = undefined
  }

  // 若是有 rewriteCreateStoreFunc,那就生成新的 createStore
  if(rewriteCreateStoreFunc){
      const newCreateStore =  rewriteCreateStoreFunc(createStore);
      return newCreateStore(reducer, initState);
  }
  

  // 如下爲舊 createStore 邏輯
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
    return () => {
      const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

  // 注意:Symbol() 不跟任何值相等,因此 type 爲 Symbol(),則 reducer 不匹配任何 type,走 default 邏輯,返回初始化 state 值,這樣就達到了初始化 state 的效果。
  dispatch({ type: Symbol() })

  return {
    getState,
    dispatch,
    subscribe,
  }
}
複製代碼
replaceReducer方法

有時候,咱們的頁面作了按需加載,若是想把每一個按需加載的頁面的組件對應的reducer也按需加載,能夠經過replaceReducer來實現,好比:

// 初始頁面須要的 reducer
const reducer = combineReducers({
  books: booksReducer
});
const store = createStore(reducer);

// 按需加載的頁面1須要的 reducer,在頁面加載的時候再加載
// 而後,經過 combineReducers 生成新的 reducer
// 最後經過 store.replaceReducer 方法把新的 reducer 替換掉舊的 reducer
const nextReducer = combineReducers({
  playfulPerson: playfulPersonReducer,
  books: booksReducer,
});
store.replaceReducer(nextReducer);

// 按需加載的頁面2須要的 reducer,在頁面加載的時候再加載
// 而後,經過 combineReducers 生成新的 reducer
// 最後經過 store.replaceReducer 方法把新的 reducer 替換掉舊的 reducer
const nextReducer = combineReducers({
  studyPerson: studyPersonReducer,
  playfulPerson: playfulPersonReducer,
  books: booksReducer,
});
store.replaceReducer(nextReducer);
複製代碼

createStore函數內部新增replaceReducer方法,同時返回的store中包含replaceReducer方法,代碼實現以下:

const createStore = (reducer, initState, rewriteCreateStoreFunc) => {
  // 兼容省略參數 initState
  if (typeof initState === 'function') {
    rewriteCreateStoreFunc = initState
    initState = undefined
  }

  // 若是有 rewriteCreateStoreFunc,那就生成新的 createStore
  if(rewriteCreateStoreFunc){
      const newCreateStore =  rewriteCreateStoreFunc(createStore);
      return newCreateStore(reducer, initState);
  }
  

  // 如下爲舊 createStore 邏輯
  let listeners = []
  let state = initState || {}

  const getState = () => state

  const dispatch = (action) => {
    state = reducer(state, action)
    listeners.forEach(listerner => listerner())
  }

  const subscribe = (listener) => {
    listeners.push(listener)
    return () => {
      const index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

  function replaceReducer(nextReducer) {
    reducer = nextReducer
    // 把新的 reducer 的默認狀態更新到 state 樹上
    dispatch({ type: Symbol() })
  }

  // 注意:Symbol() 不跟任何值相等,因此 type 爲 Symbol(),則 reducer 不匹配任何 type,走 default 邏輯,返回初始化 state 值,這樣就達到了初始化 state 的效果。
  dispatch({ type: Symbol() })

  return {
    getState,
    dispatch,
    subscribe,
    replaceReducer,
  }
}
複製代碼

至此,我要實現的一個redux已經所有實現了,爲了縮短篇幅,省略了用的比較少的bindActionCreators方法的講解和實現,同時實現代碼中去掉了各類校驗代碼。

完整的示例源碼見 demo-3

最後

文中提到了純函數,那麼什麼是純函數呢?

簡單來講,就是相同的輸入,獲得的輸出永遠是同樣的,沒有任何反作用。

純函數屬於函數式編程中的一個概念。

給你們推薦一本js函數式編程指南的書,支持在線瀏覽函數式編程指南

相關文章
相關標籤/搜索