Redux专题:实用
本文是『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包含的字段不应该超过type
、payload
、error
、meta
这四种。
- 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,它接受三个参数:reducer
、preloadedState
和enhancer
。
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接受的两个参数:mapStateToProps
和mapDispatchToProps
,所谓的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专题:实用相关推荐
- React Native debug debugger
React Native & debug & debugger http://localhost:8081/debugger-ui/ react-devtools # yarn:$ y ...
- 彻底征服 React.js + Flux + Redux【讲师辅导】-曾亮-专题视频课程
彻底征服 React.js + Flux + Redux[讲师辅导]-74574人已学习 课程介绍 [会员免费]链接 http://edu.csdn.net/lecturer/585 ...
- Python:第三篇【Python】实用库与框架-关东升-专题视频课程
Python:第三篇[Python]实用库与框架-612人已学习 课程介绍 本课程包括6章.内容包括Python数据交换格式,Python数据库编程,Python网络编程,wxPyth ...
- 图形图像-Adobe PhotoshopCS6超速入门,一节课学会PS [实用技能]-韦语洋(Lccee)-专题视频课程...
图形图像-Adobe PhotoshopCS6超速入门,一节课学会PS [实用技能]-44130人已学习 课程介绍 该视频为"Photoshop CS6视频教程"C ...
- 【吴刚】PS软件基础实用技巧标准视频教程-吴刚-专题视频课程
[吴刚]PS软件基础实用技巧标准视频教程-1465人已学习 课程介绍 <PS软件基础实用技巧标准教程>为吴老师亲自讲解的PS软件基础篇的技术操作内容,通篇干货,使学员上手出 ...
- 【孙伟】基础实用Photoshop设计软件标准视频教程-孙伟-专题视频课程
[孙伟]基础&实用Photoshop设计软件标准视频教程-159人已学习 课程介绍 photoshop软件是设计工作中掌握软件之一,是非常重要的软件: 本课程详细的讲解的了软件 ...
- Cordova+React+OnsenUI+Redux新闻App开发实战教程-姜博-专题视频课程
Cordova+React+OnsenUI+Redux新闻App开发实战教程-779人已学习 课程介绍 Cordova+React+OnsenUI+Redux新闻App开发实战视频培训 ...
- MYSQL专题-绝对实用的MYSQL优化总结
相信大家不管是在面试中,或者是在实际的开发过程中,都有接触过SQL优化相关的事情.之前有看到过类似知识的小伙伴可能能说出点东西来,对于没有涉及过相关知识的伙伴也不用着急,看了这篇,相信也足够你去应对面 ...
- 数据加密与安全专题《mbedtls工具篇,实用教程1@mbedtls简介和安装教程》
引言 物联网的关键在与可通过网络进行远程数据传输与控制,例如手机APP控制家里的摄像头.微波炉等,安全问题尤为重要,对传输数据进行加密传输成为保障网络安全的必然手段之一,在物联网领域,比较成熟且应用 ...
最新文章
- 客户关系管理:客户关系选型
- 阿里云地图添加点线面
- PAT 乙级 1041 考试座位号
- ModuleNotFoundError: No module named 'win32api'
- Python之Requests
- 通过rpm包安装、配置及卸载mysql的详细过程.
- 线性表的定义与操作-顺序表,链式表(C语言)
- jzoj2700-数字【数论,LCM】
- android 玩pc游戏,Shield掌机试玩: Android系统 可玩PC单机游戏
- 与继承相关的一些重构(一)
- 第一章节 ASP.NET Web应用程序基础(二)
- 阶段3 2.Spring_03.Spring的 IOC 和 DI_12 注入集合数据
- 玩转 Android10 源码开发定制(一)源码下载
- 软件工程:软件开发生命周期 (SDLC)
- 19个重要物理单位背后的人和故事
- 你在项目中做过哪些安全防范措施?
- openGL中的抗锯齿实现
- 无纸化考试系统(CS)
- 解决移动Windows Kits后运行出错的问题
- Side Window Filtering 论文笔记
热门文章
- 失效日期 软件测试案例_软件异常测试经验总结(精)
- leetcode算法题--从尾到头打印链表
- Win7环境下mysql报错1045:Access denied for user root@localhost错误解决方法
- Android异步下载
- 在python多进程中使用manager和Barrier
- 【BZOJ】1834: [ZJOI2010]network 网络扩容(最大流+费用流)
- ACCESS TOKEN
- Windows Phone 8初学者开发—第12部分:改进视图模型和示例数据
- 关于Nowpaper
- 使用动态代理,提高工作效率