redux啟示與實踐

小雨心情發表於2018-12-21

redux三原則及data flow

  • 單一資料來源
  • 狀態只讀
  • 使用純函式來改變狀態

稍微注意一下,這些原則,在redux的實現中是一點也沒體現出來。

In a very real sense, each one of those statements is a lie!
      -- 摘自tao-of-redux

而這些原則是指導你如何使用redux

資料流

data_flow

counter demo

function counter(state = { num: 0 }, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { num: state.num + 1 }
    case 'DECREMENT':
      return { num: state.num - 1 }
    default:
      return state
  }
}
const store = createStore(counter)

store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'INCREMENT' })
store.dispatch({ type: 'DECREMENT' })
// { num: 1 }
console.log(store.getState())
複製程式碼

因為介紹redux的文章太多,這裡就略過了
建議看下官方文件的基礎部分

Action Creator

為什麼要使用action creator?
可以閱讀
基礎部分的actions#action-creators
技巧部分的reducing-boilerplate#action-creatorswhy-use-action-creators

示例見 常用工具的使用 -> redux-actions

Naive Implement

其實想一下,createStore建立出來的物件,無非包含幾個方法,dispatch, getState, subscribe...

createStore很好實現了

function createStore(reducer, preloadState) {
  let currentState = preloadState
  let listener = []

  function getState() {
    return currentState
  }

  function subscribe(listener) {
    listener.push(listener)
    return function unsubscribe() {
      let index = listeners.indexOf(listener)
      listeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    currentState = currentReducer(state, action)
    listeners.forEach(listener => listener())

    return action
  }

  return {
    dispatch,
    subscribe,
    getState
  }
}
複製程式碼

大概不到30行的程式碼,讓你想不到的是,redux的確就是類似這樣的方式來實現的。
你之前或許會想,應該搞一個factory建立一個類, 或者其他一些技巧儘量避開閉包。。。

所以有時候,寫程式碼的時候,真的不要想太多,在程式碼的"樣子"還沒有對效能,可讀性...產生影響的時候,越簡單越好

Middleware

官方文件的middleware的介紹

It provides a third-party extension point between dispatching an action, and the moment it reaches the reducer

其實它是一種裝飾者模式,一種可以動態新增功能的模式
詳細可以閱讀
JS 5種不同的方法實現裝飾者模式
js實現裝飾者模式,有幾種方法,為了配合文件,這裡說一下monkeypatch和middleware的方式

monkeypatch -- 猴補丁
為什麼叫猴補丁呢? 想了解可以搜尋一下

猴補丁怎麼體現在程式碼上呢? 在執行時替換方法、屬性等 當然,既然已經稱為模式,肯定是不能修改原始碼的,要不違反開閉原則

let p = {
  sayHello(name) {
    return 'hello, ' + name
  }
}

function decorateWithUpperFirst(obj) {
  let originSay = obj.sayHello
  obj.sayHello = (name) => {
    return originSay(name.charAt(0).toUpperCase() + name.substr(1).toLowerCase())
  }
}

function decorateWithLongName(obj) {
  let originSay = obj.sayHello
  obj.sayHello = (name) => {
      if (name.length > 4) {
        return originSay(name)
      } else {
        console.log('sorry...')
      }
  } 
}

decorateWithUpperFirst(p)
decorateWithLongName(p)

// 返回hello, Xiangwangdeshenghuo
p.sayHello('xiangwangdeshenghuo')
// 控制檯輸出sorry...,返回
p.sayHello('jxtz')
複製程式碼

第一次呼叫sayHello時,會進入decorateWithLongName方法中定義的sayHello,由於name長度大於4,會呼叫它外層的originSay, 即是decrateWithUpperFirst方法中定義的sayHello, 將首字母大寫,最後呼叫它外層的originSay,即最初的p.sayHello

middleware

let p = {
  prefix: 'hello, ',

  sayHello(name) {
    return this.prefix + name
  }
}

function addDecorators(obj, decorators) {
  let sayHello = obj.sayHello

  decorators.slice().reverse().forEach((decorator) => {
    sayHello = decorator(obj)(sayHello)
  })

  obj.sayHello = sayHello
}

function decorateWithUpperFirst(obj) {
  return (nextSay) => {
    return function sayHello1(name) {
      nextSay.call(obj, name.charAt(0).toUpperCase() + name.substr(1).toLowerCase())
    }
  }
}

function decorateWithShortName(obj) {
  return (nextSay) => {
    return function sayHello2(name) {
      if (name.length <= 4) {
        nextSay.call(obj, name)
      } else {
        console.log('substr...')
        obj.sayHello(name.substr(0, 3))
      }
    }
  }
}

addDecorators(p, [decorateWithUpperFirst, decorateWithShortName])


// hello, Jxtz
p.sayHello('jxtz')
// hello, Xia
p.sayHello('xiangwangdeshenghuo')
複製程式碼

上面兩次客戶端呼叫sayHello, sayHello1函式,分別呼叫了幾次?

其實middleware只是把monkey patch隱藏起來 官方文件的middleware 介紹的很詳細
至於redux真實情況是怎麼實現middleware的, applyMiddleware, 其利用了compose函式,熟悉函式式的應該特別熟悉這個組合函式

Split Reducer

技巧裡的splitting-reducer-logic
拆分就要考慮重用,以及其他(如slice reducer之間的狀態獲取)...
refactoring-reducers-example

由於我們的state,往往是巢狀層級的(當然redux希望你去標準化它),由於這個需求太過於普遍性,redux提供了combineReducers這個工具方法,但是redux對很多實踐都是unbiased, 對此也是,你甚至可以不用combineReducers

由於使用combineReducers是redux的common practice
下面看combineReducers的使用

function postsById(state = {}, action) {
  let { id, post } = action
  switch(action.type) {
    case 'ADD_POST':
      return Object.assign({}, state, { [id]: post })
      break
    default:
      return state
  }
}

function postsallIds(state = [], action) {
  let { id } = action
  switch(action.type) {
    case 'ADD_POST':
      return state.concat(id)
      break
    default:
      return state
  }
}

const posts = combineReducers({
  byId: postsById,
  allIds: postsallIds
})


// 類似posts...
function commentsById(state = {}, action) {
  let { id, comment } = action
  switch(action.type) {
    case 'ADD_COMMENT':
      Object.assign({}, state, { [id]: comment })
      break
    default:
      return state
  }
}

function commentsAllIds(state = [], action) {
  let { id } = action
  switch(action.type) {
    case 'ADD_COMMENT':
      return state.concat(id)
      break
    default:
      return state
  }
}

const comments = combineReducers({
  byId: commentsById,
  allIds: commentsAllIds
})

const rootReducer = combineReducers({
  posts,
  comments
})

// 使用
let store = createStore(rootReducer)
// 其實在createStore已經建立了初始值
// 聰明的讀者,你能知道createStore是如何建立這個初始值的嗎?
// {
//   posts: { byId: {}, allIds: [] },
//   comments: { byId: {}, allIds: [] }
// }
console.log(store.getState())

// dispatch會觸發所有的reducer執行, 這裡的slice reducer, case reducer
store.dispatch({ type: 'ADD_POST', id: 1,  post: '這是一篇文章哦' })

// {
//   posts: { byId: {1: '這是一篇文章哦'}, allIds: [1] },
//   comments: { byId: {}, allIds: [] }
// }
console.log(store.getState())
複製程式碼

使用了之後,自然可以去看redux的實現

顯而易見,combineReducer也並不神祕,返回的僅僅也是一個reducer函式, 它將key值與state對應起來,從而在呼叫combine後的reducer時將state[key]值傳入對應的slice reducer函式,從而slice reducer只處理自身感興趣的state部分

在這裡applyMiddleware做了一個優化,由於我們的action.type為'ADD_POST', 所以對comments部分的狀態是沒有改變的, 所以這部分comments狀態會直接返回之前的引用 並不會返回新物件

Async Actions

如果沒有middleware, 我們只能在元件中呼叫ajax 然後就會重複程式碼,我們需要重用邏輯 async-action-creators

Middleware lets us write more expressive, potentially async action creators.

介紹redux-thunk 見 常用工具使用 -> redux-thunk

一些常用工具的使用

redux-actions

Flux Standard Action

let defaultState = { num: 10 }

let addNum = createAction('ADD', (n) => {
  return {
    n
  }
})

let subtractNum = createAction('SUBTRACT', (n) => {
  return {
    n
  }
})

const rootReducer = handleActions({
  'ADD': (state, action) => {
    let { payload: { n } } = action
    return { ...state, num: state.num + n }
  },
  'SUBTRACT': (state, action) => {
    let { payload: { n } } = action
    return { ...state, num: state.num - n }
  }
}, defaultState)

let store = createStore(rootReducer)

store.dispatch(addNum(2))
// { num: 12 }
console.log(store.getState())
store.dispatch(subtractNum(1))
// { num: 11 }
console.log(store.getState())
複製程式碼

當然你可以使用更簡潔的combineActions,見其repo

這裡簡單說一下,handleAction的實現, 他提供了next, throw的api,其實是檢視action.error來判斷呼叫next還是throw, 至於他內部也是判斷action.type是否被include在它的第一個type引數(強制被轉成陣列)裡, 從而決定是否執行此reducer.

redux-thunk(redux-promise, redux-saga)

“Thunk” middleware lets you write action creators as “thunks”, that is, functions returning functions. This inverts the control: you will get dispatch as an argument, so you can write an action creator that dispatches many times.

function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => next => action => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;
複製程式碼

你看的沒錯,這就是redux-thunk的全部程式碼,僅僅判斷如果action是函式,即action creator中返回的函式,那麼將呼叫此函式並將dispatch和getState的傳入。
特別注意,正如前面middleware中的程式碼,此時傳入的dispatch,是applyMiddleware的middlewareAPI物件中的dispatch, 那麼呼叫這個dispatch, 會讓整個middleware chain都從頭呼叫一遍, 就如前面decorateWithShortName的else部分

更多, 建議看看如下文件及程式碼 官方例項asyncreal world

當然你可以選擇使用promise,而不是function, 那麼你可以用redux-promise
也可以選擇generator的方式, redux-saga

reselect

Reselect is a simple library for creating memoized, composable selector functions. Reselect selectors can be used to efficiently compute derived data from the Redux store.

官方文件computing-derived-data 看過文件,對他的使用也有所瞭解

這裡,關注一下, 他到底做了啥優化?
memorize函式, 應該也見過很多次, 複習下

function defaultEqualityCheck(a, b) {
  return a === b
}

function areArgumentsShallowlyEqual(equalityCheck, prev, next) {
  if (prev === null || next === null || prev.length !== next.length) {
    return false
  }

  // Do this in a for loop (and not a `forEach` or an `every`) so we can determine equality as fast as possible.
  const length = prev.length
  for (let i = 0; i < length; i++) {
    if (!equalityCheck(prev[i], next[i])) {
      return false
    }
  }

  return true
}

export function defaultMemoize(func, equalityCheck = defaultEqualityCheck) {
  let lastArgs = null
  let lastResult = null
  // we reference arguments instead of spreading them for performance reasons
  return function () {
    if (!areArgumentsShallowlyEqual(equalityCheck, lastArgs, arguments)) {
      // apply arguments instead of spreading for performance.
      lastResult = func.apply(null, arguments)
    }

    lastArgs = arguments
    return lastResult
  }
}
複製程式碼

意思就是傳入一個函式的func,它只接受一個陣列引數,memorize將返回一個函式,呼叫它時,會檢查這個陣列的每個元素,與之前的是否 "===", 如果均通過,則使用"記憶"的資料,不重新計算

剩下就是將最後一個函式前面所有的依賴函式呼叫的值,與之前進行比較,如果相同則使用原先的結果,不再呼叫最後一個函式

const state = {
  a : {
      first : 5
  },
  b : 10
};

const selectA = state => state.a;
const selectB = state => state.b;

const selectA1 = createSelector(
    [selectA],
    a => a.first
);

const selectResult = createSelector(
    [selectA1, selectB],
    (a1, b) => {
        console.log("Output selector running");
        return a1 + b;
    }
);

const result = selectResult(state);
// Log: "Output selector running"
console.log(result);
// 15

const secondResult = selectResult(state);
// No log output
console.log(secondResult);
// 15
複製程式碼

總之reselect,可以提升效能,一方面,一個複雜轉換操作,其效能損耗大,那麼僅在state.someData變化時,才執行,而state.someElseData變化,它只需返回快取資料,另一方面,對react-redux, connect方法, 根據你返回的mapState的所有欄位是否與之前"===", 來決定元件是否rerender, 而返回快取資料,不會觸發元件rerender
using-reselect-selectors

小結

關於redux的內容,還有很多內容沒有介紹,比如server-render, immutable(immer)結合, devtools, react-redux...

redux對很多使用規則都是無偏見的,只要你遵循他的思想, 所以還需要多實踐它的common practice,找到適合自己的best practice

參考

這裡有redux很多資料
https://redux.js.org/introduction/learning-resources
最好多看作者的stackoverflow和issue中的回答

相關文章