本文是『horseshoe·Redux专题』系列文章之一,后续会有更多专题推出javascript
来个人 GitHub repo 阅读完整的专题文章java
来个人 我的博客 得到无与伦比的阅读体验react
Redux是一套精巧而实用的工具,这也是它在开发者中如此流行的缘由。git
因此对待Redux,最重要的就是熟练使用它的主要API,一旦将它了然于胸,就会对Redux的设计思想有一个全局的认识,也就能清楚的判断本身的应用需不须要劳驾Redux出手。github
须要注意:我们默认将Redux和React搭配使用,不过Redux不是非得和它在一块儿的。编程
要达成某个目的,开发者首先要描述本身的意图。Action就是用来描述开发者意图的。它不是一个函数,而是一个普通的对象,经过声明的类型来触发相应的动做。redux
咱们来看一个例子:dom
{
type: 'ADD_TODO_ITEM',
payload: {
content: '每周看一本书',
done: false,
},
}
复制代码
Redux官方定义了字段的一些规范:一个Action必须包含type
字段,同时一个Action包含的字段不该该超过type
、payload
、error
、meta
这四种。异步
有效载荷
。引伸到程序中就是有效字段的意思,也就是说真正用于构建应用的信息都应该放到payload字段里。元
。在这里表示除了payload以外的信息。由于意图是经过类型来定义的,因此type字段必不可少,称某个对象为一个Action的标志就是它有一个type字段。ide
除此以外,一个动做可能包含更为丰富的信息。开发者能够随意添加字段,毕竟它就是个普通对象。不过遵照必定的规范便于其余开发者阅读你的代码,能够提高协做效率。
前面说了type字段通常用全大写的字符串表示,多个字母用下划线分隔。不只如此,你们还有一个约定俗成:用一个结构相同的变量保存该字符串,由于它会在多处用到。
const ADD_TODO_ITEM = 'ADD_TODO_ITEM';
复制代码
集中保存这些变量的文件就叫Constants.js
。
在此,我提出一点异议。若是你以为不麻烦,那遵循规范再好不过。但开发者向来以为Redux过于繁琐,若是你也这么以为,大可没必要维护所谓的Constants。维护Constants的好处不过是一处更改到处生效,然而字符串和变量是结构相同的,若是字符串做了修改,语意上必然大打折扣,何况type字段一旦定义极少更改,因此视你的协做规模和我的喜爱而定,为Redux的繁琐减负不是么?
咱们知道Action是一个对象,可是若是屡次用到这个对象,咱们能够写一个生成Action的函数。
function addTodoItem(content) {
return {
type: ADD_TODO_ITEM,
payload: { content, done: false },
};
}
复制代码
同理,若是你以为繁琐,这一步是能够免去的。
异步场景下Action Creators会大有用处,后面会讲到。
须要注意的是:所谓的Action更确切的说是一个执行动做的指令,而不是一个动做。或者咱们换一种说法,这里的动做指的是动做描述,而不是动做派发。
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('有一个动做被发射了'));
复制代码
顾名思义,替换Reducer,这主要是方便开发者调试Redux用的。
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只应该处理一个动做,但是咱们的应用不可能只有一个动做,因此一个典型的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的简洁和优美,决定牺牲这一部分性能。
只是一些纯函数而已,莫慌。
当咱们使用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。
链接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')
);
复制代码
老式的context写法,在子组件中定义contextTypes
就能够接收到传入的参数。固然,你确定也想到,Redux把这些细节都封装好了,这就是connect
。
connect接口的意义主要有三点:
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接受四个参数:
<App value={value} />
,那么ownProps就是一个包含value的对象。Object.assign()
合并上述三种props。咱们注意到,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专题一览