实现MyRedux(二)

纯函数(Pure Function)

简单说,一个函数的返回结果只依赖于他的参数,并且执行过程中没有副作用,就是纯函数

从上面看,纯函数需要两个条件:

  • 函数的返回结果只依赖于它的参数
  • 函数的执行过程没有副作用

现在分别看一下两个条件

  1. 函数的返回结果只依赖于它的参数

    1
    2
    3
    const a = 1
    const foo = (b) => a + b
    foo(2) // => 3

    foo就不是一个纯函数,因为他的返回值要依赖外部变量a,我们不知道a的情况下,不能保证foo(2)的返回值是3,如果a变化,那返回值是不可预料的。

    1
    2
    3
    const a = 1
    const foo = (x, b) => x + b
    foo(1, 2) // => 3

    修改一下foo,现在函数的返回结果只依赖于他的参数,所以是纯函数

  2. 函数执行过程没有副作用

再修改一下foo

1
2
3
4
5
6
7
const a = 1
const foo = (obj, b) => {
return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 3
counter.x // => 1

把原来的x变成了一个对象,可以往里面传一个对象进行计算,过程中不会对传入的对象进行更改,counter不会发生变化,所以是纯函数,但是如果我们稍微修改它一下:

1
2
3
4
5
6
7
8
const a = 1
const foo = (obj, b) => {
obj.x = 2
return obj.x + b
}
const counter = { x: 1 }
foo(counter, 2) // => 4
counter.x // => 2

foo内部添加了一句obj.x = 2,计算前counter.x是1,但是计算以后的counter.x是2。函数内的执行对外部产生了影响,也叫副作用,所以不是纯函数

只要函数执行产生了外部可以观察到的变化,就是副作用,像调用DOM API修改页面,发送AJAX请求,甚至console.log也是副作用


优化共享结构的对象

其实之前的部分已经可以算是简单实现了一个redux,但是如果细心就会发现,之前的版本中有很严重的性能问题,如果我们试着在每个渲染函数开头输出一些日志看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function renderApp (appState) {
console.log('render app...')
renderTitle(appState.title)
renderContent(appState.content)
}

function renderTitle (title) {
console.log('render title...')
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = title.text
titleDOM.style.color = title.color
}

function renderContent (content) {
console.log('render content...')
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = content.text
contentDOM.style.color = content.color
}

执行一次初始化渲染和两次更新

1
2
3
4
5
6
const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState())) // 监听数据变化

renderApp(store.getState()) // 首次渲染页面
store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: 'React.js' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'blue' }) // 修改标题颜色

在控制塔瞄一眼呗:

看得出来一组三个render函数输出了三次,第一次肯定是首次渲染,后边两次分别是两次store.dispatch导致的。可以看出每次更新数据都要重新渲染整个App,但是我们两次更新都只是更新了title字段,并不需要重新调用renderContent,它是一个冗余的操作,现在需要优化它

提出一种解决方法(但不限于一种),在每个渲染函数调用之前判断一下传入的数据和旧数据是否相同,相同就不调用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function renderApp (newAppState, oldAppState = {}) { // 防止 oldAppState 没有传入,所以加了默认参数 oldAppState = {}
if (newAppState === oldAppState) return // 数据没有变化就不渲染了
console.log('render app...')
renderTitle(newAppState.title, oldAppState.title)
renderContent(newAppState.content, oldAppState.content)
}

function renderTitle (newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return // 数据没有变化就不渲染了
console.log('render title...')
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
}

function renderContent (newContent, oldContent = {}) {
if (newContent === oldContent) return // 数据没有变化就不渲染了
console.log('render content...')
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
}

然后用一个oldState 来保存旧的应用状态,在需要重新渲染的地方把新旧数据传进去:

1
2
3
4
5
6
7
8
const store = createStore(appState, stateChanger)
let oldState = store.getState() // 缓存旧的 state
store.subscribe(() => {
const newState = store.getState() // 数据可能变化,获取新的 state
renderApp(newState, oldState) // 把新旧的 state 传进去渲染
oldState = newState // 渲染完以后,新的 newState 变成了旧的 oldState,等待下一次数据变化重新渲染
})
...

可别以为这样就好了……,看看stateChanger

1
2
3
4
5
6
7
8
9
10
11
12
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,但是state还是原来那个state,一切都是以前的一切,这些引用指向的还是于谦的对象,只是内容改变了而已,就像下边这个语句:

1
2
3
4
5
6
let obj = {
x: 0
}
let obj2 = obj
obj2.x = 1
obj !== obj2 // false ,两个引用指向同一个对象,怎么会不相等?

想让两者成为两个完全不同的对象,就得使用ES6的语法

1
2
const obj = {a:1}
const obj2 = {...obj}

放到例子中就是

1
2
3
4
5
6
7
let newAppState = { // 新建一个 newAppState
...appState, // 复制 appState 里面的内容
title: { // 用一个新的对象覆盖原来的 title 属性
...appState.title, // 复制原来 title 对象里面的内容
text: 'new text' // 覆盖 text 属性
}
}

用一个图来表示对象结构

appStatenewAppState是两个不同的对象,因为浅复制所以两个对象的content指向同一对象,但是title被一个新的对象覆盖,所以指向不同,同样的可修改appState.title.color

1
2
3
4
5
6
7
let newAppState1 = { // 新建一个 newAppState1
...newAppState, // 复制 newAppState1 里面的内容
title: { // 用一个新的对象覆盖原来的 title 属性
...newAppState.title, // 复制原来 title 对象里面的内容
color: "blue" // 覆盖 color 属性
}
}

这样每次我们修改某些数据的时候,都不会碰原来的数据,每次都是copy的一个新对象。现在我们可以修改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 // 没有修改,返回原来的对象
}
}
1
2
3
4
5
6
7
8
9
10
function createStore (state, stateChanger) {
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = stateChanger(state, action) // 覆盖原对象
listeners.forEach((listener) => listener())
}
return { getState, dispatch, subscribe }
}

现在再看一眼控制台:

这样我们就能避免不需要的渲染了!过两天再往下走……



实现MyRedux(二)
https://moewang0321.github.io/2019/12/04/2019-12-04-Redux(二)/
作者
Moe Wang
发布于
2019年12月4日
许可协议