[React-章节11 终结篇] 做一个留言板项目 之 重构至redux
思考
最近我也一直在研究redux到底是干什么的,经过零零散散的学习之后,我有这么几个不算成熟的认识分享给大家:
- redux和react没有依赖关系,redux出现是提出了一种规范和模型,可以这么来描述它:程序(可以理解为UI/展现组件)某处产生一个action(理解为一个消息,一个事件),通过dispatch(理解为一个邮递员,信使)的派发,交由处理者reducer(其实就是处理action)进行最终的计算,结果存储到store(就是一个对象,存的是程序的状态state)里。
- redux在全局只维护一个store存储对象,程序分很多组件,那么各个组件的state状态都存储到这1个store里,同时每个组件的reducer处理器也聚合到一起挂接到store上。
- react把组件的展示,数据的异步获取,state的变化全部实现在component里,随着代码膨胀愈加复杂,而redux有利于我们将数据的处理和展示进行进一步分层,优化项目结构。
- redux将所有组件的state存储在1个store中,很自然的有利于跨组件的state共享,我个人认为这是非常重要的一个特性。如果没有Redux,我们的react跨组件共享信息,难免通过深层次的callback传递与notify思路去实现,是很麻烦的事情。
学习方法
其实我按照自己的计划学习实践到第11篇博客,已经感受到纯react代码混合实现异步数据和组件特效会导致代码一团糟。如果redux能够不费多大功夫帮我把『数据和计算』与『组件展现』分离开来,我还是愿意花点时间来掌握的。
学习redux,首先是基础知识:至少知道redux的理念,基础API,同时对redux如何结合react,甚至结合react-router有一个朦朦胧胧的认识,我认为就足够了,这方面应该看一下这篇教程。
不过,我看完这个教程也挺迷糊的,并不清楚怎么应用到我的项目里来,所以我接着看了这篇博客,它主要是介绍react和redux是如何结合的,更加贴合实践。其中connect是我们最需要了解的API,它极大的简化了react和redux结合的背后问题,使得我们可以很自然的在react框架下套用redux的理念。
当然看完之后,大概扫一下这个github里的例子会加深一点理解,这个例子里用到了redux-simple-router这个库,它是为react-router封装的redux库,因为我的项目也使用react-router(我相信绝大多人都要用),因此后续实践也会涉及到这个库的使用。
打开它最新的官方地址,阅读一下介绍会发现它是redux-simple-router的前身,现在叫做react-router-redux,并且里面的例子似曾相识。我们知道react-redux是为react封装了redux便于使用,而react-router-redux是封装了react-router相关的redux逻辑,因此我们最终会同时使用react,react-redux,react-router-redux,它们分别只解决自己关心的问题,需要组合使用才能完成项目。
下载源码
重构项目
安装依赖
- npm install redux –save
- 安装redux不必多说
- npm install redux-thunk –save
- 这是一个redux的中间件,能够支持我们dispatch一个function而不是action对象,后面会看到具体啥意思,没那么复杂
- npm install react-redux –save
- 基于react封装的redux,从而我们可以很方便的为组件注入action和state到组件props,其实就是屏蔽redux原生API的复杂性
- npm install react-router-redux –save
- 为react-router封装了一下相关的redux逻辑,因为react-router是最外层的组件(<Router>还记得吗),我们的组件都套在里面。因此如果如果我们的组件要用redux,那Router组件不实现redux相关逻辑我们又怎么用呢,所以顺理成章。
创建store
redux本来创建store也只需要传入reducer函数就足够了,之后做的事情无非是定义一下action对象,通过dispatch方法交给reducer进行处理,所以store的创建仅需要reducer函数。
而比较简单的是,正因为react-router本身需要为store提供reducer函数,因此我自己的组件MsgListPage,MsgDetailPage不需要立马进行改造。我先把react-router的reducer注入到store上,把这个全局的store对象建立出来,代码应该可以正常运行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
import React from "react"; import ReactDOM from "react-dom"; import {Router, Route, IndexRoute, hashHistory} from "react-router"; // 引入redux,react-redux,react-router-redux,它们各有各的职责 import {createStore, combineReducers, applyMiddleware} from 'redux' import {Provider} from 'react-redux' import {syncHistoryWithStore, routerReducer} from 'react-router-redux'; import thunk from 'redux-thunk'; // 默认的App根路由,作为组件容器 import Container from "../component/Container"; // 各种小组件在这里引入 import MsgListPage from "../component/MsgListPage/MsgListPage"; import MsgDetailPage from "../component/MsgDetailPage/MsgDetailPage"; import MsgCreatePage from "../component/MsgCreatePage/MsgCreatePage"; // 引入reducer // 当你读博客的时候,先忽视这一行 import MsgDetailPageReduer from "../component/MsgDetailPage/reducer"; // 聚集所有reducer // 注:这里的key就是全局store的1级key,用于划分不同reducer的state集合,避免互相污染 const reducer = combineReducers({ MsgDetailPageReduer: MsgDetailPageReduer, // 当你读博客的时候,先忽视这一行 routing: routerReducer, // react-router所需要的reducer }, ); // 创建redux的store const store = createStore( reducer, // 全部的reducer applyMiddleware( // 安装若干中间件 thunk, ), ); // 增强react-router的history能力,其实就是把history相关信息也存储到store中 // 在<Router>中取代原有的hashHistory const history = syncHistoryWithStore(hashHistory, store) ReactDOM.render( ( <Provider store={store}> <Router history={history}> <Route path="/" component={Container}> <IndexRoute component={MsgListPage} /> <Route path="msg-list-page" component={MsgListPage}/> <Route path="msg-detail-page/:msgId" component={MsgDetailPage}/> <Route path="msg-create-page" component={MsgCreatePage}/> </Route> </Router> </Provider> ), document.getElementById('reactRoot') ); |
这是Router.es6修改后的代码,做了几个修改点:
- 引入redux,使用了combineReducers实现reducer聚合,createStore实现store创建,applyMiddleware引入中间件。
- 引入react-redux,使用了它为react封装的外层容器<Provider>。
- 引入react-router-redux,使用了它为react-router提供的routerReducer以及用于增强react-router的history能力的syncHistoryWithStore,用于取代原生的hashHistory(这样router的location数据也就存储到了store中)。
- 引入了redux-thunk中间件,因为我有异步dispatch action的需求。
经过简单的修改,现在我们已经为redux铺垫好了基础环境,并且对我们现有的程序不会造成任何影响。这样,接下来我就可以从最简单的组件MsgDetailPage入手,为其实现action和reducer,同时把组件需要的state和action以props的形式注入到组件内,最终把reducer添加到store中。
MsgDetailPage详情页
按道理说,只要涉及到setState调用的相关逻辑链,都应该用redux重构。
最终的效果应该是component仅访问props即可完成UI渲染,而所有业务逻辑和状态管理都应该挪到action和reducer中处理。
为了方便翻阅,先贴出修改前、后的MsgDetailPage.es6的完整代码:
修改前的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 |
import React from "react"; import {Link} from "react-router"; import $ from "jquery"; import style from "./MsgDetailPage.css"; import ToolBar from "./ToolBar/ToolBar"; import LoadingLayer from "../LoadingLayer/LoadingLayer"; export default class MsgDetailPage extends React.Component { constructor(props, context) { super(props, context); this.state = { msgId: this.props.params.msgId, contentHeight: 0, isLoading: true, outerStyle: {height: 0}, }; } fetchDetail() { let msgId = this.state.msgId; $.ajax({ type: 'GET', url: '/msg-detail', data: {'msgId': msgId}, dataType: 'json', success: (response) => { this.setState({ msgTitle: response.data.title, msgContent: response.data.content, isLoading: false, // 首屏加载完成, 标记loading结束 }); console.log(`msg-detail?msgId=${msgId} 请求成功, msgContent=${this.state.msgContent}`); }, error: () => { console.log(`msg-detail?msgId=${msgId} 请求异常`); } }); } componentDidMount() { // 调整loading界面的样式 let ToolBar = $(this.refs.ToolBar.refs.ToolBarContainter); this.setState({outerStyle: {height: window.innerHeight - ToolBar.height()}}); // 发起数据加载(setTimeout模拟延迟) setTimeout(() => { this.fetchDetail(); }, 500); } componentDidUpdate() { // 加载完成 if (!this.state.isLoading) { let title = $(this.refs.MsgTitle); let container = $(this.refs.MsgContainer); let ToolBar = $(this.refs.ToolBar.refs.ToolBarContainter); // 上半部分总高度 let height = title.height() + parseInt(title.css('padding-top')) + parseInt(title.css('padding-bottom')) + parseInt(container.css("padding-top")) + parseInt(container.css("padding-bottom")) + parseInt(ToolBar.height()); // 窗口高度-上半部分总高度作为文章的最小高度 if (this.state.contentHeight != window.innerHeight - height) { // 如果一样则不要setState避免递归渲染 this.setState({ contentHeight: window.innerHeight - height, }); } } } renderLoading() { let outerStyle = { height: window.innerHeight, }; return ( <div> <ToolBar ref="ToolBar"/> <LoadingLayer outerStyle={this.state.outerStyle}/> </div> ); } renderPage() { // refs属性会捕获对应的原生的Dom节点,会在componentDidUpdate中访问Dom来动态计算高度 return ( <div> <ToolBar ref="ToolBar"/> <h1 id={style.MsgTitle} ref="MsgTitle">{this.state.msgTitle}</h1> <div id={style.MsgContainer} ref="MsgContainer" style={{minHeight: this.state.contentHeight}}> <p id={style.MsgContent}>{this.state.msgContent}</p> </div> </div> ); } render() { if (this.state.isLoading) { return this.renderLoading(); } else { return this.renderPage(); } } } MsgDetailPage.contextTypes = { router: () => { React.PropTypes.object.isRequired } }; |
修改后的代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 |
import React from "react"; import {Link} from "react-router"; import $ from "jquery"; import style from "./MsgDetailPage.css"; import ToolBar from "./ToolBar/ToolBar"; import LoadingLayer from "../LoadingLayer/LoadingLayer"; // redux相关 import { bindActionCreators } from 'redux' import { connect } from 'react-redux' import * as actions from "./action"; class MsgDetailPage extends React.Component { constructor(props, context) { super(props, context); // 在这里先重置上一次阅读留下的state信息 // action: MSG_DETAIL_PAGE_INIT_STATE this.props.initState(); } componentDidMount() { // 调整Loading界面高度 // action: MSG_DETAIL_PAGE_ADJUST_LOADING_HEIGHT let ToolBar = $(this.refs.ToolBar.refs.ToolBarContainter); this.props.adjustLoadingHeight(window.innerHeight - ToolBar.height()); // 发起数据加载(setTimeout模拟延迟) // action: MSG_DETAIL_PAGE_FETCH_DETAIL this.props.fetchDetail(this.props.msgId); } componentDidUpdate() { // 加载完成 if (!this.props.isLoading) { let title = $(this.refs.MsgTitle); let container = $(this.refs.MsgContainer); let ToolBar = $(this.refs.ToolBar.refs.ToolBarContainter); // 上半部分总高度 let height = title.height() + parseInt(title.css('padding-top')) + parseInt(title.css('padding-bottom')) + parseInt(container.css("padding-top")) + parseInt(container.css("padding-bottom")) + parseInt(ToolBar.height()); // 窗口高度-上半部分总高度作为文章的最小高度 if (this.props.contentHeight != window.innerHeight - height) { // 如果一样则不要setState避免递归渲染 // 调整文章部分最小高度 // action: MSG_DETAIL_PAGE_ADJUST_CONTENT_HEIGHT this.props.adjustContentHeight(window.innerHeight - height); } } } renderLoading() { return ( <div> <ToolBar ref="ToolBar"/> <LoadingLayer outerStyle={this.props.outerStyle}/> </div> ); } renderPage() { // refs属性会捕获对应的原生的Dom节点,会在componentDidUpdate中访问Dom来动态计算高度 return ( <div> <ToolBar ref="ToolBar"/> <h1 id={style.MsgTitle} ref="MsgTitle">{this.props.msgTitle}</h1> <div id={style.MsgContainer} ref="MsgContainer" style={{minHeight: this.props.contentHeight}}> <p id={style.MsgContent}>{this.props.msgContent}</p> </div> </div> ); } render() { if (this.props.isLoading) { return this.renderLoading(); } else { return this.renderPage(); } } } MsgDetailPage.contextTypes = { router: () => { React.PropTypes.object.isRequired } }; // 将redux store里的state映射到本组件的Props上 // 注:这里传来的state是全局store,从而可以共享所有全局状态的访问! function mapStateToProps(state, ownProps) { console.log(state); return { msgId: ownProps.params.msgId, // 访问react-router的参数是可以的 contentHeight: state.MsgDetailPageReduer.contentHeight, isLoading: state.MsgDetailPageReduer.isLoading, outerStyle: state.MsgDetailPageReduer.outerStyle, msgTitle: state.MsgDetailPageReduer.msgTitle, msgContent: state.MsgDetailPageReduer.msgContent, }; } // 将实现的若干action方法映射到本组件的Props上,后续用来实现逻辑,触发redux事件流 function mapDispatchToProps(dispatch) { return bindActionCreators(actions, dispatch); } //通过react-redux提供的connect方法将我们需要的state中的数据和actions中的方法绑定到props上 export default connect(mapStateToProps, mapDispatchToProps)(MsgDetailPage); |
为组件注入state与action
从代码对比看出,这里使用export default connect….取代了原先的export default class MsgDetailPage。
- mapStateToPorps函数用于将redux store中的state注入到component对象的props中,所谓『注入』其实就是只需要我们返回一个映射关系即可:其中key是props里的名字,value是redux store中某个字段的值。
- mapDispatchToProps里调用了bindActionCreators,顾名思义是将我们实现的若干『action生成方法』注入到component的props里。
- connect方法将上述2个注入函数关联到组件MsgDetaiPage,最终导出一个经过修饰包装的组件。
- ownProps用于访问react-router提供给我们的一些数据,例如:获取url query参数和router捕获的params。
- 关注一下代码里的注释,我们完全有能力将redux store中任何属性注入到组件中来访问,这就提供了非常方便的跨组件数据共享的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
// 将redux store里的state映射到本组件的Props上 // 注:这里传来的state是全局store,从而可以共享所有全局状态的访问! function mapStateToProps(state, ownProps) { console.log(state); return { msgId: ownProps.params.msgId, // 访问react-router的参数是可以的 contentHeight: state.MsgDetailPageReduer.contentHeight, isLoading: state.MsgDetailPageReduer.isLoading, outerStyle: state.MsgDetailPageReduer.outerStyle, msgTitle: state.MsgDetailPageReduer.msgTitle, msgContent: state.MsgDetailPageReduer.msgContent, }; } // 将实现的若干action方法映射到本组件的Props上,后续用来实现逻辑,触发redux事件流 function mapDispatchToProps(dispatch) { return bindActionCreators(actions, dispatch); } //通过react-redux提供的connect方法将我们需要的state中的数据和actions中的方法绑定到props上 export default connect(mapStateToProps, mapDispatchToProps)(MsgDetailPage); |
编写action
我将涉及state修改的业务逻辑全部抽取成独立的『action生成方法』,这里有4个:
- initState:重置redux store中的state为初始状态
- adjustLoadingHeight(height):用于调整Loading界面的高度
- fetchDetail(msgId):用于ajax获取文章内容
- adjustContentHeight(height):用于调整文章内容的最小高度
action完整代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
import * as consts from "../../common/consts"; import $ from "jquery"; /** * 调整loading界面的高度 */ export function adjustLoadingHeight(height) { // 隐式的dispatch: // 直接返回action对象,这是同步dispatch的最简单套路, // 框架会立即交给reducer,立即生效到props return { type: consts.MSG_DETAIL_PAGE_ADJUST_LOADING_HEIGHT, height: height }; } /** * 请求文章内容 */ export function fetchDetail(msgId) { // 显式的diapatch // 基于react-thunk实现,支持返回function从而获得dispatch上下文,异步的发送action return (dispatch) => { setTimeout(() => { $.ajax({ type: 'GET', url: '/msg-detail', data: {'msgId': msgId}, dataType: 'json', success: (response) => { dispatch({ type: consts.MSG_DETAIL_PAGE_FETCH_DETAIL, title: response.data.title, content: response.data.content, }); console.log(`msg-detail?msgId=${msgId} 请求成功, msgContent=${response.data.content}`); }, error: () => { console.log(`msg-detail?msgId=${msgId} 请求异常`); } }); }, 1000); } } /** * 调整文章最小高度 * @param height * @returns {{type, contentHeight: *}} */ export function adjustContentHeight(height) { return { type: consts.MSG_DETAIL_PAGE_ADJUST_CONTENT_HEIGHT, contentHeight: height, }; } |
其中initState,adjustLoadingHeight,adjustContentHeight是『同步action』,也就是直接return返回一个action对象。这样,框架会帮我们隐式的dispatch这个action,从而交给reducer进一步进行处理,生效。
而fetchDetail则不同,它基于之前的redux-thunk中间件,它支持返回一个function而不是action对象。这个函数应该接受至少一个dispatch参数(第二个可选参数是getState用于获取当前redux store里的值)。我们返回的函数会立即被调用,我们可以使用传入的dispatch发起『同步action』,也可以先执行异步的ajax请求等待回调后再dispatch发起action,而后者就是『异步action』的含义了。
编写reducer
每个组件的reducer都应该与其实现的action对应,这样才能完整的实现redux的流程。
因此,reducer中也有对应的4个action的处理函数实现,它们接收现有的redux state和action对象,经过处理返回新的redux state,框架将帮我们存储到redux store中全局存储。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 |
import * as consts from "../../common/consts"; // 组件初始化状态,其实就是把component的constructor的挪到这里就完事了 const initState = { contentHeight: 0, isLoading: true, outerStyle: {height: 0}, msgTitle: '', msgContent: '', }; function MSG_DETAIL_PAGE_ADJUST_LOADING_HEIGHT_reducer(state, action) { return Object.assign({}, state, { outerStyle: {height: action.height} }); } function MSG_DETAIL_PAGE_FETCH_DETAIL_reducer(state, action) { return Object.assign({}, state, { msgTitle: action.title, msgContent: action.content, isLoading: false, // 首屏加载完成, 标记loading结束 }); } function MSG_DETAIL_PAGE_ADJUST_CONTENT_HEIGHT_reducer(state, action) { return Object.assign({}, state, { contentHeight: action.contentHeight }); } function MSG_DETAIL_PAGE_INIT_STATE_reducer(state, action) { return initState; } // Reducer函数 // 1, 在redux初始化,路由切换等时机,都会被唤醒,从而有机会返回初始化state, // 这将领先于componnent从而可以props传递 // 2, 这里redux框架传来的是state对应Reducer的子集合 export default function MsgDetailPageReduer(state = initState, action) { switch (action.type) { // 调整Loading界面高度 case consts.MSG_DETAIL_PAGE_ADJUST_LOADING_HEIGHT: return MSG_DETAIL_PAGE_ADJUST_LOADING_HEIGHT_reducer(state, action); case consts.MSG_DETAIL_PAGE_FETCH_DETAIL: return MSG_DETAIL_PAGE_FETCH_DETAIL_reducer(state, action); case consts.MSG_DETAIL_PAGE_ADJUST_CONTENT_HEIGHT: return MSG_DETAIL_PAGE_ADJUST_CONTENT_HEIGHT_reducer(state, action); case consts.MSG_DETAIL_PAGE_INIT_STATE: return MSG_DETAIL_PAGE_INIT_STATE_reducer(state, action); // 有2类action.type会进入default // 1) 你不关心的action,属于其他组件 // 2)系统的action,例如router切换了location,redux初始化了等等 default: console.log(action); return state; // 返回当前默认state或者当前state } } |
我们为组件MsgDetailPage导出唯一的reducer函数入口,它判断action.type后分发到具体的4个处理函数,需要注意的几个点如下:
- MsgDetalPageReduer函数的initState参数应该定义成组件初始化的state,也就是此前通过mapSteteToProps()函数定义的那些state。这个做法和在组件的constructor里定义this.state类似,只不过现在是这些state被存储到了redux store中。
- 在组件对象分配前,这个reducer函数会被框架调用,通过日志可以看到收到了这些action:Object {type: “@@redux/INIT”}、Object {type: “@@redux/INIT”}、 Object {type: “@@router/LOCATION_CHANGE”, payload: Object},从而让我们有机会返回组件的初始化state,这个过程也是在default分支中生效的。
- 每个组件的reducer函数需要注册到redux的store中,可以回头看Router.es6中createStore的实现:
123456789// 引入reducerimport MsgDetailPageReduer from "../component/MsgDetailPage/reducer";// 聚集所有reducer// 注:这里的key就是全局store的1级key,用于划分不同reducer的state集合,避免互相污染const reducer = combineReducers({MsgDetailPageReduer: MsgDetailPageReduer,routing: routerReducer, // react-router所需要的reducer}, );
因此,某一个组件的action被dispatch到store时,是有可能通知到其他组件的reducer中,因此default分支也有这方面的用途(不做任何动作,返回当前state),同时也意味着各个组件定义的action.type不能重复。 - redux的state不能直接原地修改,需要拷贝一个副本进行修改,这里使用的Object.assign非常适合这个用途。
- initState作用很大,因为redux store是全局维护的,因此重复进入同一个组件的话会访问到此前遗留在redux store中的state,而MsgDetailPage希望每次进入都是崭新的样子,所以在MsgDetailPage的构造函数里发起了一个用于重置store的action。
定义action.type常量
为了确保action.type全局唯一,所以在common/consts.es6中定义所有action的type。
1 2 3 4 |
export const MSG_DETAIL_PAGE_INIT_STATE = "MSG_DETAIL_PAGE_INIT_STATE"; export const MSG_DETAIL_PAGE_ADJUST_LOADING_HEIGHT = "MSG_DETAIL_PAGE_ADJUST_LOADING_HEIGHT"; export const MSG_DETAIL_PAGE_FETCH_DETAIL = "MSG_DETAIL_PAGE_ADJUST_FETCH_DETAIL"; export const MSG_DETAIL_PAGE_ADJUST_CONTENT_HEIGHT = "MSG_DETAIL_PAGE_ADJUST_CONTENT_HEIGHT"; |
回头看看组件的变化
我们回到开始的组件完整代码,对比发现代码中已经没有state关键字了,所有的状态都从this.props(从redux注入的全局state)中获取,所有的处理方法(用redux注入的action)也都是从this.props获取并调用的。
可见,通过redux我们原先的组件变得非常简单,仅仅是从this.props获取一下属性,渲染出组件的样子就可以了。而异步网络请求这样的操作都挪到了action中,对状态的变更都挪到了reducer当中,差别仅仅是现在的状态是redux store全局状态,而组件通过注入props的方法取代了直接访问this.state,仅此而已。
补充一个细节,在组件构造函数里action改动redux store,对应的state不会立即被注入回this.props而是在首次render完成后(componentDidUpdate调用后)立即进入一轮组件update,此时(componentWillReceiveProps调用)this.props才能读到你在构造函数里曾经变更的state。
通过本篇博客,对redux如何使用以及原理应该基本掌握,作为一系列react教程来说,至此也该画上句号。
后续,我可能会继续补充前端开发的相关基础知识,比如scss之类的,也可能会去动手实现一些比较常见的特效,比如滚动公告栏等。
后话
我后续已经把MsgListPage也重构成redux版本,现在你可以下载最新的完整代码进行运行体验了。
如果文章帮助您解决了工作难题,您可以帮我点击屏幕上的任意广告,或者赞助少量费用来支持我的持续创作,谢谢~

6
1