随着JavaScript单页应用开发日趋复杂,管理不断变化的state很是困难,Redux的出现就是为了解决state里的数据问题。在React中,数据在组件中是单向流动的,数据从一个方向父组件流向子组件(经过props),因为这个特征,两个非父子关系的组件(或者称做兄弟组件)之间的通讯就比较麻烦javascript
store是一个数据仓库,一个应用中store是惟一的,它里面封装了state状态,当用户想访问state的时候,只能经过store.getState来取得state对象,而取得的对象是一个store的快照,这样就把store对象保护起来。html
action描述了一个更新state的动做,它是一个对象,其中type属性是必须有的,它指定了某动做和要修改的值:java
{type: UPDATE_TITLE_COLOR, payload: 'green'}
复制代码
若是每次派发动做时都写上长长的action对象不是很方便,而actionCreator就是建立action对象的一个方法,调用这个方法就能返回一个action对象,用于简化代码。node
dispatch是一个方法,它用于派发一个动做action,这是惟一的一个可以修改state的方法,它内部会调用reducer来调用不一样的逻辑基于旧的state来更新出一个新的state。react
reducer是更新state的核心,它里面封装了更新state的逻辑,reducer由外界提供(封装业务逻辑,在createStore时传入),并传入旧state对象和action,将新值更新到旧的state对象上返回。redux
const INCREAMENT='INCREAMENT';
复制代码
let initState={...};
function reducer(state=initState,action){
//...
}
复制代码
function reducer(state=initState,action){
let newState;
switch(action.type){
//...
}
return newState;
}
复制代码
let store=createStore(reducer);
复制代码
store.subcribe(function(){});
复制代码
store.dispatch(/*某个action*/);
复制代码
能够看到经过以上几个步骤,就可使用redux,且不局限于某种“框架”中,redux是一个设计思想,只要符合你的需求就可使用redux。数组
如下编写一个待办事项的小功能,描述以下:bash
小项目的目录结构:架构
项目根结点
┗━ components 存放组件
┗━ todo-header.js
┗━ todo-list.js
┗━ todo-footer.js
┗━ index.js
┗━ store 保存redux的相关文件
┗━ actions 定义action
┗━ action-type 定义动做类型
┗━ action-types.js
┗━ index.js
┗━ reducers 定义reducer
┗━ index.js
┗━ index.js 默认文件用于导出store
┗━ index.html 模版页面
复制代码
以上4个功能咱们使用redux结合react来实现。框架
组件拆分为3个:
此功能的核心就是把全部“未完成”的数量统计出来,在编写redux程序时,首先定义好默认state,默认state是写在reducer中的:
//定义默认状态
let initState = {
todos: [
{
id: parseInt(Math.random() * 10000000),
isComplete: false,
title: '学习redux'
}, {
id: parseInt(Math.random() * 10000000),
isComplete: true,
title: '学习react'
}, {
id: parseInt(Math.random() * 10000000),
isComplete: false,
title: '学习node'
}
]
};
复制代码
在reducer目录下建立一个index.js,因为这4个功能点过于简单,没必要拆分为多个reducer,所以全部的功能都在这一个index.js文件中完成。这样还能够减小combineReducer这个步骤。
以上定义了一个默认state对象,它里面有3条数据,描述了待办事项的内容。
因为目前没有具体的功能逻辑,咱们建立一个空的reducer:
function reducer(state=initState,action){
return state;
}
export default reducer;
复制代码
能够看到,传入了默认的initState,这样就能够基于旧的state对象来做更新,每次reducer都会根据原state更新出一个新的state返回。
以后就能够建立仓库(store),引用刚刚写好的reducer,并把store返回给顶层组件使用:
import {createStore} from 'redux';
import reducer from './reducers';
let store = createStore(reducer);//传入reducer
export default store;
复制代码
在store目录下的index.js默认导出store对象,方便组件引入。
在根组件中引入store对象,它是全部组件的容器,所以它要作全部组件的store提供者的角色,因此它的任务要把store提供给全部子组件使用,这就须要react-redux包提供的一个组件:Provider
:
Provider也是一个组件,它只有一个属性:store
,传入建立好的store对象便可:
import {Provider} from 'react-redux';
import store from '../store';
//其它代码略...
ReactDOM.render(<Provider store={store}> <div> <TodoHeader/> <TodoList/> <TodoFooter/> </div> </Provider>, document.querySelector('#root'));
复制代码
这样就意味着Provider
包裹的全部组件均可合法的取到store。
如今数据已经提供,还须要子组件来接收,一样接收store数据react-redux包也为咱们提供了一个方法:connect
。
connect这个方法很是奇妙,它的功能很是强大,它能够把仓库中state数据注入到组件的属性(this.props)中,这样子组件就能够经过属性的方式拿到仓库中的数据。 首先定义一个头组件,用于显示未完成的数量:
import React from 'react';
import ReactDOM from 'react-dom';
class TodoHeader extends React.Component {
//代码略...
}
复制代码
下面使用connect方法将state数据注入到TodoHeader组件中:
import {connect} from 'react-redux';
let ConnectedTodoHeader = connect((state) => ({
...state
}))(TodoHeader);
复制代码
能够看到它的写法很怪,connect是一个高阶函数(函数返回函数),它的最终返回值是一个组件,这个组件(ConnectedTodoHeader)最终“链接”好了顶层组件Provider提供的store数据。
connect的第一个参数是一个函数,它的返回是一个对象,返回的对象会绑定到目标组件的属性上,函数参数state就是store.getState的返回值,使用它就能够取到全部state上的数据,目前state就是todos的3条待办事项
而高阶函数传入的参数就是要注入的组件,这里是TodoHeader,这样在TodoHeader组件中就能够经过this.props.todos
取到待办事项的数据。
这样就能够编写好咱们的第一个统计功能,下面附上代码:
class TodoHeader extends React.Component {
//取得未完成的todo数量
getUnfinishedCount() {
//this.props.todos就是从connect传入的state数据
return this.props.todos.filter((i) => {
return i.isComplete === false;
}).length;
}
render() {
return (<div> <div>您有{this.getUnfinishedCount()}件事未完成</div> </div>);
}
}
//导出注入后的组件
export default connect((state) => ({
...state//此时的state就是todos:[...]数据
}))(TodoHeader);
复制代码
能够看到,经过connect取得state注入到组件属性上,便可编写逻辑完成功能。
接下来完成添加待办项的功能,用户在一个文本框中输入待办项,把数据添加到仓库中,并更新视图。
因为有用户的操做了,咱们须要编写动做(Action),Action须要一个具体的动做类型,咱们在action-types.js中建立须要动做类型便可:
//添加待办事项
export const ADD_TODO = 'ADD_TODO';
复制代码
能够看到它很是简单,就定义了一个动做类型,也就是一个描述Action动做的指令,导出它给reducer来使用。
接下来编写ActionCreator,它是一个函数,只返回用刚刚这个指令生成的Action对象:
import {ADD_TODO} from './action-type/action-types';
let actions = {
addTodo: function(payload) {
return {type: ADD_TODO, payload};
}
};
export default actions;//导出ActionCreators
复制代码
能够看到引入了action-type,addTodo返回了一个形如{type:XXX, payload:XXX}
的一个Action对象。这就是一个标准的Action对象的形式,第二个参数payload就是用户传入的参数。
注意在导出时必定要将ActionCreator函数包到一个对象中返回,这样redux内部会经过bindActionCreators将dispatch的功能封装到每一个函数中,这样在connect链接时极大的方便了用户的操做,稍候会看到。
下面编写reducer,它里面封装了“添加待办项”的逻辑:
import {ADD_TODO} from '../actions/action-type/action-types';
//部分代码略...
function reducer(state = initState, action) {
let newState;
switch (action.type) {
case ADD_TODO:
newState = {
todos: [
...state.todos,
action.payload
]
};
break;
default:
newState = state;
break;
}
return newState;
}
复制代码
以上经过switch语句的一个分支,判断动做类型是否是“添加待办”这个功能(ADD_TODO),这样在原state对象的基础上追加这条数据便可。
注意,每次reducer都返回一个新的对象,不要直接在原state.todos.push这条数据,由于reducer是一个纯函数。
...
是ES6的写法,意为展开运算符,它是将原state.todos的数据展开,并在后面添加一条新数据,至关于合并操做。
好了,到此处理数据的部分已经写好,又到了注入组件的工做了,建立展现待办的组件TodoList:
import React from 'react';
import {connect} from 'react-redux';
class TodoList extends React.Component {
//代码略...
}
export default connect((state) => ({
...state
}))(TodoList);
复制代码
再次经过connect方法将state数据注入到组件 (TodoList)的属性上,让组件内部能够经过this.props取得state数据。
下面编写展现待办项的功能:
class TodoList extends React.Component {
getTodos() {
return this.props.todos.map((todo, index) => {
return (<li key={index}> <input type="checkbox" checked={todo.isComplete}/> { todo.isComplete ? <del>{todo.title}</del> : <span>{todo.title}</span> } <button type="button" data-id={todo.id}>删除</button> </li>); }); } render() { return (<div> <ul> {this.getTodos()} </ul> </div>); } } 复制代码
在组件中定义一个getTodos方法用于循环全部待办项,能够看到经过this.props.todos便可拿到connect传入的数据,并在render中调用getTodos渲染便可。
如今能够初探整个小项目的逻辑,咱们取数据再也不是经过一层一层的组件传递了,而是全部的数据操做都交由redux来解决,组件只负责展现数据。
接下来实现更改一条待办项的状态,当用户给一条待办打勾就记为已完成,不然置为未完成。
仍是同样,新建一个action-type:
//更改待办项的完成状态
export const TOGGLE_COMPLETE = 'TOGGLE_COMPLETE';
复制代码
建立actionCreator,引入这个action-type:
let actions = {
//更改完成状态,此处payload传id
toggleComplete: function(payload) {
return {type: TOGGLE_COMPLETE, payload};
}
//其它略...
};
复制代码
因为用户勾选一条记录,应传入id做为惟一标识,所以这里的payload参数就是待办项的id。
payload并非必定要叫payload能够更改变量名,如todoId
,redux中管个这变量叫载荷,所以这里使用payload。
一样在reducer中再加一个swtich分支,判断TOGGLE_COMPLETE:
function reducer(state = initState, action) {
let newState;
switch (action.type) {
case TOGGLE_COMPLETE:
newState = {
//循环每一条待办,把要修改的记录更新
todos: state.todos.map(item => {
if (item.id == action.payload) {
item.isComplete = !item.isComplete;
}
return item;
})
};
break;
//其它代码略...
default:
newState = state;
break;
}
return newState;
}
复制代码
能够看到此次是修改某一条记录的isComplete属性,所以使用map函数循环,找到id为action.payload的那一条,修改isComplete的状态。
仍要注意,不要使用slice函数去修改原state,必定要返回一个基于state更新后的新对象,map函数的执行结果就是返回一个新数组,所以使用map符合这里的需求。
接下来为组件的checkbox元素添加事件,当用户勾选时,调用对应的Action toggleComplete动做便可完成逻辑:
//引入actionCreators
import actions from '../store/actions';
//其它 代码略...
class TodoList extends React.Component {
todoChange = (event) => {
//当onChange事件发生时,调用toggleComplete动做
this.props.toggleComplete(event.target.value);
}
getTodos() {
return this.props.todos.map((todo, index) => {
return (<li key={index}> <input type="checkbox" value={todo.id} checked={todo.isComplete} onChange={this.todoChange}/> { todo.isComplete ? <del>{todo.title}</del> : <span>{todo.title}</span> } <button type="button" data-id={todo.id}>删除</button> </li>); }); } render() { //略... } } export default connect((state) => ({ ...state }), actions)(TodoList); //第二个参数传入actionCreators 复制代码
这里的connect函数传入了第二个参数,它是一个actionCreator对象,同理因为组件中须要调用Action派发动做以实现某个逻辑,好比这里就是组件须要更新待办项的状态,则“功能”也是由redux传给组件的。
这样组件里的this.props就能够拿到actionCreator的方法,以调用逻辑: this.props.toggleComplete()
。
如今能够看到connect函数的强大之处,不论是数据state和功能actionCreators,都是由redux传给须要调用的组件。redux在内部自动处理了更新组件、数据传递的工做,咱们开发者没必要再为组件之间的通讯花费精力了。
咱们的从此的工做就是按照redux的架构定义好动做(Action)和reducer,也就是业务逻辑,而其它繁复的工做都由redux来完成。
删除待办项的功能相似,再也不详述。
筛选查看条件须要预先定义好3个状态,即查看所有(all)只查看未完成(uncompleted)和查看已完成(completed)。
所以,咱们修改初始化的状态,让它默认为“查看所有”:
//定义默认状态
let initState = {
//display用于控制待办项列表的显示
display:'all',
todos: [
//略...
]
};
复制代码
一样的套路,建立action-type:
//更改显示待办项的状态
export const CHANGE_DISPLAY = 'CHANGE_DISPLAY';
复制代码
建立actionCreator:
//部分代码略...
let actions = {
//更改显示待办项的状态,
//payload为如下3个值(all,uncompleted,completed)
changeDisplay: function(payload) {
return {type: CHANGE_DISPLAY, payload};
}
};
复制代码
为reducer增长CHANGE_DISPLAY的逻辑:
//部分代码略...
function reducer(state = initState, action) {
let newState;
switch (action.type) {
case CHANGE_DISPLAY:
newState = {
display: action.payload,
todos: [...state.todos]
};
break;
default:
newState = state;
break;
}
return newState;
}
复制代码
在组件中,根据display条件过滤待办项的数据便可,这里抽出一个方法filterDisplay
来实现:
class TodoList extends React.Component {
//按display条件过滤数据
filterDisplay() {
return this.props.todos.filter(item => {
switch (this.props.display) {
case 'completed':
return item.isComplete;
case 'uncompleted':
return !item.isComplete;
case 'all':
default:
return true;
}
});
}
getTodos() {
return this.filterDisplay().map((todo, index) => {
//略...
});
}
render() {
//略...
}
}
export default connect((state) => ({
...state
}), actions)(TodoList);
复制代码
以上仍是由connect方法注入数据到组件,根据状态的display条件过滤出符合条件的数据便可。
到此,所有的功能已实现。
运行效果:
这个例子虽简单却完整的展现了redux的使用,真正项目开发时只要遵循redux的“套路”便可。
要需了解redux的更深层逻辑原理,就要读redux的源码,其实也并不复杂。