Redux 的实现

Redux 的实现

最近在复习 React 的过程当中,在网上发现了一篇关于 Redux 手动实现方式的 系列文章,收益良多,所以打算在这里汇总整理一下,一方面加深一下对于 Redux 的理解,另一方面也方便以后可以经常回来温习温习,内容有所删减和补充,更多内容可以参考原文

我们都知道,ReduxReact-Redux 并不是同一个东西,Redux 是一种架构模式(Flux 架构的一种变种),它不关注你到底用什么库,你可以把它应用到 ReactVue,甚至跟 jQuery 结合都没有问题,而 React-Redux 就是把 Redux 这种架构模式和 React 结合起来的一个库,就是 Redux 架构在 React 中的体现(关于 React-Redux 的实现我们会在后面进行介绍)

所以在这里我们也就不直接介绍 Redux 当中的 reducersactionsstore 等这些 API 的关系和用法,而是从一个示例开始,一步一步来进行推演

共享状态的修改

我们先从示例的搭建开始,页面结构如下,很简单的两个容器,一个标题,一个内容

1
2
<div id='title'></div>
<div id='content'></div>

下面我们再来添加一些用于渲染的数据和几个渲染函数,它们的作用是将我们的数据渲染到上面的容器当中,如下

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
const appState = {
title: {
text: '标题',
color: 'red',
},
content: {
text: '内容',
color: 'blue'
}
}

function renderApp(appState) {
renderTitle(appState.title)
renderContent(appState.content)
}

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

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

renderApp(appState)

逻辑比较简单,我们调用了 renderApp() 方法,它会分别再去调用 rendeTitle()renderContent(),而这两者会把 appState 里面的数据通过原始的 DOM 操作更新到页面上,很明显的页面当中会显现红色的标题和蓝色的内容文字

内容虽然十分简单,但是这里存在一个重大隐患,那就是我们在渲染数据的时候,使用的是一个共享状态 appState,这也就意味着每个人都可以修改它,如果我们在 renderApp(appState) 之前执行了一大堆函数操作,你可能根本不知道它们会对 appState 做什么事情,所以 renderApp(appState) 的结果根本没法得到保障

所以我们来看看如何解决这个问题,针对这种情况我们约定,数据并不能直接去进行修改,如果想要修改,必须显式的声明你想要修改哪些数据,所以我们定义一个函数,叫 dispatch,它专门负责数据的修改

1
2
3
4
5
6
7
8
9
10
11
12
function dispatch(action) {
switch (action.type) {
case 'UPDATE_TITLE_TEXT':
appState.title.text = action.text
break
case 'UPDATE_TITLE_COLOR':
appState.title.color = action.color
break
default:
break
}
}

所有对数据的操作必须通过 dispatch 函数,它接受一个参数 action,这个 action 是一个普通的 JavaScript 对象,里面必须包含一个 type 字段来声明你到底想干什么,dispatchswtich 里面会识别这个 type 字段,能够识别出来的操作才会执行对 appState 的修改,上面的 dispatch 它只能识别两种操作

  • 一种是 UPDATE_TITLE_TEXT 它会用 actiontext 字段去更新 appState.title.text
  • 一种是 UPDATE_TITLE_COLOR,它会用 actioncolor 字段去更新 appState.title.color

任何的模块如果想要修改 appState.title.text,必须大张旗鼓地调用 dispatch

1
2
3
4
5
6
renderApp(appState)  // 首次渲染

dispatch({ type: 'UPDATE_TITLE_TEXT', text: 'newTitle' }) // 修改标题文本
dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'green' }) // 修改标题颜色

renderApp(appState) // 将新数据渲染到页面上

这样一来,我们就不需要担心在 renderApp(appState) 之前的某些函数操作,因为我们规定不能直接修改 appState,它们对 appState 的修改必须只能通过 dispatch,而我们看看 dispatch 的实现可以知道,你只能修改 title.texttitle.color,对于原来的模块(组件)修改共享数据的方式是可以直接修改的,也就如下图所示

我们很难把控每一根指向 appState 的箭头,appState 里面的东西就无法把控,但现在我们必须通过一个中间人(dispatch),所有的数据修改必须通过它

这样一来我们就不用担心共享数据状态的修改问题了,我们只要把控住 dispatch,所有对 appState 的修改就无所遁形,毕竟只有一根箭头指向 appState

监控数据变化

现在我们有了 appStatedispatch,所以我们将它们集中起来,起个名字叫做 store,然后构建一个函数 createStore,用来专门生产这种 statedispatch 的集合,这样别的 App 也可以用这种模式了

1
2
3
4
5
function createStore(state, stateChanger) {
const getState = () => state
const dispatch = (action) => stateChanger(state, action)
return { getState, dispatch }
}

createStore 接受两个参数,一个是表示应用程序状态的 state,另外一个是 stateChanger,它来描述应用程序状态会根据 action 发生什么变化,createStore 会返回一个对象,这个对象包含两个方法 getStatedispatchgetState 用于获取 state 数据,其实就是简单地把 state 参数返回,dispatch 用于修改数据,它会把 stateaction 一并传给 stateChanger

现在我们就可以利用 createStore 来修改数据生成的方式了,如下

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
const appState = {
title: {
text: '标题',
color: 'red',
},
content: {
text: '内容',
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
}
}

const store = createStore(appState, stateChanger)

renderApp(store.getState()) // 首次渲染

store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: 'newTitle' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'green' }) // 修改标题颜色

renderApp(store.getState()) // 将新数据渲染到页面上

针对每个不同的 App,我们可以给 createStore 传入初始的数据 appState,和一个描述数据变化的函数 stateChanger,然后生成一个 store,需要修改数据的时候通过 store.dispatch,需要获取数据的时候通过 store.getState

但是此时还存在一些问题,就是我们每次通过 dispatch 去修改数据的时候,都需要手动的调用 renderApp() 才能重新渲染页面,我们来稍微的调整一下,即通过监听的方式,一旦数据有所变化,就会自动的重新渲染页面,所以这里就会用到观察者模式

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) => {
stateChanger(state, action)
listeners.forEach((listener) => listener())
}
return { getState, dispatch, subscribe }
}

我们在 createStore 里面定义了一个数组 listeners,还有一个新的方法 subscribe,通过 store.subscribe(listener) 的方式给 subscribe 传入一个监听函数,这个函数会被 push 到数组当中

每当我们 dispatch 的时候,除了会调用 stateChanger 进行数据的修改,还会遍历 listeners 数组里面的函数,然后一个个地去调用,这样我们就可以在每当数据变化时候进行重新渲染

1
2
3
4
5
6
7
8
9
const store = createStore(appState, stateChanger)
store.subscribe(() => renderApp(store.getState()))

renderApp(store.getState()) // 首次渲染

store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: 'newTitle' }) // 修改标题文本
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'green' }) // 修改标题颜色

// 后续操作就无需再次调用 store.getState()

共享数据的性能优化

如果细心观察我们之前的示例,是可以发现其中是有比较严重的性能问题,即每当更新数据的时候就重新渲染整个 App,比如我们之前只是修改了 Title 当中的内容,但是 Content 中的内容也同步会被更新

这里提出的解决方案是,在每个渲染函数执行渲染操作之前先做个判断,判断传入的新数据和旧的数据是不是相同,相同的话就不渲染了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function renderApp(newAppState, oldAppState = {}) {
if (newAppState === oldAppState) return
renderTitle(newAppState.title, oldAppState.title)
renderContent(newAppState.content, oldAppState.content)
}

function renderTitle(newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
}

function renderContent(newContent, oldContent = {}) {
if (newContent === oldContent) return
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
}

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

1
2
3
4
5
6
7
8
9
const store = createStore(appState, stateChanger)

let oldState = store.getState() // 缓存旧的 state

store.subscribe(() => {
const newState = store.getState() // 数据可能变化,获取新的 state
renderApp(newState, oldState) // 把新旧的 state 传进去渲染
oldState = newState // 渲染完以后,从新赋值,等待下一次数据变化重新渲染
})

但是仔细观察我们的 state 可以发现,上面的代码根本无法达到我们想要的效果

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,每次调用 getState() 的时候返回的还是那个 state,所以我们来调整一下 stateChanger,让它修改数据的时候,并不会直接修改原来的数据 state,而是返回一个新的对象

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
}
}

因为 stateChanger 不会修改原来对象了,而是返回对象,所以我们需要修改一下 createStore,让它用每次 stateChanger(state, action) 的调用结果覆盖原来的 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 }
}

Reducer

现在我们已经有了一个比较通用的 createStore,但是我们还可以将 appStatestateChanger 合并到一起,如下

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
function stateChanger(state, action) {
if (!state) {
return {
title: {
text: '标题',
color: 'red',
},
content: {
text: '内容',
color: 'blue'
}
}
}
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
}
}

stateChanger 现在既充当了获取初始化数据的功能,也充当了生成更新数据的功能,如果有传入 state 就生成更新数据,否则就是初始化数据,这样一来我们就可以优化 createStore 成一个参数,因为 statestateChanger 合并到一起了

1
2
3
4
5
6
7
8
9
10
11
12
function createStore(stateChanger) {
let state = null
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = stateChanger(state, action)
listeners.forEach((listener) => listener())
}
dispatch({}) // 初始化 state
return { getState, dispatch, subscribe }
}

createStore 内部的 state 不再通过参数传入,而是一个局部变量 let state = nullcreateStore 的最后会手动调用一次 dispatch({})dispatch 内部会调用 stateChanger,这时候的 statenull,所以这次的 dispatch 其实就是初始化数据了,createStore 内部第一次的 dispatch 导致 state 初始化完成,后续外部的 dispatch 就是修改数据的行为了

最后,我们给 stateChanger 起一个比较通用的名字,那就是 reducer,所以我们最终版本的 createStore 如下

1
2
3
4
5
6
7
8
9
10
11
12
function createStore(reducer) {
let state = null
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach((listener) => listener())
}
dispatch({}) // 初始化 state
return { getState, dispatch, subscribe }
}

createStore 接受一个叫 reducer 的函数作为参数,这个函数规定是一个纯函数,它接受两个参数,一个是 state,一个是 action

  • 如果没有传入 state 或者 statenull,那么它就会返回一个初始化的数据
  • 如果有传入 state 的话,就会根据 action 来修改数据(其实是返回一个合并后的新对象)
  • 如果它不能识别你的 action,它就不会产生新的数据,而是(在 default 内部)把 state 原封不动地返回

reducer 是不允许有副作用的,你不能在里面操作 DOM,也不能发 Ajax 请求,更不能直接修改 state,它要做的仅仅只是初始化和计算新的 state

总结

至此,我们的 createStore 可以直接拿来使用了,方式就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 定一个 reducer
function reducer(state, action) {
// 初始化 state 和 switch case
}

// 生成 store
const store = createStore(reducer)

// 监听数据变化重新渲染页面
store.subscribe(() => renderApp(store.getState()))

// 首次渲染页面
renderApp(store.getState())

// 后面可以随意 dispatch 了,页面自动更新
store.dispatch(...)

最终的汇总代码如下

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
function renderApp(newAppState, oldAppState = {}) {
if (newAppState === oldAppState) return
renderTitle(newAppState.title, oldAppState.title)
renderContent(newAppState.content, oldAppState.content)
}

function renderTitle(newTitle, oldTitle = {}) {
if (newTitle === oldTitle) return
const titleDOM = document.getElementById('title')
titleDOM.innerHTML = newTitle.text
titleDOM.style.color = newTitle.color
}

function renderContent(newContent, oldContent = {}) {
if (newContent === oldContent) return
const contentDOM = document.getElementById('content')
contentDOM.innerHTML = newContent.text
contentDOM.style.color = newContent.color
}

function stateChanger(state, action) {
if (!state) {
return {
title: {
text: '标题',
color: 'red',
},
content: {
text: '内容',
color: 'blue'
}
}
}
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
}
}

function createStore(reducer) {
let state = null
const listeners = []
const subscribe = (listener) => listeners.push(listener)
const getState = () => state
const dispatch = (action) => {
state = reducer(state, action)
listeners.forEach((listener) => listener())
}
dispatch({})
return { getState, dispatch, subscribe }
}

const store = createStore(stateChanger)

let oldState = store.getState()

store.subscribe(() => {
const newState = store.getState()
renderApp(newState, oldState)
oldState = newState
})

renderApp(store.getState())

store.dispatch({ type: 'UPDATE_TITLE_TEXT', text: 'newTitle' })
store.dispatch({ type: 'UPDATE_TITLE_COLOR', color: 'green' })

在上面我们虽然手动实现了 Redux 的整体流程,但是需要注意的是,现在的实现跟 React 一点关系都没有,但是接下来我们会把 ReactRedux 结合起来,用 Redux 模式帮助我们来管理 React 的应用状态,其实也就是 React-Redux 的实现,关于这部分内容,我们会另起篇幅来进行介绍

# React

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×