本文来自#React系列教程:https://mp.weixin.qq.com/mp/appmsgalbum?__biz=Mzg5MDAzNzkwNA==&action=getalbum&album_id=1566025152667107329)
在React的开发过程中,Redux对于我们是非常重要的。
但是对于很多人来说,初次接触redux会感觉redux的学习坡度非常陡峭,难度比较大。
在接下来,我会通过一个系列的方式循循渐进、层层深入的方式来讲解Redux:从redux的独立使用过程、到redux的结构划分、到redux结合react的过程、到redux中异步操作、到复杂数据的结构处理、到结合immutable的使用。
整个过程,都是在现有知识的基础之上层层递增、一步步掌握redux的使用!
函数式编程中有一个概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
在React中,纯函数的概念非常重要,在接下来我们学习的Redux中也非常重要,所以我们有必须来回顾(如果你之前没有学过,那么你就是学习)一下纯函数。
纯函数的维基百科定义:
在程序设计中,若一个函数符合以下条件,那么这个函数被称为纯函数:
当然上面的定义会过于的晦涩,所以我简单总结一下:
那么我们来看几个函数是否是纯函数:
案例一:
function sum(num1, num2) {
return num1 + num2;
}
案例二:
let foo = 5;
function add(num) {
return foo + num;
}
console.log(add(5));
foo = 10;
console.log(add(5));
add
函数不是一个纯函数;const foo = 5;
即可案例三:
function printInfo(info) {
console.log(info.name, info.age);
info.name = "哈哈哈";
}
printInfo
不是一个纯函数;undefined
,但是它产生了副作用,修改了传入的对象;当然纯函数还有很多的变种,但是我们只需要理解它的核心就可以了。
为什么纯函数在函数式编程中非常重要呢?
React 非常灵活,但它也有一个严格的规则:
class
声明的组件),都必须像纯函数一样,保护它们的props
不被修改。在之后学习redux中,reducer
也被要求是一个纯函数。
JavaScript开发的应用程序,已经变得越来越复杂了:
管理不断变化的state
是非常困难的:
state
在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;React是在视图层帮助我们解决了DOM的渲染过程,但是State
依然是留给我们自己来管理:
state
,还是组件之间的通信通过props
进行传递;也包括通过Context
进行数据之间的共享;state
如何维护最终还是我们自己来决定;Redux就是一个帮助我们管理State
的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;
Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)
Redux的核心理念非常简单。
比如我们有一个朋友列表需要管理:
const initialState = {
friends: [
{ name: "why", age: 18 },
{ name: "kobe", age: 40 },
{ name: "lilei", age: 30 },
]
};
如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;
friends.push
的方式增加了一条数据;friends[0].age = 25
修改了一条数据;Redux要求我们通过action
来更新数据:
action
来更新;action
是一个普通的JavaScript对象,用来描述这次更新的type
和content
;比如下面就是几个更新friends
的action
:
const action1 = { type: "ADD_FRIEND", info: { name: "lucy", age: 20 } }
const action2 = { type: "INC_AGE", index: 0 }
const action3 = { type: "CHANGE_NAME", playload: { index: 0, newName: "coderwhy" } }
action
的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;action
是固定的对象,真实应用中,我们会通过函数来定义,返回一个action
;但是如何将state
和action
联系在一起呢?答案就是reducer
reducer
是一个纯函数;reducer
做的事情就是将传入的state
和action
结合起来生成一个新的state
;function reducer(state = initialState, action) {
switch (action.type) {
case "ADD_FRIEND":
return { ...state, friends: [...state.friends, action.info] }
case "INC_AGE":
return {
...state, friends: state.friends.map((item, index) => {
if (index === action.index) {
return { ...item, age: item.age + 1 }
}
return item;
})
}
case "CHANGE_NAME":
return {
...state, friends: state.friends.map((item, index) => {
if (index === action.index) {
return { ...item, name: action.newName }
}
return item;
})
}
default:
return state;
}
}
这个reducer
有点像 Android 的 Handler
的 handleMessage()
方法中根据不同的消息命令进行分发处理的逻辑。
1. 单一数据源
整个应用程序的state
被存储在一颗object tree
中,并且这个object tree
只存储在一个 store
中:
Store
,但是那样做并不利于数据的维护;2. State是只读的
唯一修改State
的方法一定是触发action
,不要试图在其他地方通过任何的方式来修改State
:
state
,它们只能通过action
来描述自己想要如何修改state
;3. 使用纯函数来执行修改
通过 reducer
将 旧state
和 actions
联系在一起,并且返回一个新的State
:
reducer
拆分成多个小的reducers
,分别操作不同state tree的一部分;reducer
都应该是纯函数,不能产生任何的副作用;安装redux
:
npm install redux --save
# 或
yarn add redux
这里,我通过创建一个简单的js
文件,我们先来简单学习一下redux
:
搭建项目结构
learn-redux
# 执行初始化操作
yarn init
# 安装redux
yarn add redux
创建src
目录,并且创建index.js
文件
修改package.json
可以执行index.js
"scripts": {
"start": "node src/index.js"
}
开始在index.js
中编写代码
const redux = require('redux');
const initialState = {
counter: 0
}
Store
来存储这个state
store
时必须创建reducer
;store.getState
来获取当前的state
// 创建reducer
const reducer = (state = initialState, action) => {
return state;
}
// 根据reducer创建store
const store = redux.createStore(reducer);
console.log(store.getState());
action
来修改state
dispatch
来派发action
;action
中都会有type
属性,也可以携带其他的数据;store.dispatch({
type: "INCREMENT"
})
store.dispath({
type: "DECREMENT"
})
store.dispatch({
type: "ADD_NUMBER",
number: 5
})
reducer
中的处理代码reducer
是一个纯函数,不需要直接修改state
;state
带来的问题;const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT":
return {...state, counter: state.counter + 1};
case "DECREMENT":
return {...state, counter: state.counter - 1};
case "ADD_NUMBER":
return {...state, counter: state.counter + action.number}
default:
return state;
}
}
action
之前,监听store
的变化:store.subscribe(() => {
console.log(store.getState());
})
完成的案例代码如下:
const redux = require('redux');
const initialState = {
counter: 0
}
// 创建reducer
const reducer = (state = initialState, action) => {
switch (action.type) {
case "INCREMENT":
return {...state, counter: state.counter + 1};
case "DECREMENT":
return {...state, counter: state.counter - 1};
case "ADD_NUMBER":
return {...state, counter: state.counter + action.number}
default:
return state;
}
}
// 根据reducer创建store
const store = redux.createStore(reducer);
store.subscribe(() => {
console.log(store.getState());
})
// 修改store中的state
store.dispatch({
type: "INCREMENT"
})
// console.log(store.getState());
store.dispatch({
type: "DECREMENT"
})
// console.log(store.getState());
store.dispatch({
type: "ADD_NUMBER",
number: 5
})
// console.log(store.getState());
如果我们将所有的逻辑代码写到一起,那么当redux
变得复杂时代码就难以维护。
接下来,我会对代码进行拆分,将store、reducer、action、constants
拆分成一个个文件。
注意:node
中对ES6模块化的支持
目前我使用的node版本是v12.16.1
,从node v13.2.0
开始,node
才对ES6模块化提供了支持:
node v13.2.0
之前,需要进行如下操作:
package.json
中添加属性:"type": "module"
;node --experimental-modules src/index.js
;node v13.2.0
之后,只需要进行如下操作:
package.json
中添加属性:"type": "module"
;注意:导入文件时,需要跟上.js
后缀名;
对redux
结构进行划分
创建store/index.js
文件:
import redux from 'redux';
import reducer from './reducer.js';
const store = redux.createStore(reducer);
export default store;
创建store/reducer.js
文件:
import {
ADD_NUMBER,
SUB_NUMBER
} from './constants.js';
const initialState = {
counter: 0
}
function reducer(state = initialState, action) {
switch(action.type) {
case ADD_NUMBER:
return {...state, counter: state.counter + action.num};
case SUB_NUMBER:
return {...state, counter: state.counter - action.num};
default:
return state;
}
}
export default reducer;
创建store/actionCreators.js
文件:
import {
ADD_NUMBER,
SUB_NUMBER
} from './constants.js'
const addAction = (count) => ({
type: ADD_NUMBER,
num: count
});
const subAction = (count) => ({
type: SUB_NUMBER,
num: count
})
export {
addAction,
subAction
}
创建store/constants.js文
件:
const ADD_NUMBER = "ADD_NUMBER";
const SUB_NUMBER = "SUB_NUMER";
export {
ADD_NUMBER,
SUB_NUMBER
}
我们已经知道了redux
的基本使用过程,那么我们就更加清晰来认识一下redux
在实际开发中的流程:
Store
,存储我们的State
;Component
中在某些情况会派发Action
(这些Action
是我们提前定义好的);Reducer
会接收到这些Action
,并且在Reducer
中会返回一个新的State
,作为Store
的State
;State
发生更新之后会触发通知,告知订阅者数据发生了改变;props
中),更新到jsx
中,界面发生改变;目前redux
在react中使用是最大的,所以我们需要将之前编写的redux代码,融入到react当中去。
这里我创建了两个组件:
home.js
代码实现:
import React, { PureComponent } from 'react';
import store from '../store';
import {
addAction
} from '../store/actionCreators';
export default class Home extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
store.subscribe(() => {
this.setState({
counter: store.getState().counter
})
})
}
render() {
return (
<div>
<h1>Home</h1>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.increment()}>+1</button>
<button onClick={e => this.addCounter()}>+5</button>
</div>
)
}
increment() {
store.dispatch(addAction(1));
}
addCounter() {
store.dispatch(addAction(5));
}
}
Profile.js
代码实现:
import React, { PureComponent } from 'react';
import store from '../store';
import {
subAction
} from '../store/actionCreators';
export default class Profile extends PureComponent {
constructor(props) {
super(props);
this.state = {
counter: store.getState().counter
}
}
componentDidMount() {
store.subscribe(() => {
this.setState({
counter: store.getState().counter
})
})
}
render() {
return (
<div>
<hr/>
<h1>Profile</h1>
<div>
<h2>当前计数: {this.state.counter}</h2>
<button onClick={e => this.decrement()}>-1</button>
<button onClick={e => this.subCounter()}>-5</button>
</div>
</div>
)
}
decrement() {
store.dispatch(subAction(1));
}
subCounter() {
store.dispatch(subAction(5));
}
}
上面的代码其实非常简单,核心代码主要是两个:
componentDidMount
中定义数据的变化,当数据发生变化时重新设置 counter
;store
的dispatch
来派发对应的action
;不要忘记取消订阅:
上面的代码是否可以实现react
组件和redux
结合起来呢?
store
数据改变的代码,都需要在 componentDidMount
中完成;store
, 在调用其 dispatch
等;能否将这些公共的内容提取出来呢?
我们来定义一个connect
函数:
这个connect
函数本身接受两个参数:
component
希望使用到的 State
属性;component
希望使用到的 dispatch
动作;这个connect
函数有一个返回值,是一个高阶组件:
constructor
中的state
中保存一下我们需要获取的状态;componentDidMount
中订阅store
中数据的变化,并且执行 setState
操作;componentWillUnmount
中需要取消订阅;render
函数中返回传入的WrappedComponent
,并且将所有的状态映射到其props
中;class
组件;class
组件中,我们进行如下操作:import React, { PureComponent } from "react";
import store from '../store';
export default function connect(mapStateToProps, mapDispatchToProps) {
return function handleMapCpn(WrappedComponent) {
return class extends PureComponent {
constructor(props) {
super(props);
this.state = {
storeState: mapStateToProps(store.getState())
}
}
componentDidMount() {
this.unsubscribe = store.subscribe(() => {
this.setState({
storeState: mapStateToProps(store.getState())
})
})
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return <WrappedComponent {...this.props}
{...mapStateToProps(store.getState())}
{...mapDispatchToProps(store.dispatch)}/>
}
}
}
}
在home
和props
文件中,我们按照自己需要的state、dispatch
来进行映射:
比如home.js
中进行如下修改:
const mapStateToProps = state => {
return {
counter: state.counter
}
}
const mapDispatchToProps = dispatch => {
return {
addNumber: function(number) {
dispatch(addAction(number));
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Home);
mapStateToProps
:用于将state
映射到一个对象中,对象中包含我们需要的属性;mapDispatchToProps
:用于将dispatch
映射到对象中,对象中包含在组件中可能操作的函数;当调用该函数时,本质上其实是调用dispatch(对应的Action)
;在Profile
中也是类似的操作。
有了connect
函数,我们之后只需要关心从state
和dispatch
中映射自己需要的状态和行为即可;
但是上面的connect
函数有一个很大的缺陷:依赖导入的store
如果我们将其封装成一个独立的库,需要依赖用于创建的store
,我们应该如何去获取呢?
难道让用户来修改我们的源码吗?不太现实;
正确的做法是我们提供一个Provider
,Provider
来自于我们创建的Context
,让用户将store
传入到value
中即可;
创建一个context.js
文件:
import { createContext } from 'react';
export const StoreContext = createContext();
修改connect
函数中class
组件部分的代码:
class
组件的名称明确的定义出来,并且给它的contextType
进行了赋值;store
的地方,统一使用this.context
代替(注意:constructor
中直接使用第二个参数即可)import React, { PureComponent } from "react";
import { StoreContext } from './context';
export default function connect(mapStateToProps, mapDispatchToProps) {
return function handleMapCpn(WrappedComponent) {
class ConnectCpn extends PureComponent {
constructor(props, context) {
super(props);
this.state = {
storeState: mapStateToProps(context.getState())
}
}
componentDidMount() {
this.unsubscribe = this.context.subscribe(() => {
this.setState({
storeState: mapStateToProps(this.context.getState())
})
})
}
componentWillUnmount() {
this.unsubscribe();
}
render() {
return <WrappedComponent {...this.props}
{...mapStateToProps(this.context.getState())}
{...mapDispatchToProps(this.context.dispatch)} />
}
}
ConnectCpn.contextType = StoreContext;
return ConnectCpn;
}
}
在入口的index.js
中,使用Provider
并且提供store
即可:
import { StoreContext } from './utils/context';
import store from './store';
ReactDOM.render(
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>,
document.getElementById('root')
);
测试代码,依然可以正常运行。
开始之前需要强调一下,redux和react没有直接的关系,你完全可以在React, Angular, Ember, jQuery, or vanilla JavaScript中使用Redux。
尽管这样说,redux依然是和React或者Deku的库结合的更好,因为他们是通过state
函数来描述界面的状态,Redux可以发射状态的更新,让他们作出相应。
虽然我们之前已经实现了connect
、Provider
这些帮助我们完成连接redux
、react
的辅助工具,但是实际上redux
官方帮助我们提供了 react-redux
的库,可以直接在项目中使用,并且实现的逻辑会更加的严谨和高效。
安装react-redux
:
yarn add react-redux
使用connect
函数:
connect
函数,换成react-redux
的connect
函数;import React, { PureComponent } from 'react';
import { connect } from "react-redux";
// import connect from '../utils/connect2';
export default connect(mapStateToProps, mapDispatchToProps)(Home);
使用Provider
:
Context
的Provider
,换成react-redux
的Provider
组件:store
属性,而不是value
属性(待会儿可以在源码中查看);import { Provider } from 'react-redux';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
这里我简单带着大家看一下react-redux
的源码:
hooks
出现后大量的库转向了hooks
,所以在源码中会出现大量的hooks
代码;hooks
相关的API,所以某些hooks
的作用在这里也不方便解释(可以学习完hooks
之后再详细阅读);首先,我们简单看一下Provider
的源码:
useMemo
来返回一个contextValue
的对象;useMemo
的原因是为了进行性能的优化;store
不改变的情况下,不会进行重新计算,返回一个新的对象;Context
的Provider
中就会将其赋值给value
属性;ReactReduxContext
来自另外一个文件:
connect函数的依赖比较复杂:
createConnect
来返回一个connect
函数:createConnect
函数的调用:
connect
函数最终调用的是connectHOC
:
connectHOC
其实是connectAdvanced
的函数;connectAdvanced
函数最终返回的是wrapWithConnect
函数;wrapWithConnect
函数: