目录
1、打印日志需求 (案例使用第七点搭建的learn-redux项目的基础上进行修改)
一、JavaScript纯函数
函数式编程中有一个概念叫纯函数,JavaScript符合函数式编程的范式,所以也有纯函数的概念;
在React中,纯函数的概念非常重要,在接下来我们学习的Redux中也非常重要,所以先了解一下纯函数。
- 纯函数的维基百科定义:
在程序设计中,若一个函数符合一下条件,那么这个函数被称为纯函数:
1)此函数在相同的输入值时,需产生相同的输出。函数的输出和输入值以外的其他隐藏信息或状态无关,也和由I/O设备产生的
外部输出无关。
2)该函数不能有语义上可观察的函数副作用,诸如“触发事件”,使输出设备输出,或更改输出值以外物件的内容等。
3)当然上面的定义会过于的晦涩,所以我简单总结一下:
4) 确定的输入,一定会产生确定的输出;
5) 函数在执行过程中,不能产生副作用; - 纯函数分析
案例1:纯函数
它的输出是依赖我们的输入内容,并且中间没有产生任何副作用;function sum(num1 ,num2){ return num1 + num2; }
案例2:非纯函数
函数依赖一个外部的变量,变量发生改变时,会影响:确定的输入,产生确定的输出;let foo = 5; function addNum(num){ return num + foo; }
能否改进成纯函数呢? const foo = 5; 即可 - React中的纯函数
1)为什么纯函数在函数式编程中非常重要呢?
2)因为你可以安心的写和安心的用;
3)你在写的时候保证了函数的纯度,只是关心实现自己的业务逻辑即可,不需要关心传入的内容或者依赖其他的外部变量;
4)你在用的时候,你确定你的输入内容不会被任意篡改,并且自己确定的输入,一定会有确定的输出;
5)React中就要求我们无论是函数还是class声明一个组件,这个组件都必须像纯函数一样,保护它们的props不被修改:
6)传送门
7)在redux中,reducer被要求是一个纯函数。
二、为什么需要redux(传送门)
- JavaScript开发的应用程序,已经变得越来越复杂了:
JavaScript需要管理的状态越来越多,越来越复杂;
这些状态包括服务器返回的数据、缓存数据、用户操作产生的数据等等,也包括一些UI的状态,比如某些元素是否被选中, 是否显示加载动效,当前分页;
- 管理不断变化的state是非常困难的:
状态之间相互会存在依赖,一个状态的变化会引起另一个状态的变化,View页面也有可能会引起状态的变化;
当应用程序复杂时,state在什么时候,因为什么原因而发生了变化,发生了怎么样的变化,会变得非常难以控制和追踪;
- React是在视图层帮助我们解决了DOM的渲染过程,但是State依然是留给我们自己来管理:
无论是组件定义自己的state,还是组件之间的通信通过props进行传递;也包括通过Context进行数据之间的共享;
React主要负责帮助我们管理视图,state如何维护最终还是我们自己来决定;
UI = render(state)
- Redux就是一个帮助我们管理State的容器:Redux是JavaScript的状态容器,提供了可预测的状态管理;
- Redux除了和React一起使用之外,它也可以和其他界面库一起来使用(比如Vue),并且它非常小(包括依赖在内,只有2kb)
三、Redux的核心理念 - Store
- 比如我们有一个朋友列表需要管理:
如果我们没有定义统一的规范来操作这段数据,那么整个数据的变化就是无法跟踪的;
比如页面的某处通过products.push的方式增加了一条数据;
比如另一个页面通过products[0].age = 25修改了一条数据;
- 整个应用程序错综复杂,当出现bug时,很难跟踪到底哪里发生的变化;
四、Redux的核心理念 - action
- Redux要求我们通过action来更新数据:
所有数据的变化,必须通过派发(dispatch)action来更新;
action是一个普通的JavaScript对象,用来描述这次更新的type和content;
- 比如下面就是几个更新friends的action:
强制使用action的好处是可以清晰的知道数据到底发生了什么样的变化,所有的数据变化都是可跟追、可预测的;
当然,目前我们的action是固定的对象,真实应用中,我们会通过函数来定义,返回一个action;
五、Redux的核心理念 - reducer
- reducer是一个纯函数;
- reducer做的事情就是将传入的state和action结合起来生成一个新的state;
六、Redux的三大原则
- 单一数据源
整个应用程序的state被存储在一颗object tree中,并且这个object tree只存储在一个 store 中:
Redux并没有强制让我们不能创建多个Store,但是那样做并不利于数据的维护;
单一的数据源可以让整个应用程序的state变得方便维护、追踪、修改; - State是只读的
唯一修改State的方法一定是触发action,不要试图在其他地方通过任何的方式来修改State:
这样就确保了View或网络请求都不能直接修改state,它们只能通过action来描述自己想要如何修改state;
这样可以保证所有的修改都被集中化处理,并且按照严格的顺序来执行,所以不需要担心race condition(竟态)的问题; - 使用纯函数来执行修改
通过reducer将 旧state和 actions联系在一起,并且返回一个新的State:
随着应用程序的复杂度增加,我们可以将reducer拆分成多个小的reducers,分别操作不同state tree的一部分;
但是所有的reducer都应该是纯函数,不能产生任何的副作用
七、Redux测试项目搭建
1、安装redux:
npm install redux --save
或
yarn add redux
2、redux项目创建:
- 创建项目文件夹:learn-redux
- 执行初始化操作:
或yarn init
yarn init -y
- 安装redux
yarn add redux
- 创建index.js文件
八、 Redux的使用过程
- 创建一个对象,作为我们要保存的状态:
- 创建Store来存储这个state
创建store时必须创建reducer;
我们可以通过 store.getState 来获取当前的state - 通过action来修改state
通过dispatch来派发action;
通常action中都会有type属性,也可以携带其他的数据; - 修改reducer中的处理代码
这里一定要记住,reducer是一个纯函数,不需要直接修改state;
后面我会讲到直接修改state带来的问题; - 可以在派发action之前,监听store的变化:
// reducer store reducer var redux = require('redux'); const initialState = { counter: 0, }; //reducer function 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.num }; case 'SUB_NUMBER': return { ...state, counter: state.counter - action.num }; default: return state; } } //store const store = redux.createStore(reducer); //actions const action1 = { type: 'INCREMENT' }; const action2 = { type: 'DECREMENT' }; const action3 = { type: 'ADD_NUMBER' ,num:5 }; const action4 = { type: 'SUB_NUMBER' ,num:20}; //派发action store.dispatch(action1); store.dispatch(action2); store.dispatch(action3); store.dispatch(action4);
- 为了能验证派发之后state发生了改变,我可以订阅store的修改
// 订阅store的修改 store.subscribe(() => { console.log('counter:' ,store.getState().counter); });
这里需要注意的是:该订阅的代码必须放在action派发之前,因为在执行node命令时,代码是从上往下一次执行,完整代码:
// reducer store reducer var redux = require('redux'); const initialState = { counter: 0, }; //reducer function 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.num }; case 'SUB_NUMBER': return { ...state, counter: state.counter - action.num }; default: return state; } } //store const store = redux.createStore(reducer); //actions const action1 = { type: 'INCREMENT' }; const action2 = { type: 'DECREMENT' }; const action3 = { type: 'ADD_NUMBER' ,num:5 }; const action4 = { type: 'SUB_NUMBER' ,num:20}; // 订阅store的修改(该订阅的代码必须放在action派发之前,因为在执行node命令时,代码是从上往下一次执行) store.subscribe(() => { console.log('counter:' ,store.getState().counter); }); //派发action store.dispatch(action1); store.dispatch(action2); store.dispatch(action3); store.dispatch(action4);
-
然后终端执行node命令:
node index.js
控制台打印结果:
learn-redux % node index.js counter: 1 counter: 0 counter: 5
九、Redux结构划分
如果我们将所有的逻辑代码写到一起,那么当redux变得复杂时代码就难以维护。
- 接下来,我会对代码进行拆分,将store、reducer、action、constants拆分成一个个文件。
创建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, DECREMENT, INCREMENT } from './constants.js' const defaultState = { counter: 0 } function reducer(state = defaultState ,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.num }; case SUB_NUMBER: return { ...state, counter: state.counter - action.num }; default: return state; } } export default reducer;
创建store/actions.js文件:
import { ADD_NUMBER, SUB_NUMBER, DECREMENT, INCREMENT } from './constants.js' export const addAction = num => ({ type: ADD_NUMBER, num, }); export const subAction = num =>({ type:SUB_NUMBER, num }) export const decrementAction = _ =>({ type:DECREMENT }) export const incrementAction = _ =>({ type:INCREMENT })
创建store/constants.js文件:
export const ADD_NUMBER = 'ADD_NUMBER' export const SUB_NUMBER = 'SUB_NUMBER' export const INCREMENT = 'INCREMENT' export const DECREMENT = 'DECREMENT'
终端运行:
yarn start
输出结果:
learn-redux2 % yarn start yarn run v1.22.4 warning ../../../../package.json: No license field $ node --experimental-modules index.js (node:38435) ExperimentalWarning: The ESM module loader is experimental. { counter: 5 } { counter: -5 } { counter: -6 } { counter: -5 } ✨ Done in 0.23s.
- 注意:node中对ES6模块化的支持
目前我使用的node版本是v13.7.0,从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后缀名;
{ "name": "learn-redux2", "version": "1.0.0", "main": "index.js", "license": "MIT", "type": "module", "scripts": { "start": "node --experimental-modules index.js" }, "dependencies": { "redux": "^4.0.5" } }
十、Redux使用流程
redux在实际开发中的流程:
十一、Redux官方图
十二、redux融入react代码
目前redux在react中使用是最多的,所以我们需要将之前编写的redux代码,融入到react当中去。
- 这里我创建了两个组件:
Home组件:其中会展示当前的counter值,并且有一个+1和+5的按钮;
About组件:其中会展示当前的counter值,并且有一个-1和-5的按钮;
- 核心代码主要是两个:
在 componentDidMount 中定义数据的变化,当数据发生变化时重新设置 counter;
在发生点击事件时,调用store的dispatch来派发对应的action;
- 将第九条中的store文件夹拖入到新创建的react项目中:这里需要注意修改的是redux的引入方式有点变化:
- app.js文件:
import React from 'react'; import About from './pages/about'; import Home from './pages/home'; function App() { return ( <div className="App"> <Home/> <About/> </div> ); } export default App;
- home.js文件:
import React, { PureComponent } from 'react' import store from '../store'; import {incrementAction ,addAction} from '../store/actions'; export default class Home extends PureComponent { constructor(props){ super(props); this.state = { counter: store.getState().counter } } componentDidMount() { this.unsubscribue = store.subscribe(() => { this.setState({ counter: store.getState().counter }) }) } componentWillUnmount() { this.unsubscribue(); } increment = ()=> { store.dispatch(incrementAction()); } addNumber = (num)=>{ store.dispatch(addAction(num)); } render() { return ( <div> <h1>home</h1> <h2>当前计数:{this.state.counter}</h2> <button onClick = {()=>this.increment()}>+1</button> <button onClick = {()=>this.addNumber(5)}>+5</button> </div> ) } }
- about.js文件:
import React, { PureComponent } from 'react'; import store from '../store'; import { decrementAction, subAction } from '../store/actions'; export default class About extends PureComponent { constructor(props) { super(props); this.state = { counter: store.getState().counter, }; } componentDidMount() { this.unsubscribue = store.subscribe(() => { this.setState({ counter: store.getState().counter, }); }); } componentWillUnmount() { this.unsubscribue(); } decrement = () => { store.dispatch(decrementAction()); }; subNumber = (num) => { store.dispatch(subAction(num)); }; render() { return ( <div> <hr /> <h1>about</h1> <h2>当前计数:{this.state.counter}</h2> <button onClick={(e) => this.decrement()}>-1</button> <button onClick={(e) => this.subNumber(5)}>-5</button> </div> ); } }
- 运行效果:
十三、高阶函数
- 高阶函数的维基百科定义:至少满足以下条件之一:
接受一个或多个函数作为输入;
输出一个函数;
- JavaScript中比较常见的filter、map、reduce都是高阶函数。
十四、高阶组件
- 什么是高阶组件呢?
高阶组件的英文是 Higher-Order Components,简称为 HOC;
官方的定义:高阶组件是参数为组件,返回值为新组件的函数; - 我们可以进行如下的解析:
首先, 高阶组件 本身不是一个组件,而是一个函数;
其次,这个函数的参数是一个组件,返回值也是一个组件;
十五、高阶组建的定义
案例:
-
import React, { PureComponent } from 'react'; class App extends PureComponent { render() { return <div>aaa</div>; } } function enhanceComponent(WrappedComponent) { return class NewComponent extends PureComponent { render() { return <WrappedComponent />; } }; } const EnhanceComponent = enhanceComponent(App); export default EnhanceComponent;
- 高阶组件的调用过程类似于这样:
const EnhanceComponent = enhanceComponent(App);
- 高阶组建的编写过程类似于这样:
function enhanceComponent(WrappedComponent) { return class NewComponent extends PureComponent { render() { return <WrappedComponent />; } }; }
- 组件的名称问题:
在ES6中,类表达式中类名是可以省略的
运行结果:省略之后默认显示它所继承的父组建名称function enhanceComponent(WrappedComponent) { return class extends PureComponent { render() { return <WrappedComponent />; } }; }
- 组件的名称都可以通过displayName来修改;
App.displayName = 'AAA';
运行结果:function enhanceComponent(WrappedComponent) { const NewComponent = class extends PureComponent { render() { return <WrappedComponent />; } }; NewComponent.displayName = 'BBB'; return NewComponent; }
- 高阶组件并不是React API的一部分,它是基于React的 组合特性而形成的设计模式;
- 高阶组件在一些React第三方库中非常常见:
比如redux中的connect;
比如react-router中的withRouter;
十六、高阶组建应用---- props的增强
案例:
import React, { PureComponent } from 'react'
class About extends PureComponent {
render() {
return <h2>About:{`名字:${this.props.name} 区域:${this.props.region}`}</h2>
}
}
class Home extends PureComponent {
render() {
return <h2>About:{`名字:${this.props.name} 区域:${this.props.region}`}</h2>
}
}
function enhanceRegionProps(WrappedComponent){
return props => {
return <WrappedComponent {...props} region="非洲"/>
}
}
const EnhanceAbout = enhanceRegionProps(About);
const EnhanceHome = enhanceRegionProps(Home);
export default class App extends PureComponent {
render() {
return (
<div>
<EnhanceAbout name="张三"/>
<EnhanceHome name="里斯"/>
</div>
)
}
}
- 不修改原有代码的情况下,添加新的props
function enhanceRegionProps(WrappedComponent ,otherProps){ return props => <WrappedComponent {...props} {...otherProps}/> }
- 利用高阶组件来共享Context
默认代码:
利用高阶组建修改后代码:import React, { PureComponent, createContext } from 'react' //创建context const UserContext = createContext({ region:"重庆", nickname:'默认昵称' }) class Home extends PureComponent{ render(){ return( <UserContext.Consumer> { user => { return <h2>Home: {`昵称: ${user.nickname} 区域: ${user.region}`}</h2> } } </UserContext.Consumer> ) } } class About extends PureComponent { render() { return ( <UserContext.Consumer> { user => { return <h2>About: {`昵称: ${user.nickname} 区域: ${user.region}`}</h2> } } </UserContext.Consumer> ) } } export default class App extends PureComponent { render() { return ( <div> <UserContext.Provider value={{nickname: "fuyun", region: "陕西"}}> <Home/> <About/> </UserContext.Provider> </div> ) } }
分析:import React, { PureComponent, createContext } from 'react' //定义高阶组建 function withUser(WrappedComponent){ return props => { return ( <UserContext.Consumer> { user => { return <WrappedComponent {...props} {...user}/> } } </UserContext.Consumer> ) } } //创建context const UserContext = createContext({ region:"重庆", nickname:'默认昵称' }) class Home extends PureComponent{ render(){ return <h2>Home: {`昵称: ${this.props.nickname} 区域: ${this.props.region}`}</h2> } } class About extends PureComponent { render() { return <h2>About: {`昵称: ${this.props.nickname} 区域: ${this.props.region}`}</h2> } } const UserHome = withUser(Home); const UserAbout = withUser(About); export default class App extends PureComponent { render() { return ( <div> <UserContext.Provider value={{nickname: "fuyun", region: "陕西"}}> <UserHome/> <UserAbout/> </UserContext.Provider> </div> ) } }
修改之前的类组建
修改之后的类组建:
将公共的部分抽离到高阶组建:
十七、高阶组建应用---- 渲染判断鉴权
- 开发中遇到这样的场景:
某些页面是必须用户登录成功才能进行进入;
如果用户没有登录成功,那么直接跳转到登录页面;
- 这个时候,我们就可以使用高阶组件来完成鉴权操作:
import React, { PureComponent } from 'react'
function withAuth(WrappedComponent){
const NewCpn = props => {
const {isLogin} = props;
if (isLogin){
return <WrappedComponent {...props}/>
}else{
return <LoginPage/>
}
}
NewCpn.displayName = 'AuthCpn';
return NewCpn;
}
class LoginPage extends PureComponent{
render(){
return <h2>LoginPage</h2>
}
}
class CartPage extends PureComponent{
render(){
return <h2>CartPage</h2>
}
}
const AuthCartPage = withAuth(CartPage);
export default class App extends PureComponent {
render() {
return (
<div>
<AuthCartPage isLogin={true}/>
</div>
)
}
}
高阶组建:开发中是否登录的状态一般存储在本地直接获取登录状态,不需要外面传入登录状态
function withAuth(WrappedComponent){
const NewCpn = props => {
//开发中是否登录的状态一般存储在本地直接获取登录状态,不需要外面传入登录状态
const {isLogin} = global;
if (isLogin){
return <WrappedComponent {...props}/>
}else{
return <LoginPage/>
}
}
NewCpn.displayName = 'AuthCpn';
return NewCpn;
}
十八、高阶组建应用---- 生命周期劫持
我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:
默认代码:
import React, { PureComponent } from 'react';
class Home extends PureComponent {
// 即将渲染获取一个时间 beginTime
UNSAFE_componentWillMount() {
this.beginTime = Date.now();
}
// 渲染完成再获取一个时间 endTime
componentDidMount() {
this.endTime = Date.now();
const interval = this.endTime - this.beginTime;
console.log(`Home渲染时间: ${interval}`)
}
render() {
return <h2>Home</h2>
}
}
class About extends PureComponent {
// 即将渲染获取一个时间 beginTime
UNSAFE_componentWillMount() {
this.beginTime = Date.now();
}
// 渲染完成再获取一个时间 endTime
componentDidMount() {
this.endTime = Date.now();
const interval = this.endTime - this.beginTime;
console.log(`About渲染时间: ${interval}`)
}
render() {
return <h2>About</h2>
}
}
export default class App extends PureComponent {
render() {
return (
<div>
<Home />
<About />
</div>
)
}
}
修改之后代码:
import React, { PureComponent } from 'react';
function withRenderTime(WrappedComponent) {
return class extends PureComponent {
// 即将渲染获取一个时间 beginTime
UNSAFE_componentWillMount() {
this.beginTime = Date.now();
}
// 渲染完成再获取一个时间 endTime
componentDidMount() {
this.endTime = Date.now();
const interval = this.endTime - this.beginTime;
console.log(`${WrappedComponent.name}渲染时间: ${interval}`)
}
render() {
return <WrappedComponent {...this.props}/>
}
}
}
class Home extends PureComponent {
render() {
return <h2>Home</h2>
}
}
class About extends PureComponent {
render() {
return <h2>About</h2>
}
}
const TimeHome = withRenderTime(Home);
const TimeAbout = withRenderTime(About);
export default class App extends PureComponent {
render() {
return (
<div>
<TimeHome />
<TimeAbout />
</div>
)
}
}
十九、自定义connect函数
创建connect.js文件:
import React ,{ PureComponent } from 'react';
import store from '../store';
export function connect(mapStateToProps, mapDispatchToProps) {
return function enhanceHOC(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}
{...this.state.storeState}
{...mapDispatchToProps(store.dispatch)}/>;
}
};
};
}
about.js文件:
import React from 'react';
import { decrementAction, subAction } from '../store/actions';
import {connect} from '../utils/connect'
const About = (props)=> {
return (
<div>
<hr />
<h1>about</h1>
<h2>当前计数:{props.counter}</h2>
<button onClick={(e) => props.decrement()}>-1</button>
<button onClick={(e) => props.subNumber(5)}>-5</button>
</div>
);
}
const mapStateToProps = state => {
return {
counter: state.counter
};
};
const mapDispatchToProps = dispatch => {
return {
decrement: function (){
dispatch(decrementAction())
},
subNumber: function (num){
dispatch(subAction(num))
}
};
};
export default connect(mapStateToProps , mapDispatchToProps)(About);
相比之前的about代码:
替换为映射:
connect的功能基本实现了,但是如果有一个致命缺陷,就是在connect.js文件中需要引入store:
import store from '../store';
作为一个工具文件,或者三方库,需要引入别人业务层的sotre,显然是不行的,我们需要使用context,将store由外面传入:
创建context.js文件:
import React from 'react'
const StoreContext = React.createContext();
export {
StoreContext
}
继续改造connect.js文件:将store的地方使用context替换
import React ,{ PureComponent } from 'react';
import {StoreContext} from './context'
export function connect(mapStateToProps, mapDispatchToProps) {
return function enhanceHOC(WrappedComponent) {
class EnhanceComponet extends PureComponent {
constructor(props ,context){
super(props ,context)
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}
{...this.state.storeState}
{...mapDispatchToProps(this.context.dispatch)}/>;
}
};
EnhanceComponet.contextType = StoreContext;
return EnhanceComponet;
};
}
然后在外面使用创建的context传入store:
index.js文件:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import {StoreContext} from './utils/context'
import store from './store'
ReactDOM.render(
<StoreContext.Provider value = {store}>
<App />
</StoreContext.Provider>
,
document.getElementById('root')
);
二十、使用redux的connect:
接着上面的代码,我们在项目中引入react-redux,终端输入:
yarn add react-redux
然后将Provider和connect替换为react-redux的就ok了:
二十一、react-redux源码解读
- Provider源码:
- connect源码:通过导出createConnect()函数调用,做一些参数的初始化操作,然后return一个connect函数
- connect函数作用:
- wrapWithConnect高阶组建的作用:给我们组建添加了一些属性,并创建了一个Connect组建,并对Connect组建和传入的WrappedComponent的属性进行一个合并然后返回:
这里需要注意的是hoistStatics是引入的三方hoist-non-react-statics的函数:
二十二、hoist-non-react-statics使用
当使用高阶组建包装组建时,即原始的组建被容器组建包裹后,会导致新组建会丢失原始组建的所有静态方法,还是上面的例子:
我们给About组建添加一个测试方法:
这个时候我们在调用高阶组建的时候,就会丢失这里的test静态方法,除非进行一一拷贝给新组建:
这样做很显然你是需要知道原始组建的所有静态方法的,作为一个三方的库而已,显然不现实,这里就可以用到hoist-non-react-statics库,帮你自动拷贝所有非React的静态方法:
hoistStatics(EnhanceComponet ,WrappedComponent)
二十三、组件中异步操作
- 在上面的案例中,redux中保存的counter是一个本地定义的数据
我们可以直接通过同步的操作来dispatch action,state就会被立即更新。
但是真实开发中,redux中保存的很多数据可能来自服务器,我们需要进行异步的请求,再将数据保存到redux中。 - 网络请求可以在class组件的componentDidMount中发送,所以我们可以有这样的结构:
- 上面的流程有一个缺陷:
我们必须将网络请求的异步代码放到组件的生命周期中来完成;
事实上,网络请求到的数据也属于我们状态管理的一部分,更好的一种方式应该是将其也交给redux来管理;
二十四、redux-thunk中间件
1、如何在redux中进行异步网络请求呢?
- redux中有引入了中间件(Middleware)的概念:
这个中间件的目的是在dispatch的action和最终达到的reducer之间,扩展一些自己的代码;
比如日志记录、调用异步接口、添加代码调试功能等等; - 我们现在要做的事情就是发送异步的网络请求,所以我们可以添加对应的中间件:
官网推荐的、包括演示的网络请求的中间件是使用 redux-thunk; - redux-thunk是如何做到让我们可以发送异步的请求呢?
我们知道,默认情况下的dispatch(action),action需要是一个JavaScript的对象;
redux-thunk可以让dispatch(action函数),action可以是一个函数; - 该函数会被调用,并且会传给这个函数一个dispatch函数和getState函数;
dispatch函数用于我们之后再次派发action;
getState函数考虑到我们之后的一些操作需要依赖原来的状态,用于让我们可以获取之前的一些状态;
2、如何使用redux-thunk
继续上面的案例
- 安装redux-thunk
yarn add redux-thunk
- 在创建store时传入应用了middleware的enhance函数
通过applyMiddleware来结合多个Middleware, 返回一个enhancer;
将enhancer作为第二个参数传入到createStore中;
在store目录下的index.js文件中:
import { createStore ,applyMiddleware } from 'redux'; import reducer from './reducer.js'; import thunkMiddleware from 'redux-thunk' const storeEnhancer = applyMiddleware(thunkMiddleware); const store = createStore(reducer ,storeEnhancer); export default store;
- 定义返回一个函数的action:
注意:这里不是返回一个对象了,而是一个函数;
该函数在dispatch之后会被执行;
需要网络请求,引入axios,终端输入:
yarn add axios
// redux-thunk异步请求 export const fetchDataAction = (dispatch, getState) => { axios({ url: "http://localhost:3000/data", }).then(res => { const data = res.data; console.log(data); dispatch({ type: FETCH_SERVICE_DATA, data }); }) };
- 为了能制造一个server环境,请求到json,我们搭建一个json-server服务,写一串json 传送门 然后跑起来:
大概步骤:新建文件夹server ----> cd server ---> 终端yarn init ---->新建db.json文件 ---> 配置package.json文件的start启动项
- 有了服务以后跑起来我们的案例就能看到log数据了,为了能8把数据展示到界面上,我们添加li标签
完整代码:
store/index.js文件:
import { createStore ,applyMiddleware } from 'redux'; import reducer from './reducer.js'; import thunkMiddleware from 'redux-thunk' const storeEnhancer = applyMiddleware(thunkMiddleware); const store = createStore(reducer ,storeEnhancer); export default store;
store/actions.js:
import axios from 'axios'; import { ADD_NUMBER, SUB_NUMBER, DECREMENT, INCREMENT, FETCH_SERVICE_DATA } from './constants.js' export const addAction = num => ({ type: ADD_NUMBER, num, }); export const subAction = num =>({ type:SUB_NUMBER, num }) export const decrementAction = () =>({ type:DECREMENT }) export const incrementAction = () =>({ type:INCREMENT }) // redux-thunk异步请求 export const fetchDataAction = (dispatch, getState) => { axios({ url: "http://localhost:3000/data", }).then(res => { const data = res.data; console.log(data); dispatch({ type: FETCH_SERVICE_DATA, data }); }) };
store/constants.js
export const ADD_NUMBER = 'ADD_NUMBER' export const SUB_NUMBER = 'SUB_NUMBER' export const INCREMENT = 'INCREMENT' export const DECREMENT = 'DECREMENT' export const FETCH_SERVICE_DATA = 'FETCH_SERVICE_DATA'
store/reducer.js
import { ADD_NUMBER, SUB_NUMBER, DECREMENT, INCREMENT, FETCH_SERVICE_DATA, } from './constants.js'; const defaultState = { counter: 0, }; function reducer(state = defaultState, 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.num }; case SUB_NUMBER: return { ...state, counter: state.counter - action.num }; case FETCH_SERVICE_DATA: return { ...state, data: action.data }; default: return state; } } export default reducer;
home.js
import React, { PureComponent } from 'react' import {incrementAction ,addAction ,fetchDataAction} from '../store/actions'; import {connect} from 'react-redux'; class Home extends PureComponent { componentDidMount() { this.props.fetchData(); } render() { const {data = []} = this.props; return ( <div> <h1>home</h1> <h2>当前计数:{this.props.counter}</h2> <button onClick = {()=>this.props.increment()}>+1</button> <button onClick = {()=>this.props.addNumber(5)}>+5</button> <br/> <ul> { data.map((item)=>{ return ( <li key={item.id}>{item.title}</li> ) }) } </ul> </div> ) } } const mapStateToProps = state => ({ counter:state.counter, data:state.data }) const mapDispatchToProps = dispatch => ({ increment: ()=> { dispatch(incrementAction()) }, addNumber: (num)=> { dispatch(addAction(num)) }, fetchData: ()=> { dispatch(fetchDataAction) } }) export default connect(mapStateToProps ,mapDispatchToProps)(Home)
这里需要注意的是,由于引入了redux-thunk中间件后,home.js文件中dispatch的一定是个函数,而不是函数的调用:
该dispatch传入一个函数fetchDataAction,redux-thunk内部会对fetchDataAction函数进行调用,因此一定不能写成函数调用运行看结果:
二十五、redux-devtools
- redux官网为我们提供了redux-devtools(传送门)的工具,利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;
- 安装该工具需要两步:
第一步:在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索Redux DevTools即可,其他方法可以参GitHub)(传送门);
第二步:在redux中继承devtools的中间件;
store/index.js文件中
就可以调试了,当然如果需要用到trace功能,还需要添加:trace参数为true:import { createStore, applyMiddleware, compose } from 'redux'; import reducer from './reducer.js'; import thunkMiddleware from 'redux-thunk'; const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; const storeEnhancer = applyMiddleware(thunkMiddleware); const store = createStore(reducer, composeEnhancers(storeEnhancer)); export default store;
运行:const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace:true}) || compose;
二十六、generator
saga中间件使用了ES6的generator语法,简单了解下generator的用法
- 在JavaScript中编写一个普通的函数,进行调用会立即拿到这个函数的返回结果。
创建一个html文件:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> </head> <body> <script> function foo(){ console.log('foo被执行'); } foo(); </script> </body> </html>
控制台:
- 如果我们将这个函数编写成一个生成器函数:
//生成器 function* foo(){ } //iterator 迭代器 const result = foo(); console.log(result);
- 调用iterator的next函数,会销毁一次迭代器,并且返回一个yield的结果。
//2、生成器函数的定义 //生成器函数 function* foo(){ yield "aaaa" yield "bbbb" yield "cccc" } //iterator 迭代器 const result = foo(); console.log(result); //3、使用迭代器 //调用一次next,就会消耗一次迭代器 const res1 = result.next(); console.log(res1);
我们使用用完迭代器后看结果://3、使用迭代器 //调用一次next,就会消耗一次迭代器 const res1 = result.next(); console.log(res1); const res2 = result.next(); console.log(res2); const res3 = result.next(); console.log(res3); const res4 = result.next(); console.log(res4);
- 研究一下foo生成器函数代码的执行顺序:
//执行顺序 //2、生成器函数的定义 //生成器函数 function* foo() { console.log('111') yield "aaaa" console.log('222') yield "bbbb" console.log('333') yield "cccc" console.log('444') } //iterator 迭代器 const result = foo(); console.log(result); //3、使用迭代器 //调用一次next,就会消耗一次迭代器 const res1 = result.next(); console.log(res1); const res2 = result.next(); console.log(res2); const res3 = result.next(); console.log(res3); const res4 = result.next(); console.log(res4);
执行foo()后并不会立即打印111,而是在执行第一个next()后foo函数里的第一个log 打印 111,返回结果aaa的iterator对象
案例:一次生成1-10个数字,需要用的时候再一次去那对应的值
//小练习:一次生成1-10个数字,需要用的时候再一次去那对应的值 function* generatorNumber(){ for (let i = 0; i < 10; i++) { yield i; } } const num = generatorNumber(); console.log(num.next().value);
- generator和promise一起使用
// 6 、generator和Promise结合使用 function* bar() { console.log('1111') const result = yield new Promise((resolve, reject) => { //模拟异步请求 setTimeout(() => { console.log('即使结束,准备返回结果') resolve('3s过去了,generator'); }, 3000); }); console.log('拿到result:', result); } console.log('开始调用bar()') const it = bar(); console.log('bar()调用完成得到迭代器') it.next().value.then(res => { console.log('执行完第一次next,拿到then的res') it.next(res); console.log('执行完第二次next') });
结合上面了解的执行顺序,这里就不难理解了;
这里需要注意的是next(res),给next传入res,会直接将res返回给result,然后执行:console.log('拿到result:', result); 打印结果result才有值
二十七、redux-saga的使用
redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活。redux官网是有提到redux-saga(传送门)
依然是上面的案例
Redux-saga的使用步骤如下
-
安装redux-saga
yarn add redux-saga
- 集成redux-saga中间件
导入创建中间件的函数;
通过创建中间件的函数,创建中间件,并且放到applyMiddleware函数中;
启动中间件的监听过程,并且传入要监听的saga;
import { createStore, applyMiddleware, compose } from 'redux'; import reducer from './reducer.js'; import thunkMiddleware from 'redux-thunk'; import createSagaMiddleware from 'redux-saga' import mySaga from './saga' //创建composeEnhancers函数 const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace:true}) || compose; //应用中间件 //1、引入thunkMiddleware中间件 //2、引入sagaMiddleware中间件 const sagaMiddleware = createSagaMiddleware(); const storeEnhancer = applyMiddleware(thunkMiddleware ,sagaMiddleware); const store = createStore(reducer, composeEnhancers(storeEnhancer)); sagaMiddleware.run(mySaga); export default store;
- saga.js文件的编写
takeEvery:可以传入多个监听的actionType,每一个都可以被执行(对应有一个takeLatest,会取消前面的)
put:在saga中派发action不再是通过dispatch,而是通过put;
all:可以在yield的时候put多个action;
import { takeEvery, put, all, takeLatest } from 'redux-saga/effects'; import axios from 'axios'; import { FETCH_SERVICE_SAGA_DATA } from '../store/constants'; import { changeSagaAction } from './actions'; function* fetchSagaDataAction() { const res = yield axios({ url: 'http://localhost:3000/saga', }); console.log(res); yield put(changeSagaAction(res)); //也可以执行多个put // yield all([ // yield put(changeAction1(data)), // yield put(changeAction2(data)) // ]) } function* mySaga() { // takeEvery会监听拦截action事件的类型 FETCH_SERVICE_SAGA_DATA, //然后执行fetchSagaDataAction生成器函数 yield takeEvery(FETCH_SERVICE_SAGA_DATA, fetchSagaDataAction); // takeLatest takeEvery区别: // takeLatest: 依次只能监听一个对应的action // takeEvery: 每一个都会被执行 //多个执行放在all函数的数组里 // yield all([ // takeLatest(FETCH_SERVICE_SAGA_DATA, fetchSagaDataAction), // // takeLatest(ADD_NUMBER, fetchHomeMultidata), // ]); } export default mySaga;
- 这里需要在actions.js文件中定义changeSagaAction函数:
这里的FETCH_SERVICE_SAGA_TYPE类型在constants.js添加一个就ok了。//saga文件中put的函数,用于派发到reducer export const changeSagaAction = (sagaData)=> ({ type: FETCH_SERVICE_SAGA_TYPE, sagaData })
- 然后派发到reducer后,需要再更新state,所在reducer.js文件添加:
case FETCH_SERVICE_SAGA_TYPE: return { ...state, sagaData: action.data };
- 如何使用呢,还需要在home.js文件中添加对应的mapDispatchToProps映射fetchSagaData,然后在调用就可以触发了:
fetchSagaData: ()=> { dispatch(fetchSagaDataAction) }
- 这里的fetchSagaDataAction需要传入saga拦截所以需要的type类型对象,我们定义在actions.js文件里:
// redux-saga拦截的action export const fetchSagaDataAction = { type: FETCH_SERVICE_SAGA_DATA }
- 完整代码
actions.js完整代码:
home.js完整代码:import axios from 'axios'; import { ADD_NUMBER, SUB_NUMBER, DECREMENT, INCREMENT, FETCH_SERVICE_DATA, FETCH_SERVICE_SAGA_DATA, FETCH_SERVICE_SAGA_TYPE } from './constants.js' export const addAction = num => ({ type: ADD_NUMBER, num, }); export const subAction = num =>({ type:SUB_NUMBER, num }) export const decrementAction = () =>({ type:DECREMENT }) export const incrementAction = () =>({ type:INCREMENT }) // redux-thunk异步请求 export const fetchDataAction = (dispatch, getState) => { axios({ url: "http://localhost:3000/data", }).then(res => { const data = res.data; console.log(data); dispatch({ type: FETCH_SERVICE_DATA, data }); }) }; // redux-saga拦截的action export const fetchSagaDataAction = { type: FETCH_SERVICE_SAGA_DATA } //saga文件中put的函数,用于派发到reducer export const changeSagaAction = (sagaData)=> ({ type: FETCH_SERVICE_SAGA_TYPE, sagaData })
reducer.js完整代码:import React, { PureComponent } from 'react' import {incrementAction ,addAction ,fetchDataAction ,fetchSagaDataAction} from '../store/actions'; import {connect} from 'react-redux'; class Home extends PureComponent { componentDidMount() { this.props.fetchData(); this.props.fetchSagaData(); } render() { const {data = []} = this.props; return ( <div> <h1>home</h1> <h2>当前计数:{this.props.counter}</h2> <button onClick = {()=>this.props.increment()}>+1</button> <button onClick = {()=>this.props.addNumber(5)}>+5</button> <br/> <ul> { data.map((item)=>{ return ( <li key={item.id}>{item.title}</li> ) }) } </ul> </div> ) } } const mapStateToProps = state => ({ counter:state.counter, data:state.data }) const mapDispatchToProps = dispatch => ({ increment: ()=> { dispatch(incrementAction()) }, addNumber: (num)=> { dispatch(addAction(num)) }, fetchData: ()=> { dispatch(fetchDataAction) }, fetchSagaData: ()=> { dispatch(fetchSagaDataAction) } }) export default connect(mapStateToProps ,mapDispatchToProps)(Home)
import { ADD_NUMBER, SUB_NUMBER, DECREMENT, INCREMENT, FETCH_SERVICE_DATA, FETCH_SERVICE_SAGA_TYPE, } from './constants.js'; const defaultState = { counter: 0, }; function reducer(state = defaultState, 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.num }; case SUB_NUMBER: return { ...state, counter: state.counter - action.num }; case FETCH_SERVICE_DATA: return { ...state, data: action.data }; case FETCH_SERVICE_SAGA_TYPE: return { ...state, sagaData: action.data }; default: return state; } } export default reducer;
- 运行结果:
二十八、中间件
1、打印日志需求
注意:案例使用第七点搭建的learn-redux项目的基础上进行修改
-
前面我们已经提过,中间件的目的是在redux中插入一些自己的操作:
比如我们现在有一个需求,在dispatch之前,打印一下本次的action对象,dispatch完成之后可以打印一下最新的store 、state;
也就是我们需要将对应的代码插入到redux的某部分,让之后所有的dispatch都可以包含这样的操作; - 如果没有中间 件,我们是否可以实现类似的代码呢? 可以在派发的前后进行相关的打印。
在index.js文件中:
//注意,node环境路径必须写全,只有在webpack环境下才能省略:/index.js import store from './store/index.js'; import { addAction, subAction } from './store/actions.js'; //1、基本方法---------------------- console.log('dispatch前---dispatch action' ,addAction(5)); store.dispatch(addAction(5)); console.log('dispatch前---dispatch action' ,store.getState()); console.log('dispatch前---dispatch action' ,addAction(10)); store.dispatch(subAction(10)); console.log('dispatch前---dispatch action' ,store.getState());
- 但是这种方式缺陷非常明显:
首先,每一次的dispatch操作,我们都需要在前面加上这样的逻辑代码;
其次,存在大量重复的代码,会非常麻烦和臃肿; - 是否有一种更优雅的方式来处理这样的相同逻辑呢?
我们可以将代码封装到一个独立的函数中
//2、封装一个函数-------------------- function dispatchAndLoggin(action) { console.log('dispatch前---dispatch action', action); store.dispatch(addAction(5)); console.log('dispatch前---dispatch action', store.getState()); } dispatchAndLoggin(addAction(5)) dispatchAndLoggin(addAction(10))
- 但是这样的代码有一个非常大的缺陷:
调用者(使用者)在使用我的dispatch时,必须使用我另外封装的一个函数dispatchAndLog;
显然,对于调用者来说,很难记住这样的API,更加习惯的方式是直接调用dispatch; - 修改dispatch
我们对代码进行如下的修改:利用一个hack一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;
这样就意味着我们已经直接修改了dispatch的调用过程;//3、在函数的基础上进行优化:修改原有的dispatch-------------------- //hack技术: monkeyingpatch 修改原有api的能力 let next = store.dispatch; function dispatchAndLoggin(action) { console.log('dispatch前---dispatch action', action); next(addAction(5)); console.log('dispatch前---dispatch action', store.getState()); } store.dispatch = dispatchAndLoggin; store.dispatch(addAction(5)) store.dispatch(addAction(10))
在调用dispatch的过程中,真正调用的函数其实是dispatchAndLog;
- 当然,我们可以将它封装到一个模块中,只要调用这个模块中的函数,就可以对store进行这样的处理
//4、将上面的代码封装成一个函数-------------------- //当需要使用我们的这块代码的逻辑的时候,只需要调用该函数,这个打印日志功能就会生效 //可以把这部分代码封装到一个独立的文件中并export出这个函数即可 function patchLogging(store) { let next = store.dispatch; function dispatchAndLoggin(action) { console.log('dispatch前---dispatch action', action); next(addAction(5)); console.log('dispatch前---dispatch action', store.getState()); } store.dispatch = dispatchAndLoggin; } patchLogging(store) store.dispatch(addAction(5)) store.dispatch(addAction(10))
-
实现thunk需求:
- redux-thunk的作用:
我们知道redux中利用一个中间件redux-thunk可以让我们的dispatch不再只是处理对象,并且可以处理函数;
那么redux-thunk中的基本实现过程是怎么样的呢?事实上非常的简单。 - 我们来看下面的代码:
我们又对dispatch进行转换,这个dispatch会判断传入的是对象还是函数
//5、将上面的代码封装成一个函数-------------------- function patchThunk(store) { let next = store.dispatch; function dispatchAndThunk(action) { if (typeof action === 'function') { action(store.dispatch, store.getState); } else { next(action); } } store.dispatch = dispatchAndThunk; } patchThunk(store); //传入对象 store.dispatch(addAction(5)); store.dispatch(addAction(10)); //传入函数 function foo(dispatch ,getState){ console.log(dispatch ,getState) dispatch(subAction(10)) } store.dispatch(foo);
- redux-thunk的作用:
2、合并中间件
- 单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:
完整代码:function applyMiddlewares(...middlewares){ middlewares.forEach(middleware => { store.dispatch = middleware(store); }); }
//6、封装 applyMiddleware -------------------- function patchThunk(store) { let next = store.dispatch; function dispatchAndThunk(action) { if (typeof action === 'function') { action(store.dispatch, store.getState); } else { next(action); } } //直接返回这个dispatchAndThunk,统一在middleware中调用 return dispatchAndThunk; } function patchLogging(store) { let next = store.dispatch; function dispatchAndLoggin(action) { console.log('dispatch前---dispatch action', action); next(addAction(5)); console.log('dispatch前---dispatch action', store.getState()); } //直接返回这个dispatchAndLoggin,统一在middleware中调用 return dispatchAndLoggin; } function applyMiddlewares(...middlewares){ middlewares.forEach(middleware => { //为了保证middleware是个纯函数,所有在此处进行dispatch的一个负值操作 store.dispatch = middleware(store); }); //如果要保证applyMiddlewares是一个纯函数,也可以在此处return store } applyMiddlewares(patchThunk ,patchLogging)
- 我们来理解一下上面操作之后,代码的流程:
- 真实的中间件实现起来会更加的灵活,有兴趣可以参考redux合并中间件的源码流程。
二十九、Reducer
1、Reducer代码拆分
- 为什么这个函数叫reducer?官方就就叫reduder(传送门)
因为我们这个纯函数的作用和Array.prototype.reduce(reducer, ?initialValue)的作用非常相似
- 我们来看一下目前我们的reducer: (继续上面的代码)
当前这个reducer既有处理counter的代码,又有处理home页面的数据;
后续counter相关的状态或home相关的状态会进一步变得更加复杂;
我们也会继续添加其他的相关状态;
如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。 - 因此,我们可以对reducer进行拆分:
我们先抽取一个对counter处理的reducer;
再抽取一个对home处理的reducer;
将它们合并起来;
2、Reducer文件拆分
目前我们已经将不同的状态处理拆分到不同的reducer中,但是依然有弊端:
- 虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;
- 另外关于reducer中用到的constant、action等我们也依然是在同一个文件中;
3、combineReducers函数
-
目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象。
-
事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并:
- 那么combineReducers是如何实现的呢?
事实上,它也是讲我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了);
在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state;
新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;
在上面拆分好文件的项目中的reducer.js文件中将:
替换为:function reducer(state = {}, action) { return { aboutInfo: aboutReducer(state.aboutInfo, action), homeInfo: homeReducer(state.homeInfo, action) } }
const reducer = combineReducers({ aboutInfo: aboutReducer, homeInfo: homeReducer });
运行依然ok