redux手动实现之四性能优化

在上一节的代码中,其实存在一个很严重的性能问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function renderTitle(title) {
console.log("render title...");
const titleDom = document.querySelector("#title");
titleDom.innerHTML = title.text;
titleDom.style.color = title.color;
}

function renderContent(content) {
console.log("render content...");
const contentDom = document.querySelector("#content");
contentDom.innerHTML = content.text;
contentDom.style.color = content.color;
}

function renderApp(appState) {
console.log("render app...");
renderTitle(appState.title);
renderContent(appState.content);
}

其他代码保持不变,依旧执行一次初始化渲染,和两次更新,然后打开控制台看下log

前三个是第一次渲染打印出来的。中间三个是第一次 store.dispatch 的结果,最后三个是第二次 store.dispatch 的结果。问题就是后两次的更新都没有改动 content 对象,只是修改了 title 对象。 renderContent 是不需要执行的,这里的操作需要优化。

可以通过在每个渲染函数执行渲染操作之前先做个判断,判断传入的新数据和旧的数据是不是相同,相同的话就不渲染了。

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
function renderTitle(newTitle, oldTile = {}) {
if (newTitle === oldTile) {
return false;// 数据没有变化就不渲染了
}
console.log("render title...");
const titleDom = document.querySelector("#title");
titleDom.innerHTML = newTitle.text;
titleDom.style.color = newTitle.color;
}

function renderContent(newContent, oldContent = {}) {
if (newContent === oldContent) {
return false;// 数据没有变化就不渲染了
}
console.log("render content...");
const contentDom = document.querySelector("#content");
contentDom.innerHTML = newContent.text;
contentDom.style.color = newContent.color;
}

function renderApp(newAppState, oldAppState = {}) {
if (newAppState === oldAppState) {
return false;// 数据没有变化就不渲染了
}
console.log("render app...");
renderTitle(newAppState.title, oldAppState.title);
renderContent(newAppState.content, oldAppState.content);
}

然后我们用一个 oldState 变量保存旧的应用状态,在需要重新渲染的时候把新旧数据传进入去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 生成 store
let store = createStore(appState, stateChanger);
// 缓存旧的state
let oldState = store.getState();
// 监听数据变化
store.subscribe(() => {
// 获取新的 state
let newState = store.getState();
// 把新旧的 state 传进去渲染
renderApp(newState, oldState);
// 新的 newState 变成了旧的 oldState,等待下一次数据变化重新渲染
oldState = newState;
});
renderApp(store.getState());
// 三秒钟之后,修改标题和标题颜色,并重新渲染
setTimeout(function () {
store.dispatch({ type: "UPDATE_TITLE_TEXT", text: "Redux是React是好基友" });
store.dispatch({ type: "UPDATE_TITLE_COLOR", color: "green" });
// renderApp(store.getState());
}, 3000);

我们的代码现在变成了这样:

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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>

<body>
<div id='title'></div>
<div id='content'></div>

<script>
const appState = {
title: {
text: "redux",
color: "red",
},
content: {
text: "redux文档内容",
color: "blue"
}
};

function stateChanger(state, action) {
switch (action.type) {
case "UPDATE_TITLE_TEXT": {
state.title.text = action.text;
break;
}
case "UPDATE_TITLE_COLOR": {
state.title.color = action.color;
break;
}
default:
break;
}
}

// 添加 createStore
function createStore(state, stateChanger) {
let events = [];
return {
subscribe: (event) => {
events.push(event);
},
dispatch: (action) => {
stateChanger(state, action);
events.forEach((event) => event());
},
getState: () => state
};
}

function renderTitle(newTitle, oldTile = {}) {
if (newTitle === oldTile) {
return false;// 数据没有变化就不渲染了
}
console.log("render title...");
const titleDom = document.querySelector("#title");
titleDom.innerHTML = newTitle.text;
titleDom.style.color = newTitle.color;
}

function renderContent(newContent, oldContent = {}) {
if (newContent === oldContent) {
return false;// 数据没有变化就不渲染了
}
console.log("render content...");
const contentDom = document.querySelector("#content");
contentDom.innerHTML = newContent.text;
contentDom.style.color = newContent.color;
}

function renderApp(newAppState, oldAppState = {}) {
if (newAppState === oldAppState) {
return false;// 数据没有变化就不渲染了
}
console.log("render app...");
renderTitle(newAppState.title, oldAppState.title);
renderContent(newAppState.content, oldAppState.content);
}

// 生成 store
let store = createStore(appState, stateChanger);
// 缓存旧的state
let oldState = store.getState();
// 监听数据变化
store.subscribe(() => {
// 获取新的 state
let newState = store.getState();
// 把新旧的 state 传进去渲染
renderApp(newState, oldState);
// 新的 newState 变成了旧的 oldState,等待下一次数据变化重新渲染
oldState = newState;
});
renderApp(store.getState());
// 三秒钟之后,修改标题和标题颜色,并重新渲染
setTimeout(function () {
store.dispatch({ type: "UPDATE_TITLE_TEXT", text: "Redux是React是好基友" });
store.dispatch({ type: "UPDATE_TITLE_COLOR", color: "green" });
// renderApp(store.getState());
}, 3000);

</script>
</body>

</html>

打开页面,会发现只执行了第一次渲染,而后面的两次更新根本就不执行了,why???????

我们知道在 JavaScript 函数中,所有的参数都是值传递,参数为基本类型时传递的直接就是值,参数为对象时,参数的值就是对象所在的内存地址空间,看下我们修改 state 的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function stateChanger(state, action) {
switch (action.type) {
case "UPDATE_TITLE_TEXT": {
state.title.text = action.text;
break;
}
case "UPDATE_TITLE_COLOR": {
state.title.color = action.color;
break;
}
default:
break;
}
}

通过上面的代码可以看到,我们只是修改了 state.title 中的 text 和 color 属性,而 title 对象的内存地址依然原来的,state 的内存地址也依然是原来,所以 newState 和 oldState 都是指向同一个内存地址,所以 newAppState === oldAppStatetrue,所以也就不会触发新的渲染。

内存空间详细图解

怎么办??????

是不是可以通过浅复制生成一个新的对象,然后将修改的部分覆盖到这个新的对象上,这样既可以保证没有被修改的对象内存地址保持不变,而被修改的对象又可以获得新的地址,继而触发渲染。

每次修改某些数据的时候,不去改变原来的数据,而是把需要修改数据对象都 copy 一个出来,然后再去修改新生成的数据。如上图所示,content 对象就可以在不同的阶段进行共享。

根据这个思路,来修改 stateChanger:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function stateChanger(state, action) {
switch (action.type) {
case "UPDATE_TITLE_TEXT":
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
text: action.text
}
};
case "UPDATE_TITLE_COLOR":
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
color: action.color
}
};
default:
return state; // 没有修改,返回原来的对象
}
}

每次需要修改的时候都会产生新的对象,并且返回。而如果没有修改(在 default 语句中)则返回原来的 state 对象。

因为 stateChanger 不会修改原来对象了,而是返回对象,所以我们需要修改一下 createStore。让它用每次 stateChanger(state, action) 的调用结果覆盖原来的 state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createStore(state, stateChanger) {
let events = [];
return {
subscribe: (event) => {
events.push(event);
},
dispatch: (action) => {
// 每次修改之后, 获取新的state
state = stateChanger(state, action);
events.forEach((event) => event());
},
getState: () => 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
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
112
113
114
115
116
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>测试</title>
</head>

<body>
<div id='title'></div>
<div id='content'></div>

<script>
const appState = {
title: {
text: "redux",
color: "red",
},
content: {
text: "redux文档内容",
color: "blue"
}
};

function stateChanger(state, action) {
switch (action.type) {
case "UPDATE_TITLE_TEXT":
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
text: action.text
}
};
case "UPDATE_TITLE_COLOR":
return { // 构建新的对象并且返回
...state,
title: {
...state.title,
color: action.color
}
};
default:
return state; // 没有修改,返回原来的对象
}
}

// 添加 createStore
function createStore(state, stateChanger) {
let events = [];
return {
subscribe: (event) => {
events.push(event);
},
dispatch: (action) => {
// 每次修改之后, 获取新的state
state = stateChanger(state, action);
events.forEach((event) => event());
},
getState: () => state
};
}

function renderTitle(newTitle, oldTile = {}) {
if (newTitle === oldTile) {
return false;// 数据没有变化就不渲染了
}
console.log("render title...");
const titleDom = document.querySelector("#title");
titleDom.innerHTML = newTitle.text;
titleDom.style.color = newTitle.color;
}

function renderContent(newContent, oldContent = {}) {
if (newContent === oldContent) {
return false;// 数据没有变化就不渲染了
}
console.log("render content...");
const contentDom = document.querySelector("#content");
contentDom.innerHTML = newContent.text;
contentDom.style.color = newContent.color;
}

function renderApp(newAppState, oldAppState = {}) {
if (newAppState === oldAppState) {
return false;// 数据没有变化就不渲染了
}
console.log("render app...");
renderTitle(newAppState.title, oldAppState.title);
renderContent(newAppState.content, oldAppState.content);
}

// 生成 store
let store = createStore(appState, stateChanger);
// 缓存旧的state
let oldState = store.getState();
// 监听数据变化
store.subscribe(() => {
// 获取新的 state
let newState = store.getState();
// 把新旧的 state 传进去渲染
renderApp(newState, oldState);
// 新的 newState 变成了旧的 oldState,等待下一次数据变化重新渲染
oldState = newState;
});
renderApp(store.getState());
// 三秒钟之后,修改标题和标题颜色,并重新渲染
setTimeout(function () {
store.dispatch({ type: "UPDATE_TITLE_TEXT", text: "Redux是React是好基友" });
store.dispatch({ type: "UPDATE_TITLE_COLOR", color: "green" });
// renderApp(store.getState());
}, 3000);

</script>
</body>

</html>

刷新页面,然后打开控制台看下log

另外,并不需要担心每次修改都新建共享结构对象会有性能、内存问题,因为构建对象的成本非常低,而且我们最多保存两个对象引用(oldState 和 newState),其余旧的对象都会被垃圾回收掉。

参考

http://huziketang.mangojuice.top/books/react/