本文是『horseshoe·Redux专题』系列文章之一,后续会有更多专题推出

来我的 GitHub repo 阅读完整的专题文章

来我的 个人博客 获得无与伦比的阅读体验

Redux是一套精巧而实用的工具,这也是它在开发者中如此流行的原因。

所以对待Redux,最重要的就是熟练使用它的主要API,一旦将它了然于胸,就会对Redux的设计思想有一个全局的认识,也就能清楚的判断自己的应用需不需要劳驾Redux出手。

需要注意:咱们默认将Redux和React搭配使用,不过Redux不是非得和它在一起的。

Action

要达成某个目的,开发者首先要描述自己的意图。Action就是用来描述开发者意图的。它不是一个函数,而是一个普通的对象,通过声明的类型来触发相应的动作。

我们来看一个例子:

{type: 'ADD_TODO_ITEM',payload: {content: '每周看一本书',done: false,},
}
复制代码

Redux官方定义了字段的一些规范:一个Action必须包含type字段,同时一个Action包含的字段不应该超过typepayloaderrormeta这四种。

  • type声明Action的类型。一般用全大写的字符串表示,多个字母用下划线分隔。
  • payload在英文中的意思是有效载荷。引申到程序中就是有效字段的意思,也就是说真正用于构建应用的信息都应该放到payload字段里。
  • error字段并不承载错误信息,而是一个出错的token。只有当值为true时才表示出错,值为其他或者干脆没有该字段表示程序运行正常。那么错误信息放哪呢?当然是放payload里面,因为错误信息也属于构建应用的有效信息。
  • meta在英文中的意思是。在这里表示除了payload之外的信息。

因为意图是通过类型来定义的,所以type字段必不可少,称某个对象为一个Action的标志就是它有一个type字段。

除此之外,一个动作可能包含更为丰富的信息。开发者可以随意添加字段,毕竟它就是个普通对象。不过遵守一定的规范便于其他开发者阅读你的代码,可以提升协作效率。

Constants

前面说了type字段一般用全大写的字符串表示,多个字母用下划线分隔。不仅如此,大家还有一个约定俗成:用一个结构相同的变量保存该字符串,因为它会在多处用到。

const ADD_TODO_ITEM = 'ADD_TODO_ITEM';
复制代码

集中保存这些变量的文件就叫Constants.js

在此,我提出一点异议。如果你觉得不麻烦,那遵循规范再好不过。但开发者向来觉得Redux过于繁琐,如果你也这么觉得,大可不必维护所谓的Constants。维护Constants的好处不过是一处更改处处生效,然而字符串和变量是结构相同的,如果字符串作了修改,语意上必然大打折扣,况且type字段一旦定义极少更改,所以视你的协作规模和个人喜好而定,为Redux的繁琐减负不是么?

Action Creators

我们知道Action是一个对象,但是如果多次用到这个对象,我们可以写一个生成Action的函数。

function addTodoItem(content) {return {type: ADD_TODO_ITEM,payload: { content, done: false },};
}
复制代码

同理,如果你觉得繁琐,这一步是可以免去的。

异步场景下Action Creators会大有用处,后面会讲到。

需要注意的是:所谓的Action更确切的说是一个执行动作的指令,而不是一个动作。或者我们换一种说法,这里的动作指的是动作描述,而不是动作派发。

Store

Redux的本质不复杂,就是用一个全局的外部的对象来存储状态,然后通过观察者模式来构建一套状态更新触发通知的机制。

这里的Store就是存储状态的容器。

但是呢?它需要开发者动手写一套逻辑来指导它怎么处理状态的更新,这就是后面要讲的Reducer,暂且按下不表。

问题是Store怎么接收这套逻辑呢?

import { createStore } from 'redux';import reducer from './reducer';const store = createStore(reducer);
复制代码

看到没有,Redux专门有一个API用来创建Store,它接受三个参数:reducerpreloadedStateenhancer

reducer就是处理状态更新的逻辑。

preloadedState是初始状态,如果你需要让Store一开始不是空对象,那么可以从这里传进去。

enhancer翻译成中文是增强器,是用来装载第三方插件以增强Redux的功能的。

怎么存

我们已经了解了Action的作用,但是Action只是对动作的描述,怎么说它得有个发射器吧。这个发射器就隐藏在Store里。

执行createStore返回的对象包含了一个函数dispatch,传入Action执行就会发射一个动作。

import React, { Component } from 'react';
import store from './store';
import action from './action';class App extends Component {render() {return (<button onClick={() => store.dispatch(action)}>dispatch</button>);}
}export default App;
复制代码

怎么取

好了我们已经发射了一个动作,假设现在Store中已经有状态了,我们怎么把它取出来呢?

直接store.xxx么?

我们先来打印Store这个对象看看:

{dispatch: ƒ dispatch(action),getState: ƒ getState(),replaceReducer: ƒ replaceReducer(nextReducer),subscribe: ƒ subscribe(listener),Symbol(observable): ƒ observable(),
}
复制代码

打印出来一堆API,这可咋整?

别着急,茫茫人海中看到一个叫getState的东西,它就是我们要找的高人吧。插一句,大家注意区分Store和State的区别,Store是存储State的容器。

Redux隐藏了Store的内部细节,所以开发者只能用getState来获取状态。

订阅

Redux是基于观察者模式的,所以它开放了一个订阅的API给开发者,每次发射一个动作,传入订阅器的回调都会执行。通过它开发者就能监听动作的派发以执行相应的逻辑。

import store from './store';store.subscribe(() => console.log('有一个动作被发射了'));
复制代码

replaceReducer

顾名思义,替换Reducer,这主要是方便开发者调试Redux用的。

Reducer

Reducer是Redux的核心概念,因为Redux的作者Dan Abramov这样解释Redux这个名字的由来:Reducer+Flux。

其实Reducer是一个计算机术语,包括JavaScript中也有用于迭代的reduce函数。所以我们先来聊聊应该怎样理解Reducer这个概念。

reduce翻译成中文是减少,Reducer在计算机中的含义是归并,也是化多为少的意思。

我们来看JavaScript中reduce的写法:

const array = [1, 2, 3, 4, 5];
const sum = array.reduce((total, num) => total + num);
复制代码

再来看Redux中Reducer的写法:

function todoReducer(state = [], action) {switch (action.type) {case 'ADD_TODO_ITEM':const { content, done } = action.payload;return [...state, { content, done }];case 'REMOVE_TODO_ITEM':const todos = state.filter(todo => todo.content !== action.content);return todos;default:return state;}
}
复制代码

state参数是一个旧数据集合,action中包含的payload是一个新的数据项,Reducer要做的就是将新的数据项和旧数据集合归并到一起,返回给Store。这样看起来Reducer这个名字起的也没那么晦涩了是不是?

一个Reducer接受两个参数,第一个参数是旧的state,我们返回的数据就是用来替换它的,然后风水轮流转,这次返回的数据下次就变成旧的state了,如此往复;第二个参数是我们派发的action。

因为Reducer的结构类似,都是根据Action的类型返回相应的数据,所以一般采用switch case语句,如果没有变动则返回旧的state,总之它必须有返回值。

纯函数

Reducer的作用是归并,也只能是归并,所以Redux规定它必须是一个纯函数。相同的输入必须返回相同的输出,而且不能对外产生副作用。

所以开发者在返回数据的时候不能直接修改原有的state,而是应该在拷贝的副本之上再做修改。

多个Reducer

一个Reducer只应该处理一个动作,可是我们的应用不可能只有一个动作,所以一个典型的Redux应用会有很多Reducer函数。那么怎么管理这些Reducer呢?

首先来看只有一个Reducer的情况:

import { createStore } from 'redux';import reducer from './reducer';const store = createStore(reducer);export default store;
复制代码

如果只有一个Reducer,那我们只需要将它传入createStore这个函数中,就这么简单。这时候Reducer返回的状态就是Store中的全部状态。

而如果有多个Reducer,我们就要动用Redux的另一个API了:combineReducers

const reducers = combineReducers({userStore: userReducer,todoStore: todoReducer,
});
复制代码

当我们有多个Reducer,就意味着有多个状态需要交给Store管理,我们就需要子容器来存储它们,其实就是对象嵌套对象的意思。combineReducers就是用来干这个的,它把每一个Reducer分门别类的与不同的子容器对应起来,某个Reducer只处理对应的状态。

{userStore: {},todoStore: {},
};
复制代码

当我们用getState获取整个Store的状态,返回的对象就是上面这样的。

你猜对了,传入combineReducers的对象的key就是子容器的名字。

默认值

当开发者调用createStore创建Store时,传入的所有Reducer都会执行一遍。注意,这时开发者还没有发射任何动作呢,那为什么会执行一遍?

const randomString = () => Math.random().toString(36).substring(7).split('').join('.');const ActionTypes = {INIT: `@@redux/INIT${randomString()}`,REPLACE: `@@redux/REPLACE${randomString()}`,PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
};dispatch({ type: ActionTypes.INIT });
复制代码

因为Redux源码中,在createStore函数里面放了这样一段逻辑,这初始化时的dispatch是Redux自己发射的。

为什么?

还记得Reducer接受两个参数吗?第一个是state,而我们可以给state设置默认值。

聪明的你一定想到了,初始化Store时Redux自己发射一个动作的目的是为了收集这些默认值。Reducer会将这些默认值返回给Store,这样默认值就保存到Store中了。

聪明的你大概还想到一个问题:createStore也有默认值,Reducer也有默认值,不会打架么?

Redux的规矩:createStore的默认值优先级更高,所以不会打架。

执行

在一个有若干Reducer的应用中,一个动作是怎么找到对应的Reducer的?

这是一个好问题,答案是挨个找。

假如应用有1000个Reducer,与某个动作对应的Reducer又恰好在最后一个,那要把1000个Reducer都执行一遍,Redux不会这么傻吧?

Redux还真就这么傻。

因为当一个动作被派发时,Redux并不知道应该由哪个Reducer来处理,所以只能让每个Reducer都处理一遍,看看到底是谁的菜。可不可以在设计上将动作与Reducer对应起来呢?当然是可以的,但是Redux为了保证API的简洁和优美,决定牺牲这一部分性能。

只是一些纯函数而已,莫慌。

react-redux

当我们使用Redux时,我们希望每发射一个动作,应用的状态自动发生改变,从而触发页面的重新渲染。

import React, { Component } from 'react';
import store from './store';class App extends Component {state = { name: 'Redux' };render() {const { name } = this.state;return (<div>{name}</div>);}componentDidMount() {this.unsubscribe = store.subscribe(() => {const { name } = store.getState();this.setState({ name });});}componentWillUnmount() {this.unsubscribe();}
}
复制代码

怎么办呢?开发者得手动维护一个订阅器,才能监听到状态变化,从而触发页面重新渲染。

但是React最佳实践告诉我们,一个负责渲染UI的组件不应该有太多的逻辑,那么有没有更好的办法使得开发者可以少写一点逻辑,同时让组件更加优雅呢?

别担心,Redux早就帮开发者做好了,不过它是一个独立的模块:react-redux。顾名思义,这个模块的作用是连接React和Redux。

Provider

连接React和Redux的第一步是什么呢?当然是将Store集成到React组件中,这样我们就不用每次在组件代码中import store了。多亏了React context的存在,Redux只需要将Store传入根组件,所有子组件就能通过某种方式获取传入的Store。

import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';import store from './store';import App from './App';ReactDOM.render(<Provider store={store}><App /></Provider>,document.getElementById('root')
);
复制代码

connect

老式的context写法,在子组件中定义contextTypes就可以接收到传入的参数。当然,你肯定也想到,Redux把这些细节都封装好了,这就是connect

connect接口的意义主要有三点:

  • 封装用context从根组件获取数据的细节。
  • 封装Redux订阅器的细节。
  • 作为一个容器组件真正连接React和Redux。
import React from 'react';
import Todo from './Todo';const App = ({ todos, addTodoItem }) => {return (<div><button onClick={() => addTodoItem()}>add</button>{todos.map(todo => <Todo key={todo.id} {...todo} />)}</div>);
}const mapStateToProps = (state, ownProps) => {return {todos: state.todoStore,};
};const mapDispatchToProps = (dispatch, ownProps) => {return {addTodoItem: (todoItem) => dispatch({ type: 'ADD_TODO_ITEM', payload: todoItem }),};
};export default connect(mapStateToProps, mapDispatchToProps)(App);
复制代码

我们看上面例子,connect接受的两个参数:mapStateToPropsmapDispatchToProps,所谓的map就是映射,意思就是将所有state和dispatch依次映射到props上。如此真正的组件需要的数据和功能都在props上,它就可以安安心心的做一个傻瓜组件。

connect接受四个参数:

  • mapStateToProps。也可以写成mapState,这个参数是用来接收订阅得到的数据更新的,也就是说如果这个参数传null或者undefined,则被connect包裹的组件无法收到更新的数据。mapStateToProps必须是一个函数,而且必须返回一个纯对象。它接收两个参数,第一个参数是存储在Store中完整的state,第二个参数是被connect包裹的组件自身的属性。假如App组件挂载时写成这样:<App value={value} />,那么ownProps就是一个包含value的对象。
  • mapDispatchToProps。也可以写成mapDispatch,这个参数是用来封装所有发射器的。mapDispatchToProps必须是一个函数,而且必须返回一个纯对象。它接收两个参数,第一个参数是dispatch发射器函数,第二个参数和mapStateToProps的第二个参数相同。
  • mergeProps。顾名思义,合并props。现在被connect包裹的组件拥有三种props:由state转化而来的props,由dispatch转化而来的props,自身的props。它返回的纯对象就是最终组件能接收到的props。默认返回的对象是用Object.assign()合并上述三种props。
  • options。用来自定义connect的选项。

我们注意到,connect要先执行一次,返回的结果再次执行才传入开发者定义的组件。它返回一个新的组件,这个新的组件不会修改原组件(除非你操纵了ownProps的返回),而是为组件增加一些新的props。

我们也可以用装饰器写法来重写connect:

import React from 'react';
import Todo from './Todo';const mapStateToProps = (state, ownProps) => {return {todos: state.todoStore,};
};const mapDispatchToProps = (dispatch, ownProps) => {return {addTodoItem: (todoItem) => dispatch({ type: 'ADD_TODO_ITEM', payload: todoItem }),};
};@connect(mapStateToProps, mapDispatchToProps)
const App = ({ todos, addTodoItem }) => {return (<div><button onClick={() => addTodoItem()}>add</button>{todos.map(todo => <Todo key={todo.id} {...todo} />)}</div>);
}export default App;
复制代码

总结

Redux通过调用createStore返回Store,它是一个独立于应用的全局对象,通过观察者模式能让应用监听到Store中状态的变化。最佳实践是一个应用只有一个Store。

Redux必须通过一个明确的动作来修改Store中的状态,描述动作的是一个纯对象,必须有type字段,传递动作的是Store的属性方法dispatch。

Store本身并没有任何处理状态更新的逻辑,所有逻辑都要通过Reducer传递进来,Reducer必须是一个纯函数,没有任何副作用。如果有多个Reducer,则需要利用combineReducers定义相应的子状态容器。

基于容器组件和展示组件分离的设计原则,也为了提高开发者的编程效率,Redux通过一个额外的模块将React和Redux连接起来,使得所有的状态管理接口都映射到组件的props上。其中,Provider将Store注入应用的根组件,解决的是连接的充分条件;connect将需要用到的state和dispatch都映射到组件的props上,解决的是连接的必要条件。只有被Provider包裹的组件,才能使用connect包裹。

Redux专题一览

考古

实用

中间件

时间旅行

Redux专题:实用相关推荐

  1. React Native debug debugger

    React Native & debug & debugger http://localhost:8081/debugger-ui/ react-devtools # yarn:$ y ...

  2. 彻底征服 React.js + Flux + Redux【讲师辅导】-曾亮-专题视频课程

    彻底征服 React.js + Flux + Redux[讲师辅导]-74574人已学习 课程介绍         [会员免费]链接 http://edu.csdn.net/lecturer/585 ...

  3. Python:第三篇【Python】实用库与框架-关东升-专题视频课程

    Python:第三篇[Python]实用库与框架-612人已学习 课程介绍         本课程包括6章.内容包括Python数据交换格式,Python数据库编程,Python网络编程,wxPyth ...

  4. 图形图像-Adobe PhotoshopCS6超速入门,一节课学会PS [实用技能]-韦语洋(Lccee)-专题视频课程...

    图形图像-Adobe PhotoshopCS6超速入门,一节课学会PS [实用技能]-44130人已学习 课程介绍         该视频为"Photoshop CS6视频教程"C ...

  5. 【吴刚】PS软件基础实用技巧标准视频教程-吴刚-专题视频课程

    [吴刚]PS软件基础实用技巧标准视频教程-1465人已学习 课程介绍         <PS软件基础实用技巧标准教程>为吴老师亲自讲解的PS软件基础篇的技术操作内容,通篇干货,使学员上手出 ...

  6. 【孙伟】基础实用Photoshop设计软件标准视频教程-孙伟-专题视频课程

    [孙伟]基础&实用Photoshop设计软件标准视频教程-159人已学习 课程介绍         photoshop软件是设计工作中掌握软件之一,是非常重要的软件: 本课程详细的讲解的了软件 ...

  7. Cordova+React+OnsenUI+Redux新闻App开发实战教程-姜博-专题视频课程

    Cordova+React+OnsenUI+Redux新闻App开发实战教程-779人已学习 课程介绍         Cordova+React+OnsenUI+Redux新闻App开发实战视频培训 ...

  8. MYSQL专题-绝对实用的MYSQL优化总结

    相信大家不管是在面试中,或者是在实际的开发过程中,都有接触过SQL优化相关的事情.之前有看到过类似知识的小伙伴可能能说出点东西来,对于没有涉及过相关知识的伙伴也不用着急,看了这篇,相信也足够你去应对面 ...

  9. 数据加密与安全专题《mbedtls工具篇,实用教程1@mbedtls简介和安装教程》

     引言 物联网的关键在与可通过网络进行远程数据传输与控制,例如手机APP控制家里的摄像头.微波炉等,安全问题尤为重要,对传输数据进行加密传输成为保障网络安全的必然手段之一,在物联网领域,比较成熟且应用 ...

最新文章

  1. 客户关系管理:客户关系选型
  2. 阿里云地图添加点线面
  3. PAT 乙级 1041 考试座位号
  4. ModuleNotFoundError: No module named 'win32api'
  5. Python之Requests
  6. 通过rpm包安装、配置及卸载mysql的详细过程.
  7. 线性表的定义与操作-顺序表,链式表(C语言)
  8. jzoj2700-数字【数论,LCM】
  9. android 玩pc游戏,Shield掌机试玩: Android系统 可玩PC单机游戏
  10. 与继承相关的一些重构(一)
  11. 第一章节 ASP.NET Web应用程序基础(二)
  12. 阶段3 2.Spring_03.Spring的 IOC 和 DI_12 注入集合数据
  13. 玩转 Android10 源码开发定制(一)源码下载
  14. 软件工程:软件开发生命周期 (SDLC)
  15. 19个重要物理单位背后的人和故事
  16. 你在项目中做过哪些安全防范措施?
  17. openGL中的抗锯齿实现
  18. 无纸化考试系统(CS)
  19. 解决移动Windows Kits后运行出错的问题
  20. Side Window Filtering 论文笔记

热门文章

  1. 失效日期 软件测试案例_软件异常测试经验总结(精)
  2. leetcode算法题--从尾到头打印链表
  3. Win7环境下mysql报错1045:Access denied for user root@localhost错误解决方法
  4. Android异步下载
  5. 在python多进程中使用manager和Barrier
  6. 【BZOJ】1834: [ZJOI2010]network 网络扩容(最大流+费用流)
  7. ACCESS TOKEN
  8. Windows Phone 8初学者开发—第12部分:改进视图模型和示例数据
  9. 关于Nowpaper
  10. 使用动态代理,提高工作效率