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中的回答
原文发布时间为:2018年06月30日
作者:小雨心情
本文来源:掘金 如需转载请联系原作者