React的state初始化及其在dva框架中的具体表现

目录

前言

  本篇结合在使用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';

// 1. Initialize
const app = dva({
history: hashHistory,

//全局错误处理配置
onError(e) {
message.config({
top: 8,
duration: 3,
});
message.error(e.message, 3);
},
});

// 2. Plugins
//app.use({});

// 3. Model
app.model(require('./models/homePage'));

// 4. Router
app.router(require('./router'));

// 5. Start
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