React-Redux 的实现

React-Redux 的实现

接上回 Redux 的实现,我们在之前的章节当中,从一个简单的示例开始一步一步推导出 Redux 的实现方式,但是之前我们也提到过,它其实跟 React 一点关系都没有,所以在本章当中我们会把 ReactRedux 结合起来,用 Redux 模式帮助我们来管理 React 的应用状态

在前端当中应用的状态存在的问题就是一个状态可能被多个组件依赖或者影响,而 React 并没有提供好的解决方案,我们只能把状态提升到依赖或者影响这个状态的所有组件的公共父组件上,也就是我们可以把共享状态放到父组件的 Context 上,让这个父组件以下的所有组件都可以从 Context 中直接获取到状态而不需要一层层地进行传递了

但是直接从 Context 里面存放、获取数据增强了组件的耦合性,并且所有组件都可以修改 Context 里面的状态就像谁都可以修改共享状态一样,导致程序运行的不可预料,既然这样,我们为什么不把 Contextstore 结合起来呢?毕竟 store 的数据不能直接被修改,而是约定只能通过 dispatch 来进行修改,这样的话每个组件既可以去 Context 里面获取 store 从而获取状态,又不用担心它们乱改数据

最终完整代码可见 react-redux 的手动实现

初始化

所以我们就来尝试一下,就拿官方文档当中那个主题色的示例,稍微的调整一下,比如我们要做下面这样的组件树

1
2
3
4
└─ App                         
├─ Header
└─ Content
└─ ThemeSwitch - Button x 2

主体容器 App 它有两个子组件 HeaderContentHeaderContent 的组件的文本内容会随着主题色的变化而变化,而 Content 下的子组件 ThemeSwitch 有两个按钮,可以切换红色和蓝色两种主题,按钮的颜色也会随着主题色的变化而变化,各组件代码如下

1
2
3
4
5
export default class Header extends Component {
render() {
return <h2>标题</h2>
}
}
1
2
3
4
5
6
7
8
9
10
export default class Content extends Component {
render() {
return (
<div>
<div>内容</div>
<ThemeSwitch />
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
export default class ThemeSwitch extends Component {
render() {
return (
<div>
<button>Red</button>
<button>Blue</button>
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
class App extends Component {
render() {
return (
<div>
<Header />
<Content />
</div>
)
}
}

当然现在文本是没有颜色的,而且点击按钮也不会有什么反应,这些内容我们会在后面慢慢来进行完善

结合 Context 和 Store

下面我们就来构建 store,用的也就是我们在之前章节当中实现的 createStore 方法,然后我们在构建一个 themeReducer 来生成一个 store,如下

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 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 themeReducer = (state, action) => {
if (!state) return {
themeColor: 'red'
}
switch (action.type) {
case 'CHANGE_COLOR':
return { ...state, themeColor: action.themeColor }
default:
return state
}
}

const store = createStore(themeReducer)

// ...

themeReducer 定义了一个表示主题色的状态 themeColor,并且规定了一种操作 CHNAGE_COLOR,只能通过这种操作修改颜色,现在我们把 store 放到 AppContext 里面,这样每个子组件都可以获取到 store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class App extends Component {
static childContextTypes = {
store: PropTypes.object
}

getChildContext() {
return { store }
}

render() {
return (
<div>
<Header />
<Content />
</div>
)
}
}

export default App

这里有一个看上去可能有些疑惑的 childContextTypes,它的作用其实与 propsType 验证组件 props 参数的作用类似,不过它验证的是 getChildContext 返回的对象,那么为什么又要验证 Context 呢?

这是因为 Context 是一个危险的特性,所以 React 团队将其使用方式设置的复杂一些,提高使用门槛的同时也会让你注意到它的危险性,如果你要给组件设置 Context,那么 childContextTypes 是必写的(现在可以使用 useContext() 来简化我们的操作)

下面我们就可以来调整 Header 组件,让它从 Context 里面获取 store,并且获取里面的 themeColor 状态来设置自己的颜色

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
export default class Header extends Component {
static contextTypes = {
store: PropTypes.object
}

constructor() {
super()
this.state = {
themeColor: ''
}
}

componentDidMount() {
const { store } = this.context
this._updateThemeColor()
store.subscribe(() => this._updateThemeColor())
}

_updateThemeColor() {
const { store } = this.context
const state = store.getState()
this.setState({ themeColor: state.themeColor })
}

render() {
return <h2 style={{ color: this.state.themeColor }}>标题</h2>
}
}

同样的,作为子组件的 Header,想要获取 Context 里面的内容的话,就必须写 contextTypes 来声明和验证你需要获取的状态的类型,它也是必写的,如果你不写就无法获取 Context 里面的状态

运行完成以后可以发现,标题的颜色已经变成了红色,其实上面的代码逻辑很简单,我们在 constructor 里面初始化了组件自己的 themeColor 状态,然后在生命周期中调用 _updateThemeColor,_updateThemeColor 会从 Context 里面把 store 取出来,然后通过 store.getState() 获取当前的 state 对象,并且用里面的 themeColor 字段设置组件的 state.themeColor

同时通过 store.subscribe 进行监听,在数据变化的时候重新调用 _updateThemeColor,而 _updateThemeColor 会去 store 里面取最新的 themeColor 然后通过 setState 重新渲染组件

同理,我们将 Content 组件和 ThemeSwitch 组件也调整成从 store 当中来获取主题色

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
export default class Content extends Component {
static contextTypes = {
store: PropTypes.object
}

constructor() {
super()
this.state = {
themeColor: ''
}
}

componentDidMount() {
const { store } = this.context
this._updateThemeColor()
store.subscribe(() => this._updateThemeColor())
}

_updateThemeColor() {
const { store } = this.context
const state = store.getState()
this.setState({ themeColor: state.themeColor })
}


render() {
return (
<div>
<div style={{ color: this.state.themeColor }}>内容</div>
<ThemeSwitch />
</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
30
31
32
33
export default class ThemeSwitch extends Component {
static contextTypes = {
store: PropTypes.object
}

constructor() {
super()
this.state = {
themeColor: ''
}
}

componentDidMount() {
const { store } = this.context
this._updateThemeColor()
store.subscribe(() => this._updateThemeColor())
}

_updateThemeColor() {
const { store } = this.context
const state = store.getState()
this.setState({ themeColor: state.themeColor })
}

render() {
return (
<div style={{ color: this.state.themeColor }}>
<button>Red</button>
<button>Blue</button>
</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
export default class ThemeSwitch extends Component {

// ...

handleSwitchColor(color) {
const { store } = this.context
store.dispatch({
type: 'CHANGE_COLOR',
themeColor: color
})
}

render() {
return (
<div>
<button
style={{ color: this.state.themeColor }}
onClick={this.handleSwitchColor.bind(this, 'red')}
>Red</button>
<button
style={{ color: this.state.themeColor }}
onClick={this.handleSwitchColor.bind(this, 'blue')}
>Blue</button>
</div>
)
}
}

我们给两个按钮都加上了 onClick 事件监听,并绑定到了 handleSwitchColor 方法上,在点击的时候分别给这个方法传入不同的颜色,handleSwitchColor 会根据传入的颜色来 store.dispatch 一个 action 去修改颜色

如此一来,我们就完成了自由的切换主题颜色的功能了,但是其中还有不少可以优化的地方,我们下面慢慢来看

Connect 和 mapStateToProps

我们仔细观察我们之前设计的组件,发现有两个比较严重的问题

  1. 有大量重复的逻辑,它们基本的逻辑都是取出 Context,得到里面的 store,然后用里面的状态设置自己的状态,这些代码逻辑其实都是相同的
  2. Context 依赖性过强,这些组件都要依赖 Context 来取数据,使得这个组件复用性基本为零

所以我们需要针对以上两点问题来进行处理,关于第一点,我们都知道在 React 当中有一个 HOC(高阶组件)的概念,我们可以把一些可复用的逻辑放在高阶组件当中,高阶组件包装的新组件和原来组件之间通过 props 传递信息,减少代码的重复程度

至于第二点,我们可以将其改写成为 UI 组件,关于 UI 组件,也称为 Dumb Component,因为你传递给它什么,它就渲染什么出来,对参数(props)以外的数据零依赖,也不产生副作用,所以我们需要高阶组件来帮助我们从 Context 取数据,使用高阶组件和 Context 打交道,把里面数据取出来通过 props 传给 UI 组件,也就是如下图当中所示

我们把这个高阶组件起名为 connect,因为它把 UI 组件和 Context 连接(connect)起来了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component } from 'react'
import PropTypes from 'prop-types'

export const connect = (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}

render() {
return <WrappedComponent />
}
}

return Connect
}

connect 函数接受一个组件 WrappedComponent 作为参数,把这个组件包含在一个新的组件 Connect 里面,Connect 会去 Context 里面取出 store,现在要把 store 里面的数据取出来通过 props 传给 WrappedComponent,但是每个传进去的组件需要 store 里面的数据都不一样的,所以还需要告诉高级组件我们需要什么数据,高阶组件才能正确地去取数据

为了解决这个问题,我们需要一个映射函数来告诉 store 如何返回我们需要的数据,我们将其命名为 mapStateToProps,如下

1
2
3
4
5
6
7
const mapStateToProps = (state) => {
return {
themeColor: state.themeColor,
themeName: state.themeName,
// ...
}
}

这个函数会接受 store.getState() 的结果作为参数,然后返回一个对象,这个对象是根据 state 生成的,也就是我们使用 mapStateTopProps 去告知 Connect 应该如何去 store 里面取数据,然后得到我们需要的数据以后,再把这个函数的返回结果传给被包装的组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from 'react'
import PropTypes from 'prop-types'

export const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}

render() {
const { store } = this.context
let stateProps = mapStateToProps(store.getState())
return <WrappedComponent {...stateProps} />
}
}

return Connect
}

其中的 {...stateProps} 意思是把这个对象里面的属性全部通过 props 方式传递进去,connect 现在接受一个参数 mapStateToProps,然后返回一个函数,这个返回的函数才是高阶组件,它会接受一个组件作为参数,然后用 Connect 把组件包装以后再返回,connect 的用法是

1
2
3
4
5
6
7
8
9
10
11
// ...

const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}

Header = connect(mapStateToProps)(Header)

// ...

我们把上面 connect 的函数代码单独分离到一个模块当中,再把之前的监听数据变化重新渲染的逻辑放到其中调整一下,并将其取名为 react-redux.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
export const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}

constructor() {
super()
this.state = { allProps: {} }
}

componentDidMount() {
const { store } = this.context
this._updateProps()
store.subscribe(() => this._updateProps())
}

_updateProps() {
const { store } = this.context
// 额外传入 props,让获取数据更加灵活方便
let stateProps = mapStateToProps(store.getState(), this.props)
this.setState({
// 整合普通的 props 和从 state 生成的 props
allProps: {
...stateProps,
...this.props
}
})
}

render() {
return <WrappedComponent {...this.state.allProps} />
}
}

return Connect
}

我们在 Connect 组件的 constructor 里面初始化了 state.allProps,它是一个对象,用来保存需要传给被包装组件的所有的参数,为了让 connect 返回新组件和被包装的组件使用参数保持一致,我们会把所有传给 Connectprops 原封不动地传给 WrappedComponent,所以在 _updateProps 里面会把 statePropsthis.props 合并到 this.state.allProps 里面,再通过 render 方法把所有参数都传给 WrappedComponent

mapStateToProps 也发生点变化,它现在可以接受两个参数了,我们会把传给 Connect 组件的 props 参数也传给它,那么它生成的对象配置性就更强了,我们可以根据 store 里面的 state 和外界传入的 props 生成我们想传给被包装组件的参数,接下来我们就可以在 Header 当中来进行使用了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Header extends Component {
static propTypes = {
themeColor: PropTypes.string
}

render() {
return <h2 style={{ color: this.props.themeColor }}>标题</h2>
}
}

const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}

Header = connect(mapStateToProps)(Header)

export default Header

如上,可以发现我们在 Header 当中删掉了大部分关于 Context 的代码,它除了 props 什么也不依赖,所以它是一个纯粹的 UI 组件,只需要通过 connect 来取得数据,但是我们不需要知道 connect 是怎么和 Context 打交道的,所以只需要传递一个 mapStateToProps 告诉它应该怎么取数据就可以了,再用同样的方式来修改 Content

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Content extends Component {
static propTypes = {
themeColor: PropTypes.string
}

render() {
return (
<div>
<div style={{ color: this.props.themeColor }}>内容</div>
<ThemeSwitch />
</div>
)
}
}

const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}

Content = connect(mapStateToProps)(Content)

export default Content

修改以后再次刷新界面,发现功能还是跟之前一样,但是我们的 HeaderContent 的代码都大大减少了,但是我们的事情并没有做完,接下来我们还需要继续重构 ThemeSwitch

mapDispatchToProps

在重构 ThemeSwitch 的时候我们发现,ThemeSwitch 除了需要 store 里面的数据以外,还需要 storedispatch,但是我们目前版本的 connect 是达不到这个效果的,所以我们需要改进它,但是仔细一想,既然可以通过给 connect 函数传入 mapStateToProps 来告诉它如何获取、整合状态,那么我们也可以给它传入另外一个参数来告诉它我们的组件需要如何触发 dispatch 的,我们把这个参数叫 mapDispatchToProps

1
2
3
4
5
6
7
const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({ type: 'CHANGE_COLOR', themeColor: color })
}
}
}

mapStateToProps 一样,它返回一个对象,这个对象内容会同样被 connect 当作是 props 参数传给被包装的组件,而不一样的是这个函数不是接受 state 作为参数,而是 dispatch,你可以在返回的对象内部定义一些函数,这些函数会用到 dispatch 来触发特定的 action,所以我们调整 connect 让它能接受这样的 mapDispatchToProps

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
import React, { Component } from 'react'
import PropTypes from 'prop-types'

export const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}

constructor() {
super()
this.state = { allProps: {} }
}

componentDidMount() {
const { store } = this.context
this._updateProps()
store.subscribe(() => this._updateProps())
}

_updateProps() {
const { store } = this.context
let stateProps = mapStateToProps
? mapStateToProps(store.getState(), this.props)
: {}
let dispatchProps = mapDispatchToProps
? mapDispatchToProps(store.dispatch, this.props)
: {}
this.setState({
allProps: {
...stateProps,
...dispatchProps,
...this.props
}
})
}

render() {
return <WrappedComponent {...this.state.allProps} />
}
}
return Connect
}

_updateProps 内部,我们把 store.dispatch 作为参数传给 mapDispatchToProps,它会返回一个对象 dispatchProps,接着把 statePropsdispatchPropsthis.props 三者合并到 this.state.allProps 里面去,这三者的内容都会在 render 函数内全部传给被包装的组件

这时候我们就可以重构 ThemeSwitch,让它摆脱 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
class ThemeSwitch extends Component {
static propTypes = {
themeColor: PropTypes.string,
onSwitchColor: PropTypes.func
}

handleSwitchColor(color) {
if (this.props.onSwitchColor) {
this.props.onSwitchColor(color)
}
}

render() {
return (
<div>
<button
style={{ color: this.props.themeColor }}
onClick={this.handleSwitchColor.bind(this, 'red')}
>Red</button>
<button
style={{ color: this.props.themeColor }}
onClick={this.handleSwitchColor.bind(this, 'blue')}
>Blue</button>
</div>
)
}
}

const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}

const mapDispatchToProps = (dispatch) => {
return {
onSwitchColor: (color) => {
dispatch({ type: 'CHANGE_COLOR', themeColor: color })
}
}
}

ThemeSwitch = connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch)

export default ThemeSwitch

现在的 ThemeSwitch 只依赖外界传进来的 themeColoronSwitchColor,但是 ThemeSwitch 内部并不知道这两个参数其实都是我们去 store 里面取的,此时我们的三个组件的重构都已经完成了,代码大大减少、不依赖 Context,并且功能和原来一样

Provider

至此,我们的大体结构已经搭建的差不多了,但是还有一点就是我们能不能将和 Context 相关的代码从所有业务组件中清除出去,这样一来就可以保证我们的业务组件都是干净的,所以我们来稍微的重构一下我们的 App 组件

App 组件当中之所以需要用到 Context,就是因为要把 store 存放到里面,好让子组件 connect 的时候能够取到 store,所以我们可以额外构建一个单独的组件专门来做这件事情,然后让这个组件成为组件树的根节点,那么它的子组件都可以获取到 Context 了,我们把这个组件叫 Provider,因为它提供(provide)了 store

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export class Provider extends Component {
static propTypes = {
store: PropTypes.object,
children: PropTypes.any
}

static childContextTypes = {
store: PropTypes.object
}

getChildContext() {
return {
store: this.props.store
}
}

render() {
return <div>{this.props.children}</div>
}
}

Provider 做的事情也很简单,它就是一个容器组件,会把嵌套的内容原封不动作为自己的子组件渲染出来,它还会把外界传给它的 props.store 放到 Context,这样子组件 connect 的时候都可以获取到,下面我们再来调整 App 组件,也就是删除 App 里面所有关于 Context 的代码,整理过的 App 如下所示,可以发现现在已经变得很干净了

1
2
3
4
5
6
7
8
9
10
11
12
13
// 删除 App 里面所有关于 context 的代码
class App extends Component {
render() {
return (
<div>
<Header />
<Content />
</div>
)
}
}

export default App

这样我们就把所有关于 Context 的代码从组件里面删除了,然后将之前在 App 组件当中生成 store 等一系列操作移动到我们的主文件当中,如下

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
import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import 'antd/dist/antd.css'
import { Provider } from './Provider'

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 themeReducer = (state, action) => {
if (!state) return {
themeColor: 'red'
}
switch (action.type) {
case 'CHANGE_COLOR':
return { ...state, themeColor: action.themeColor }
default:
return state
}
}

const store = createStore(themeReducer)

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)

至此,我们的整个流程就算是走通了

组件划分

最后我们再来回过头看一下我们设计的组件,我们在之前的 Redux、Flux 和 React-Redux 三者之间的区别 章节当中曾经介绍过,React-Redux 将所有组件分成了两大类,UI 组件和容器组件,UI 组件基本只做一件事情,那就是根据 props 来进行渲染,而容器组件则是负责应用的逻辑、数据,把所有相关的 UI 组件组合起来,通过 props 控制它们

但是我们观察我们的 Header 组件,这个组件其实在执行 connect 之前它一直都是 UI 组件,就是因为 connect 了导致它和 Context 扯上了关系,导致它变成容器组件了,也使得这个组件没有了很好的复用性,所以我们需要来重构一下,我们在 src 目录下新建两个文件夹 componentscontainers

1
2
3
├─ App                         
│ ├─ components
│ └─ containers

我们规定所有的 UI 组件都放在 components/ 目录下,所有的容器组件都放在 containers/ 目录下,所以我们根据这个规则来重构我们的目录,这里以 Header 组件为例,我们将 components/ 文件夹下的 Header 组件调整为

1
2
3
4
5
6
7
8
9
10
11
12
import React, { Component } from 'react'
import PropTypes from 'prop-types'

export default class Header extends Component {
static propTypes = {
themeColor: PropTypes.string
}

render() {
return <h2 style={{ color: this.props.themeColor }}>标题</h2>
}
}

这样一来,重构后的 Header 是一个纯展示的 UI 组件,下面我们在对应的 container/ 文件夹下新建一个与其对应的容器组件,名字也为 Header

1
2
3
4
5
6
7
8
9
10
import { connect } from 'react-redux'
import Header from '../components/Header'

const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}

export default connect(mapStateToProps)(Header)

它引入 components/ 下的 Header 组件,经过 connect 包裹后返回一个新的 Header,就相当于我们把 Header 组件划分为了两部分,src/components/Header.js 下的负责渲染,而 src/containers/Header.js 则是跟业务相关的,同理,我们在分别重构 ThemeSwitchContent 组件,但是这里有一点需要注意的是,针对 Content 组件可以分为两种情况,即不复用和可复用,这里我们分情况来进行讨论

如果是不复用的情况下,将 Content 移至业务文件夹 container/ 下即可,最终的目录结构为

1
2
3
4
5
6
7
8
9
src
├── components
│ ├── Header.js
│ └── ThemeSwitch.js
├── containers
│ ├── Content.js
│ ├── Header.js
│ └── ThemeSwitch.js
└── App.js

如果可复用,那么 Content 的子组件 ThemeSwitch 就一定要是 UI 组件,所以在这种情况下就不能直接使用 connect 了,所以涉及到的 ThemeSwitch 的数据、onSwitchColor 函数等就要通过它的父组件传递进来,而不是通过 connect 获得,这样一来组件的划分就变为了

1
2
3
4
5
6
7
8
9
src
├── components
│ ├── Header.js
│ ├── Content.js
│ └── ThemeSwitch.js
├── containers
│ ├── Header.js
│ └── Content.js
└── App.js

这里我们为了简便,就直接采用了不复用的形式,但是我们可以发现,针对复用性的需求不同,我们划分组件的方式也有所不同,当然还有一点要注意,容器组件并不意味着完全不能复用,容器组件的复用性也是依赖场景的,在某些特定的应用场景下还是可以复用容器组件的,最终整合后的完整代码可见 组件划分

总结

我们来简单的总结一下我们在上面做了哪些事情,我们知道 store 里面的内容是不可以随意修改的,而是通过 dispatch 才能变更里面的 state,所以我们尝试把 storeContext 结合起来使用,可以兼顾组件之间共享状态问题和共享状态可能被任意修改的问题

在我们设计的第一个版本当中,因为 storeContext 结合有诸多缺陷,有大量的重复逻辑和对 Context 的依赖性过强,所以我们尝试通过构建一个高阶组件 connect 函数的方式,把所有的重复逻辑和对 Context 的依赖放在里面 connect 函数里面,而其他组件则仅仅只负责渲染(UI 组件),让 connectContext 打交道,然后通过 props 把参数传给普通的组件

而每个组件需要的数据和需要触发的 action 都不一样,所以我们调整了 connect,让它可以接受两个参数 mapStateToPropsmapDispatchToProps,分别用于告诉 connect 这个组件需要什么数据和需要触发什么 action

最后为了把所有关于 Context 的代码完全从我们业务逻辑里面清除掉,我们构建了一个 Provider 组件,Provider 作为所有组件树的根节点,外界可以通过 props 给它提供 store,它会把 store 放到自己的 Context 里面,好让子组件 connect 的时候都能够获取到

最后的最后,我们将我们的组件重新的划分了一遍,分为了 UI 组件和容器组件,UI 组件基本只做一件事情,那就是根据 props 来进行渲染,而容器组件则是负责应用的逻辑、数据,把所有相关的 UI 组件组合起来,通过 props 控制它们

当然,我们实现的这版 React-Redux 也是存在着一定问题的,比如不相关的数据变化的时候其实所有组件都会重新渲染的,不过在这里我们就不详细展开了,想了解更多的话可以参考之前整理过的一篇系列文章 Virtual DOM 与 Diff 算法内容总结 来了解更多,但是通过上面的示例,我们知道了为什么要 connect,为什么要 mapStateToPropsmapDispatchToProps,以及什么是 Provider,这样在接触官方的 React-Redux 的时候就会变得上手十分简单

# React

评论

Your browser is out-of-date!

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

×