RN学习笔记2-Redux
一、背景
React或者说ReactNative原生使用state和props管理UI状态,但是俩属性非常琐碎,稍微复杂一点的UI就没法应付,所以Redux应运而生,但是Redux又很乱,我觉得乱主要是以下原因:
- 需要声明的东西多
- 需要声明的地方多
- API方法的命名很莫名其妙
下面就来总结一下使用
二、安装
需要在项目里添加如下依赖(版本无所谓)
"react-redux": "^5.0.7",
"redux": "^4.0.0",
"redux-logger": "^3.0.6"
因为redux其实是可以独立运行的js项目,所以把他使用在react项目中,还需要使用react-redux,
redux-logger是打印redux事件log的中间件,具体内容我们后面会说
三、大牛文章
我主要是看这几个文章入门的
https://segmentfault.com/a/1190000008741380
https://www.cnblogs.com/hhhyaaon/p/5860159.html
https://codesandbox.io/s/6n20nrzlxz c11(讲pure redux)
https://segmentfault.com/a/1190000008322583
四、原理
借鉴于https://segmentfault.com/a/1190000008322583- redux是一个存储状态,响应事件动作(action)的地方,所以定义redux实现的叫store
- store有一个初始状态(default state),还有响应某个动作(action)的处理器(reducer)
- 然后UI视图将这个store及其状态(state)和方法(action)注册到视图组件的props,这样就可以在组件中取到这些状态和方法了。
- 当用户点击了某个操作等,会从props中拿到action并调用它,他会向store发送(dispatch)这个action的内容,
- 如果store中有中间件,会先逐个调用中间件来完成预处理
- 然后再调用各个reducer,来完成状态的改变。
- 状态改变以后,因为状态绑定了UI组件的props,所以react会自动刷新UI。
那么我们来仔细说一下reducer和中间件
reducer:名字起源于Array的reduce方法,作者估计向表达的是遍历的意义,但是这个名字实在是诡异,所以我给他起名叫做处理器,或者叫事件触发器,作用就是UI发来action以后,它根据action的类型,对状态进行修改。他是action的消费者,他是个函数(或者专业点叫纯函数),但是他有个缺点,就是需要立即返回,如果是网络请求等异步操作,他就没法胜任了。
中间件:中间件的作用就是完成异步请求,或者完成其他一些需要封装起来的预处理,比如redux-logger,就是把action前后的状态打印出来的中间件,本质也是个函数,但是结构很诡异,诡异程度类似于C语言中的3级指针,这个指针还尼玛是函数指针。不过这种诡异我们不需要操心,只需要填写内容
五、RTFSC
(一)redux创建
仿照大牛们的例子,我们做个通过加减按钮改变数值的功能
- 首先我们引入redux模块
import { combineReducers, createStore, applyMiddleware, compose } from 'redux'
- 创建action creator
action其实就是个对象,有一个最基本的key是type,表示action的类型,如果业务需要,还可以增加其他key,反正这个对象怎么用也是你自己的事,根据自己喜好来。
而所谓action creator就是个创建action对象的函数
// action types
export const INCREASE = 'INCREASE'
export const DECREASE = 'DECREASE'
export const RESET = 'RESET'
// actions
const increase = () => ({ type: INCREASE })
const decrease = () => ({ type: DECREASE })
const reset = (num) => ({ type: RESET, num })//除了type,你还可以加别的内容
- 定义初始状态
const defaultState = {
count: 5
}
- 创建reducer
前面说了,reducer其实就是个函数,接受两个参数,一个是当前状态,一个是发来的action,然后返回一个新的状态
function counter (state = defaultState, action) {//有个默认参数,当第一次调用的时候,使用初始状态
switch (action.type) {
case INCREASE:
return { ...state, count: state.count + 1 }
case DECREASE:
return { ...state, count: state.count - 1 }
case RESET:
return { ...state, count: action.num }
default:
return state
}
}
- 创建store
const reducers = combineReducers({counter})
const configureStore = preloadedState => {
return createStore(
reducers,
preloadedState,
compose(
applyMiddleware(createLogger)
)
)
}
const store = configureStore()
- combineReducers的意思是把多个reducer合并成一个,因为一个store可以处理很多类的业务,所以可以封装成多个reducer,当action来的时候,会一一调用,所以保证事件唯一性,还是要靠定义唯一的action type
- 显然preloadedState我们这里没有用,我们是放在了上面的defaultState里,
- createStore方法返回一个store实例,调用的时候需要把reducers放进去,如果有中间件,就依次放到applyMiddleware里,applyMiddleware支持多个参数
- 最后,我们把外界需要的变量导出
export {store, increase, decrease, reset}
store需要放到Provider组件里,包在我们的页面上,用于往页面的props里注入属性和action方法,后面一个就是action creator了,外界调用这个方法来改变store的状态
(二)redux使用
我看到的demo里,都是使用页面根节点来绑定store,根据我的理解,我觉得每个功能点的逻辑,最好分开,所以把一整个store以及所有state定义在根节点,无论对代码整理还是性能都是不可接受的,所以我google了很久终于在so上找到了解决方案。
- 首先定义一个页面叫做ReduxPage:
class ReduxPage extends Component {
constructor (props) {
super(props)
this.state = {}
}
render () {
const {increase, decrease, reset} = this.props
return (
<View style={styles.container}>
<Text style={styles.counter}>{this.props.counter.count}</Text>
<SubText {...this.props} />
<TouchableOpacity style={styles.btn} onPress={() => { reset(0) }}>
<Text>归零</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn} onPress={increase}>
<Text>加1</Text>
</TouchableOpacity>
<TouchableOpacity style={styles.btn} onPress={decrease}>
<Text>减1</Text>
</TouchableOpacity>
</View>
)
}
}
一个Text显示计数器数字,一个SubText展示子组件显示计数器数字
三个按钮展示动作action
- 然后绑定store的state和action方法到这个页面
const mapStateToProps = state => ({
counter: state.counter
})
const mapDispatchToProps = dispatch => (bindActionCreators({increase, decrease, reset}, dispatch))
//或者const mapDispatchToProps = {increase, decrease, reset}
let Container = connect(mapStateToProps, mapDispatchToProps)(ReduxPage)
- mapStateToProps是把store中的状态映射给这个页面的props,这个例子是把reducer counter的状态(存储在state.counter中)映射到this.props.counter下
- mapDispatchToProps及其后面的调用是把action creator绑定上dispatch,注入到this.props下,
什么意思呢,比如increase函数,他本身是返回一个对象,怎么才能发送给store呢?需要调用this.props.dispatch(increase());
dispatch方法是redux放到props里的,用来发送action。bindActionCreators作用是把increase函数替换成dispatch(increase()),名字还叫increase,这样我们调用this.props.increase就可以直接给store发送action了
这需要注意,其实我们不需要bindActionCreators也可以,因为:传入一个object,其中这个object所对应的value必须是actionCreator,这样redux里面会自动帮我们调用bindActionCreator,所以mapDispatchToProps里传一个object,里面是所有需要的action creator就行 - connect函数接受两个参数,然后返回一个函数,这个返回函数的参数是UI组件,最终返回一个高阶组件,我们命名为Container
- 之所以这么啰嗦,其实就是为了套用es6和react的语法,这些都是语法糖
- 导出页面组件
我们不能直接导出刚刚创建的ReduxPage,因为现在还没有人绑定store呢,使用下面的代码来绑定store并导出组件
export default class extends Component {
render () {
return (<Provider store={store}>
<Container />
</Provider>)
}
};
在其他地方,比如父组件,想怎么用就怎么用就可以了。
(三)redux难题
- 自定义子组件继承父组件的redux props
我们在redux connect的那个组件上,因为有mapStateToProps和mapDispatchToProp函数,可以在其this.props里获取到映射的状态和action,但是如果在这个组件上再嵌套一个自定义的子组件(例如我定义的SubText),在子组件里就获取不到状态和action了,但是我看教程里明明说的是可以自动往下层传的,所以我这里使用了这样的语法,也是google的
<SubText {...this.props} />
我觉得这只能算是个workaround,正规的写法是啥还没搜出来
- 映射方法的时候,定义一个key
如果我们不想污染this.props,像state一样可以通过this.props.<key>.xxx来获取方法,可以这样写
const mapDispatchToProps = dispatch => ({businessDispatch: bindActionCreators({sortChange, filterChange, fetchData}, dispatch)})
- React Navigation
redux和redux navigation合起来使用会比较麻烦,官方有教程
但是因为我的redux只是我当前一个页面的业务redux,没必要和redux navigation混合起来,所以我的解决方案:最外层是react navigation,内层是redux
export default class extends Component {
static navigationOptions = ({ navigation }) => {
const params = navigation.state.params || {}
return {
title: '客户列表',
headerRight: (//导航栏按钮
<View style={{flexDirection: 'row'}}>
<TouchableOpacity
style={{width: 40, height: 40, alignItems: 'center', justifyContent: 'center'
}}
onPress={params.search}
>
<Image source={require('../resource/search.png')} style={{width: 20, height: 20}} />
</TouchableOpacity>
<TouchableOpacity
style={{width: 40, height: 40, alignItems: 'center', justifyContent: 'center'
}}
onPress={params.showMore}
>
<Image source={require('../resource/more.png')} style={{width: 20, height: 20}} />
</TouchableOpacity>
</View>
)
}
};
render () {
return (<Provider store={store}>
<Container navigation={this.props.navigation} />
</Provider>)
}
};
- 自己创建中间件
初次看到中间件的时候感觉好高深,好难懂,好晦涩,其实根本就不是,js让我这个objcer懵逼的地方就在于乱七八糟的风格,这个本质就是个函数,其余都可以忽略
我现在就遇到一个情况,需要下拉刷新请求数据,这是个异步操作,reducer没法处理,所以用中间件。
这个方法拦截Action.FETCH_DATA(其实就是个提前定义好的字符串,表示具体action type) action, 然后延时刷新数据,刷新数据前后向store发送处理数据中的状态,所谓的刷新数据就是个延时。
function createFetchCustom ({ dispatch, getState }) {
return (next) =>
(action) => {
const prevState = getState()
const returnValue = next(action)
const nextState = getState()
const actionType = String(action.type)
const customState = nextState.customerListBusiness
if (actionType === Action.FETCH_DATA) {
console.log('middle ware fetch data')
dispatch(Action.processingdata(true))
const customs = [
{key: '1', name: '刘安博'},
{key: '2', name: '张土豪'},
{key: '3', name: '李贫农'}
]
setTimeout(() => {
dispatch(Action.reloadData(customs))
dispatch(Action.processingdata(false))
}, 1000)
}
return returnValue
}
}
可以看到,这是个“三级函数”,但是因为这个函数是redux内部调用的,所以我们不需要关心他的复杂度,
在上面的代码中,我已经获取到了action发送前后的状态,action的类型,所以你就根据action做你爱做的事情就可以了。