目录 
前言
本篇结合在使用dva框架进行React应用开发过程中遇到的问题,简要分析React中state的机制和Redux对state的状态管理。
使用dva框架开发时遇到的一个问题
关于dva框架的基本结构和开发模式,在之前的文章中已经有所提及,见React开发框架Dva.js项目结构简单小结。下面的描述直接基于框架中的概念。
问题现象描述
确定接口所需数据结构
在进行一个应用模块的开发,与后端确立的数据接口返回数据项格式如下:
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
| { "status": 200, "message": "查询成功", "data": { "advertisement_index_top":[ { "id": "99", "name": "xxx", "path": "xxx", "url": "xxx" } ], "article_index_top":[ { "id": "68", "content": "xxx", "title": "xxx", } ], "advertisement_index_middle":{ "id": "99", "name": "xxx", "subtitle":"xxx", "path": "xxx", "url": "xxx", "count_down": "xxx", } , "live_index_bottom":[ { "id": "1", "abstract": "xxx", "dispatch_time": "xxx", "tag": "xxx", "color": "xxx", "path":"xxx", "nickname":"xxx", "avatar":"xxx", } ] } }
|
model的定义
根据如上的数据接口,进行了模块数据model层的设计,可以看出返回的数据项结构是比较复杂的,整体有4项,其中三项是数组,每个数组中是若干个对象;另一个是一个单独的对象。在model设计的时候,对于store中的state,最开始只声明了最外层的4个变量,见如下第4行开始的state声明:
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
| export default { namespace: "homePage",
state: { advertisement_index_top: [], article_index_top: [], advertisement_index_middle: {}, live_index_bottom: [], },
subscriptions: { setup({dispatch, history}) { history.listen(location => { if(location.pathname === '/'){ dispatch({ type: 'query', payload: {} }) } }) } },
effects: { *query({payload}, {call, put}) { try { const data = yield call(query, parse(payload)); if(data && data.status == "200") { yield put({ type: "queryFinished", payload: { advertisement_index_top: data.data.advertisement_index_top, article_index_top: data.data.article_index_top, advertisement_index_middle: data.data.advertisement_index_middle, live_index_bottom: data.data.live_index_bottom, } }) } else { yield put({ type: 'queryFinished' }); throw data; } } catch(e) { yield put({ type: "queryFinished"}); throw new Error("查询失败,请重试。"); } }, },
reducers: {
}, };
|
容器组件(container component)的定义(代码有省略)
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
| import React, { Component, PropTypes } from 'react'; import { connect } from 'dva'; import { Link } from 'dva/router'; import styles from './HomePage.less'; import ... ...
function HomePage({ location, dispatch, homePage }) { const { advertisement_index_top, article_index_top, advertisement_index_middle, live_index_bottom, } = homePage;
return ( <div> ... ... </div> ); }
HomePage.propTypes = { homepage: PropTypes.object, location: PropTypes.object, dispatch: PropTypes.func, };
function mapStateToProps({ homePage }) { return { homePage }; }
export default connect(mapStateToProps)(HomePage);
|
异步请求模块
上面涉及到的dispatch的effects异步请求,与一般结构一致,不再描述。
进行测试,出现问题
按如上步骤进行测试,报错如下:

问题分析
上面所报错误并不是错误的直接原因,而是由真正错误所引发的后续的初始化和渲染错误。那么具体过程是怎样的,需要通过断点单步调试:
1.组件路由成功注册后,订阅的路由监听事件检测到路由匹配,dispatch出查询事件action。

这一步没有问题,是框架执行的正常环节。
2.上一步dispatch出查询action交付异步effects中的query函数,将调用真正的查询接口。

这一步没有问题,是框架执行的正常环节。
3.请求到达request函数,由fetch将请求发出。

这一步没有问题,是框架执行的正常环节。
4.以上3步,是model中的执行流程,按照dva框架的规定,接下来将按照router进行组件路由匹配,此处匹配到了HomePage组件。

问题就出在这里。
可以看到,变量homePage中有4个字段没错,但是内容却都是空数组或者空对象,这样后续的子组件会由于拿不到正常数据(是undefined)而报出各种错误。然而通过查看网络请求,我们可以看到数据是成功请求了的:

为什么请求返回的数据没有能够正常更新到state中去?
按一般想法,上面model中定义的更新事件:
1 2 3 4 5 6 7 8 9 10 11
| if(data && data.status == "200") { yield put({ type: "queryFinished", payload: { advertisement_index_top: data.data.advertisement_index_top, article_index_top: data.data.article_index_top, advertisement_index_middle: data.data.advertisement_index_middle, live_index_bottom: data.data.live_index_bottom, } }) }
|
不就会将data.data中取出的四个变量赋值给state中的4个变量吗?那这样的话,像上面写的那样,state初始化时候直接给4个变量赋值为[]或者{}好像没什么问题(反正4个变量内部的字段会完整赋值过去)。
但是问题就产生在这里,这是由于对react redux对状态管理的机制不够了解造成的。一句话来概括,“state不是立即更新的,而是通过一个队列机制实现异步更新”。
为了更好说明这一点,我们看一下dva框架的入口文件index.js,了解执行过程:
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
| import './index.html'; import 'lib-flexible'; import './index.less'; import dva from 'dva'; import { message } from 'antd'; import { hashHistory } from 'dva/router';
const app = dva({ history: hashHistory,
onError(e) { message.config({ top: 8, duration: 3, }); message.error(e.message, 3); }, });
app.model(require('./models/homePage'));
app.router(require('./router'));
app.start('#root');
|
如前面大篇幅描述的,第三步引入model执行完成后,在当前调用栈中将立即进入第四步去执行react-router相关的匹配。此时第三步model相关逻辑虽然执行完成了,但是由其发出的异步操作并没有执行完成。因此,进入第四步去执行,并且路由成功找到HomePage组件的时候,这时候HomePage组件拿到的homePage变量并不是真正向后端请求回来的数据,而是model中的state初始值,也就是那些空对象或者空数组。错误就在这里。
为了证明这一点,我们修改一下model中的state初始值:
1 2 3 4 5 6 7 8 9 10 11 12 13
| state: { advertisement_index_top: [ { id: "1", name: "12", path: "123", url: "1234", } ], article_index_top: [], advertisement_index_middle: {}, live_index_bottom: [], },
|
再次调试可以看到HomePage组件拿到的homePage变量如下:

问题解决
由上述过程了解了问题所在,下面就要进行修改—将state中所有将要从后台取到的变量全部进行初始化,而不只是最外层。
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
| state: { advertisement_index_top: [ { id: "", name: "", path: "", url: "", } ], article_index_top: [ { id: "", content: "", title: "", } ], advertisement_index_middle: { id: "", name: "", subtitle:"", path: "", url: "", count_down: "", }, live_index_bottom: [ { id: "", abstract: "", dispatch_time: "", tag: "", color: "#666FFF", path:"", nickname:"", avatar:"", } ], },
|
这样的话,当程序执行到HomePage组件内部的时候,就不会因为空值而报错:

并且由进一步的调试可以知道,实际上是在上图这个断点之后,请求数据才返回回来的(这个先后过程不好通过文字表述,但经过实际调试是这样。):

数据返回后,再分发一个更新state的action:reducers中的queryFinished:

可以看到payload中的数据长度已经不是最初的“1”了,而是“4”、“5”、“10”等,也就是真正从服务器取回的数据。通过这个reducer去更新state,就相当于原生react写法中的setState()函数,因此state更新后react框架会自动进行UI的更新,把真实数据渲染在页面上。
该问题的另一个规范化解决方案-在service层做好数据adaptor转换
详见React开发框架Dva.js项目结构简单小结#2.3.6. services