简介
这一次总结的是 React-redux 的实现,可以参考一下 大佬的文章 。
首先要知道
redux
的基本使用:
-
创建一个
Store
。<!-- store --> 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 } } <!-- reducer --> const themeReducer = (state = {}, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, themeColor: action.themeColor } default: return state } } <!-- 创建 store --> const store = createStore(themeReducer) 复制代码
Store
是保存数据的地方,整个应用只有一个,调用CreateStore
函数并且传入一个Reducer
来创建一个Store
,并且会返回新的Store
对象。 -
获取当前的
State
。<!-- 调用 store.getState 获取当前的状态 --> const state = store.getState() 复制代码
State
是Store
里面包含的数据对象,可以通过Store.getState()
获取 -
通过
Dispatch
发送Action
改变State
。<!-- 调用 dispatch 发送 action --> store.dispatch({ type: 'CHANGE_COLOR', themeColor: 'blue' }) 复制代码
Action
就是View
发出的通知,表示View
要变化,其中Type
是必须的,其余可以 自定义 。如果要写多个
Action
觉得麻烦,可以使用Action Creator
函数来生产Action
。function updateThemeColor (action) { type: action.type, themeColor: action.themeColor } store.dispatch( updateThemeColor({ type: 'CHANGE_COLOR', themeColor: 'blue' }) ) 复制代码
-
Reducer
是Store
收到Action
之后用来计算State
并且返回新的State
,也就是说必须要有Return
。<!-- reducer --> <!-- 初始 state 是必须的,redux 规定不能为 undefined 和 null --> const themeReducer = (state = {}, action) => { switch (action.type) { case 'CHANGE_COLOR': return { ...state, themeColor: action.themeColor } default: return state } } 复制代码
Reducer
可以根据不同的Type
来进行不同的逻辑处理,并且每次都会返回新的state
来覆盖原来的state
。Reducer
是一个纯函数,同样的输入就会得到同样的输出。Reducer
必须要返回一个新的状态,而不是改变原有的状态,请参考下面写法:// State 是一个对象 function reducer(state, action) { return Object.assign({}, state, { thingToChange }); // 或者 return { ...state, ...newState }; } // State 是一个数组 function reducer(state, action) { return [...state, newItem]; } 复制代码
-
调用
subscribe
传入一个函数,状态改变时会调用此函数。store.subscribe(()=> { ReactDOM.render() }) 复制代码
Store.subscribe
方法设置监听函数,一旦State
发生变化,就自动执行这个函数。一般传入
render
和this.setState()
来监听页面重新渲染。调用此方法会返回一个函数,调用函数之后可以解除监听。
React-redux
首先之前向组件传递参数时,第一次使用的是 状态提升,即通过父级传入一个函数然后拿到组件里面的东西,再传入另一个组件里面。
当嵌套的太多层时,使用 状态提升 会非常麻烦,然后第二次就开始使用了 Context ,由于 Context 能随意被改变,这时我们可以把 Context 和 Store 结合使用,这样就不能随意的改变 Context ,并且状态还能共享。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
// 引入组件
import { Header } from './component/Header';
import { Content } from './component/Content';
import * as serviceWorker from './serviceWorker';
<!-- store -->
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 };
}
<!-- reducer -->
const themeReducer = (state, action) => {
if (!state) return {
themeColor: 'red'
}
switch (action.type) {
case 'CHANGE_COLOR':
return { ...state, themeColor: action.themeColor }
default:
return state
}
}
<!-- 创建 store -->
const store = createStore(themeReducer);
class Index extends Component {
<!-- 设置子组件的 contextType -->
static childContextTypes = {
store: PropTypes.object
}
<!-- 设置 context -->
getChildContext() {
return { store }
}
render() {
return (
<div className="index">
<Header />
<Content />
</div>
)
}
}
ReactDOM.render(<Index />, document.getElementById('root'));
复制代码
创建 store 然后把它放到 context 里面,这样所有子组件都可以拿到了。
<!-- Header -->
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class Header extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor(props) {
super(props);
this.state = {
themeColor: ''
}
}
componentWillMount() {
let { store } = this.context;
this._updateThemeColor();
store.subscribe(() => this._updateThemeColor())
}
_updateThemeColor() {
let state = this.context.store.getState();
this.setState({
themeColor: state.themeColor
})
}
render() {
return (
<div className="header">
<h1 style={{color: this.state.themeColor}}>is header</h1>
</div>
)
}
}
export { Header };
<!-- Content-->
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { ThemeSwitch } from './ThemeSwitch';
class Content extends Component {
static contextTypes = {
store: PropTypes.object
}
componentWillMount() {
let { store } = this.context;
this._updateThemeColor();
store.subscribe(() => this._updateThemeColor())
}
_updateThemeColor() {
let state = this.context.store.getState();
this.setState({
themeColor: state.themeColor
})
}
render() {
return (
<div className="header">
<h2 style={{ color: this.state.themeColor }}>is Content</h2>
<ThemeSwitch />
</div>
)
}
}
export { Content };
<!-- ThemeSwitch -->
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class ThemeSwitch extends Component {
static contextTypes = {
store: PropTypes.object
}
componentWillMount() {
let { store } = this.context;
this._updateThemeColor();
store.subscribe(() => this._updateThemeColor())
}
_updateThemeColor() {
let state = this.context.store.getState();
this.setState({
themeColor: state.themeColor
})
}
render() {
return (
<div className="header">
<button style={{ color: this.state.themeColor }}>red</button>
<button style={{ color: this.state.themeColor }}>blue</button>
</div>
)
}
}
export { ThemeSwitch };
复制代码
上面三个子组件使用 store.getState() 获取到 reducer 设置的默认状态,这样的话就可以实现共享状态了。
接下来我们实现点击按钮改变颜色:
<!-- ThemeSwitch -->
updateThemeColor(color) {
let { store } = this.context;
store.dispatch({
type: 'CHANGE_COLOR',
themeColor: color
})
}
render() {
return (
<div className="header">
<button style={{ color: this.state.themeColor }} onClick={this.updateThemeColor.bind(this, 'red')}>red</button>
<button style={{ color: this.state.themeColor }} onClick={this.updateThemeColor.bind(this, 'blue')}>blue</button>
</div>
)
}
复制代码
调用 dispatch 然后传入 action ,然后会调用 reducer 函数,然后根据传入的 action.type 改变状态,之后再返回一个新的状态。
返回新状态时要想监听页面的更新,可以在 subscribe 传入要监听的函数,这样就可以在调用 dispatch 同时会调用你传入的函数,然后再一次调用 this.setState 触发页面重新渲染。
_updateThemeColor() {
<!-- 重新获取一次状态 -->
let state = this.context.store.getState();
<!-- 重新设置,并且触发重新渲染 -->
this.setState({
themeColor: state.themeColor
})
}
componentWillMount() {
let { store } = this.context;
<!-- 首次渲染 -->
this._updateThemeColor();
store.subscribe(() => this._updateThemeColor())
}
复制代码
connect
上面的组件有着重复的逻辑,首先取出 store 的 state 然后设置成自己的状态,还有一个就是对 context 依赖过强,这时我们可以利用 高阶组件 来和 context 打交道,这时就不用每个组件都获取一遍 store 了。
import React, { Component } from 'react';
import PropTypes from 'prop-types';
<!-- 接受一个组件 -->
const connect = (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
render() {
const { store } = this.context;
return <WrappedComponent />
}
}
return Connect;
}
export { connect };
复制代码
connect 是用于从 UI 组件生成 容器组件 ,也就是说我们传入的组件只是负责呈现和展示,而 容器组件 负责业务逻辑和带有内部状态,connect 负责的是将两者合并起来,生成并返回新的组件。
由于每个传进去的组件需要的 store 里面的数据都不一样,所以我们还要传入一个函数来告诉 高阶组件 正确获取数据。
- mapStateToProps
const mapStateToProps = (state) {
return {
themeColor: state.themeColor
}
}
复制代码
mapStateToProps 是一个获取 store 保存的状态,然后将这个状态转化为 UI组件 的数据的函数,它必须要返回一个对象,而这个对象用来进行状态转化的。
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
render () {
const { store } = this.context;
<!-- 从store获取state并且转化之后全部传入 props -->
let stateProps = mapStateToProps(store.getState());
return <WrappedComponent {...stateProps} />
}
}
return Connect;
}
export { connect };
复制代码
上面最关键的一步就是调用 mapStateToProps 时,从 store 获取到 state 之后然后传入到 mapStateToProps 函数中,然后这函数会返回一个转化后的 state ,然后把这些转化的状态全部传入 props 里面。
可以看出 connect 把 Dumb组件(纯组件) 和 context 连起来了,下面只需要调用 connect 然后传入一个 mapStateToProps 和 UI组件 就可以使用了。
<!-- Header -->
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from './React-redux';
class Header extends Component {
static propTypes = {
themeColor: PropTypes.string
}
render() {
return (
<div className="header">
<h1 style={{color: this.props.themeColor}}>is header</h1>
</div>
)
}
}
const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}
Header = connect(mapStateToProps)(Header)
export { Header };
<!-- Content -->
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from './React-redux';
import { ThemeSwitch } from './ThemeSwitch';
class Content extends Component {
static propTypes = {
themeColor: PropTypes.string
}
render() {
return (
<div className="header">
<h2 style={{ color: this.props.themeColor }}>is Content</h2>
<ThemeSwitch />
</div>
)
}
}
const mapStateToProps = (state) => {
return {
themeColor: state.themeColor
}
}
Content = connect(mapStateToProps)(Content)
export { Content };
复制代码
由于 mapStateToProps 返回的对象经过 connect 传入组件的 props 中,我们直接可以用 this.props 直接获取到。
接着把 connect 的代码复制到一个叫 React-redux 的文件,然后可以删掉之前那些引入 store 的代码了。
现在点击按钮只有按钮会变颜色,接下来我们修改一下 connect :
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const connect = (mapStateToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor (props) {
super(props);
this.state = {
allProps: {}
}
}
componentWillMount () {
const { store } = this.context;
this._updateProps();
store.subscribe(() => this._updateProps())
}
_updateProps () {
const { store } = this.context
<!-- 现在 mapStateToProps 可以接受两个参数 -->
let stateProps = mapStateToProps(store.getState(), this.props)
<!-- 整合普通的 props 和从 state 生成的 props -->
this.setState({
allProps: {
...stateProps,
...this.props
}
})
}
render () {
return <WrappedComponent { ...this.state.allProps } />
}
}
return Connect;
}
export { connect };
复制代码
每次点击按钮调用 dispatch 都会把心的 state 设置到自己的 state 之后,然后返回给组件,这样组件之前的 props 也会保留,同时 mapStateToProps 可以接受第二个参数,这个参数为当前 UI组件 的 props 。
<!-- 第一个为 store 获取到的 state , 第二个为当前 ui 组件的 props (不是最新的) -->
const mapStateToProps = (state, props) => {
console.log(state, props)
return {
themeColor: state.themeColor
}
}
复制代码
使用 props 作为参数后,如果容器组件的参数发生变化,也会引发 UI组件 重新渲染,connect 方法可以省略 mapStateToProps 参数,这样 store 的更新不会引起组件的更新。
- mapDispatchToProps
const mapDispatchToProps = (dispatch, props) => {
return {
updateThemeColor: () => {
dispatch({
type: 'CHANGE_COLOR',
payload: ''
})
}
}
}
复制代码
mapDispatchToProps 是 connect 的第二个参数,用来建立 UI组件 的参数到 store.dispatch 方法的映射,它作为函数时可以接受两个参数,一个是 dispatch ,一个则是 UI组件 的 props 。
mapDispatchToProps 可以定义 action 然后传给 store 。
import React, { Component } from 'react';
import PropTypes from 'prop-types';
const connect = (mapStateToProps, mapDispatchToProps) => (WrappedComponent) => {
class Connect extends Component {
static contextTypes = {
store: PropTypes.object
}
constructor (props) {
super(props);
this.state = {
allProps: {}
}
}
componentWillMount () {
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;
}
export { connect };
复制代码
接受 mapDispatchToProps 作第二个参数,调用时把 dispatch 和 props 传进去,返回 onClickUpdate 然后直接传入 props 中返回给 UI组件 ,接着我们可以直接调用 this.props.onClickUpdate 然后调用 dispatch 来更新状态。
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from './React-redux';
class ThemeSwitch extends Component {
static contextTypes = {
onClickUpdate: PropTypes.func
}
<!-- 点击调用 onClickUpdate -->
updateThemeColor(color) {
if(this.props.onClickUpdate) {
this.props.onClickUpdate(color)
}
}
render() {
return (
<div className="header">
<button style={{ color: this.props.themeColor }} onClick={this.updateThemeColor.bind(this, 'red')}>red</button>
<button style={{ color: this.props.themeColor }} onClick={this.updateThemeColor.bind(this, 'blue')}>blue</button>
</div>
)
}
}
<!-- 在真正的 react-redux 不一定是函数 -->
const mapStateToProps = (state, props) => {
return {
themeColor: state.themeColor
}
}
<!-- 在真正的 react-redux 可以是一个对象 -->
const mapDispatchToProps = (dispatch, props) => {
return {
onClickUpdate: (color) => {
dispatch({
type: 'CHANGE_COLOR',
themeColor: color
})
}
}
}
ThemeSwitch = connect(mapStateToProps, mapDispatchToProps)(ThemeSwitch);
export { ThemeSwitch };
复制代码
这样点击按钮之后又可以改变颜色了。
Provider
connect 方法生成容器后需要拿到 state 对象,目前咱们能拿到 store 是因为在
index.js
中设置了 context ,这样会直接污染index.js
, React-redux 提供了 Provider 来充当最外层容器,这样就不需要在index
设置 context 了。
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>
)
}
}
export { Provider };
复制代码
在 React-redux 的文件增加上面的代码,其实也就是另外设置一个容器来替代之前
index.js
干的活,这里返回了 this.props.children ,也说明要用这个组件把其他的组件包起来。
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
import { Provider } from './component/React-redux';
// 引入组件
import { Header } from './component/Header';
import { Content } from './component/Content';
import * as serviceWorker from './serviceWorker';
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);
class Index extends Component {
render() {
return (
<div className="index">
<Header />
<Content />
</div>
)
}
}
ReactDOM.render(
<!-- 把 store 和 外层组件包起来 -->
<Provider store= { store }>
<Index />
</Provider>,
document.getElementById('root')
);
复制代码
把 Store 传入给 Provider ,然后它把 store 设置成 context ,这样其他子组件都能拿到 store ,并且把最外层容器包起来,然后使用 this.props.children 全部罗列出来。
import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from './React-redux';
class Header extends Component {
<!-- 别忘了声明这玩意,不然拿不到 -->
static contextTypes = {
store: PropTypes.object
}
static propTypes = {
themeColor: PropTypes.string
}
componentWillMount() {
console.log(this.context)
}
render() {
return (
<div className="header">
<h1 style={{color: this.props.themeColor}}>is header</h1>
</div>
)
}
}
const mapStateToProps = (state, props) => {
return {
themeColor: state.themeColor
}
}
Header = connect(mapStateToProps)(Header)
export { Header };
复制代码
拿到之后接下来就可以浪了,可以在当前组件调用里面的方法,非常灵活。
总结
- 首先在
index.js
引入创建好的store
,然后引入Provider
把index
包起来,并且给它传递store
。 - 如果页面需要拿到状态直接调用
store.getState
,如果想监听函数调用store.subscribe
传入函数。 - 如果想订阅
store
或者修改state
,在当前组件引入connect
接着传入mapStateToProps
和mapDispatchToProps
来呈现新的UI组件
。 dispatch
还可以拓展,真正的react-redux
还可以使用中间件实现异步action
,如需要从后台返回的状态来改变当前的state
类似这种操作。- 可以使用
combineReducers
管理多个reducer
,一个store
管理N种状态。