redux三原則及data flow
- 單一資料來源
- 狀態只讀
- 使用純函式來改變狀態
稍微注意一下,這些原則,在redux的實現中是一點也沒體現出來。
In a very real sense, each one of those statements is a lie!
-- 摘自tao-of-redux
而這些原則是指導你如何使用redux
資料流
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-creators 及 why-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部分
更多, 建議看看如下文件及程式碼 官方例項的async和real 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中的回答