背景
React是一个单向数据流的view层框架,单向数据流、组件化、生命周期是其特点。在React组件关系中,组件状态由自己管理,父子组件通过props传递;兄弟组件那么就需要一个共同的父组件作中转;如果涉及层级比较深的话一层一层传递会非常麻烦。所以大量状态共享是React单独难以解决的问题。
随着单页面应用的日益复杂,JavaScript需要维护更多的状态,这些状态除了包含服务端返回的数据还有:缓冲数据、未同步到服务端的持久化数据、UI状态等。如果能将这些状态从单个组件剥离出来统一管理,将会更好的维护、拓展Web应用。
Redux就是JavaScript应用这样一个可预测化的状态管理容器。Redux本身和React其实并没有任何关系,只是二者共性的函数式编程配合起来会比较方便,当然实际React项目中还要用到react-redux做桥接。
三大原则
一、单一数据源
应用的state保存在一个JavaScript对象树中,并且这个对象树只能存在于唯一的一个store中。
代码语言:javascript复制import {createStore } from 'redux';
const store = createStore(reducer);
二、state是只读的
唯一改变state的方法就是触发action,action是一个描述state如何改变的普通对象,必须包含type属性。
代码语言:javascript复制store.dispatch({
type: 'COMPLETE_TODO',
index: 1
});
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
});
三、使用纯函数来执行修改
dispatch一个action以后,如何根据这个普通对象来修改state树,那么就需要编写对应的函数,这个函数称之为reducers。reducers必须是纯函数,所谓纯函数可以简单理解为:只要输入相同那么输出就相同,同样的输入只会输出同一个结果。
随着应用规模的增长,所要维护的state树会变的很大,这样就需要把reducers拆分成多个reducer,每个reducer来维护状态树的一部分。
代码语言:javascript复制function visibilityFilter(state = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
text: action.text,
completed: false
}
]
case 'COMPLETE_TODO':
return state.map((todo, index) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
快速上手
0、开始之前
这里以react来演示,当然还是得记住那句话,”redux和react没有任何关系”。
现在设计这么一个状态树:
代码语言:javascript复制{
visibilityFilter:"SHOW_ALL",
todos:[{
text:"consider use redux",
completed:true
},{
text:"keep all state in a single tree",
completed:true
}]
}
这个对象包含2个属性:visibilityFilter、todos。visibilityFilter表示过滤类型,值是一个字符串;todos表示待办事项,值是一个数组。
可以为todos新增或删除项目,也可以改变某个项目的完成情况——completed。
1、安装redux、react、react-dom
代码语言:javascript复制npm install redux react react-dom --save
示例对应版本: – reudx:4.0.1 – react:16.6.3 – react-dom:16.6.3
2、编写一个reducer
创建一个reducers.js,编写以下代码:
代码语言:javascript复制const initState = {
visibilityFilter:"SHOW_ALL",
todos:[]
};
function reducers(state=initState,action){
return state;
}
export {reducers}
reducer接收2个参数:state、action。现在函数内部什么都没有做,仅仅是返回state,后续再增加相关逻辑判断。
3、创建一个store
在redux中应该只有一个store,单一数据源,这一点很重要。redux向外暴露了一个createStore方法用来创建store。
所以,创建一个store.js,编写以下代码:
代码语言:javascript复制import {createStore} from "redux";
import {reducers} from "./reducers";
const store = createStore(reducers);
export default store;
4、创建一个react组件
现在实现这么个功能:一个input框用来输入待办事项,点击提交按钮将数据加到todos中,初始状态completed为false,点击完成将对应的这一条改为true。同时增加一个下拉框select,用来筛选todos。
创建一个app.js,编写以下代码:
代码语言:javascript复制import React from "react";
import ReactDOM from "react-dom";
class App extends React.Component{
constructor(props){
super(props);
this.state = {
filterList:[
{label:"全部",value:"SHOW_ALL"},
{label:"已完成",value:"SHOW_COMPLETED"},
{label:"未完成",value:"SHOW_UNCOMPLETED"},
],
visibilityFilter:"SHOW_ALL",
todoValue:"",
todos:[]
}
}
render(){
const {filterList,visibilityFilter,todoValue,todos} = this.state;
return(
<div>
<div>
<label>过滤类型:</label>
<select value={visibilityFilter} onChange={this.filterChange}>
{
filterList.map(item=>(
<option key={item.value} value={item.value}>{item.label}</option>
))
}
</select>
</div>
<div>
<input placeholder={"请输入待办事项"} value={todoValue} onChange={this.todoChange} />
<button onClick={this.submitTodo} type={"button"}>提交</button>
</div>
<div>
<table border={"border"}>
<thead>
<tr>
<th>事项</th>
<th>是否完成</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{
this.getFilterTodos(todos,visibilityFilter).map((item,index)=>(
<tr key={index}>
<td>{item.text}</td>
<td>{item.completed.toString()}</td>
<td>
{
!item.completed &&
<button onClick={()=>{this.completeTodo(index)}} type={"button"}>完成</button>
}
</td>
</tr>
))
}
</tbody>
</table>
</div>
</div>
)
}
//监听过滤条件
filterChange=(e)=>{
const visibilityFilter= e.target.value;
this.setState({
visibilityFilter
});
};
//监听input
todoChange=(e)=>{
const todoValue = e.target.value;
this.setState({
todoValue
});
};
//提交事项
submitTodo=()=>{
const {todoValue,todos} = this.state;
this.setState({
todos:[].concat(todos).concat({
text:todoValue,
completed:false
}),
todoValue:""
});
};
//完成事项
completeTodo=(i)=>{
const {todos} = this.state;
const newTodos = todos.map((item,index)=>{
if(index !== i){
return item;
}
return Object.assign({},item,{completed:true})
});
this.setState({todos:newTodos});
};
//获取筛选后的todos
getFilterTodos=(todos,visibilityFilter)=>{
switch (visibilityFilter) {
case "SHOW_ALL":
return todos;
case "SHOW_COMPLETED":
return todos.filter(item=>item.completed);
case "SHOW_UNCOMPLETED":
return todos.filter(item=>!item.completed);
default:
return todos;
}
};
}
ReactDOM.render(<App/>,document.querySelector("#root"));
这个组件是纯state维护状态的版本,现在将todos和visibilityFilter拆分到store中:
代码语言:javascript复制import React from "react";
import ReactDOM from "react-dom";
import store from "./store/store";
class App extends React.Component{
constructor(props){
super(props);
this.state = {
filterList:[
{label:"全部",value:"SHOW_ALL"},
{label:"已完成",value:"SHOW_COMPLETED"},
{label:"未完成",value:"SHOW_UNCOMPLETED"},
],
visibilityFilter:store.getState().visibilityFilter,
todoValue:"",
todos:store.getState().todos
}
}
render(){
const {filterList,visibilityFilter,todoValue,todos} = this.state;
return(
<div>
<div>
<label>过滤类型:</label>
<select value={visibilityFilter} onChange={this.filterChange}>
{
filterList.map(item=>(
<option key={item.value} value={item.value}>{item.label}</option>
))
}
</select>
</div>
<div>
<input placeholder={"请输入待办事项"} value={todoValue} onChange={this.todoChange} />
<button onClick={this.submitTodo} type={"button"}>提交</button>
</div>
<div>
<table border={"border"}>
<thead>
<tr>
<th>事项</th>
<th>是否完成</th>
<th>操作</th>
</tr>
</thead>
<tbody>
{
this.getFilterTodos(todos,visibilityFilter).map((item,index)=>(
<tr key={index}>
<td>{item.text}</td>
<td>{item.completed.toString()}</td>
<td>
{
!item.completed &&
<button onClick={()=>{this.completeTodo(index)}} type={"button"}>完成</button>
}
</td>
</tr>
))
}
</tbody>
</table>
</div>
</div>
)
}
componentDidMount(){
store.subscribe(()=>{
this.setState({
visibilityFilter:store.getState().visibilityFilter,
todos:store.getState().todos
});
});
}
//监听过滤条件
filterChange=(e)=>{
store.dispatch({
type:"VISIBILITY_FILTER_SET",
value:e.target.value
});
};
//监听input
todoChange=(e)=>{
const todoValue = e.target.value;
this.setState({
todoValue
});
};
//提交事项
submitTodo=()=>{
const {todoValue} = this.state;
store.dispatch({
type:"TODOS_ADD",
todo:{
text:todoValue,
completed:false
}
});
this.setState({
todoValue:""
});
};
//完成事项
completeTodo=(index)=>{
store.dispatch({
type:"TODOS_COMPLETED",
todo:{
index,
completed:true
}
});
};
//获取筛选后的todos
getFilterTodos=(todos,visibilityFilter)=>{
switch (visibilityFilter) {
case "SHOW_ALL":
return todos;
case "SHOW_COMPLETED":
return todos.filter(item=>item.completed);
case "SHOW_UNCOMPLETED":
return todos.filter(item=>!item.completed);
default:
return todos;
}
};
}
ReactDOM.render(<App/>,document.querySelector("#root"));
store的dispatch()方法用来派发一个action,action是一个普通对象,必须包含type属性,这个属性用来标识执行对应的reducer。
store.subscribe()方法用来监听store里state的变化,所以我们在subscribe的回调里重新获取store的state,以此来更新我们组件的state。
这里共三种action,分别为:VISIBILITY_FILTER_SET(设置过滤类型)、TODOS_ADD(新增事项)、TODOS_COMPLETED(完成事项)。所以我们的reducer需要对这三种情况做判断。
5、修改reducer
代码语言:javascript复制const initState = {
visibilityFilter:"SHOW_ALL",
todos:[]
};
function reducers(state=initState,action){
switch (action.type){
case "VISIBILITY_FILTER_SET":
return Object.assign({},state,{visibilityFilter:action.value});
case "TODOS_ADD":
return Object.assign({},state,{todos:state.todos.concat(action.todo)});
case "TODOS_COMPLETED":
return Object.assign(
{},
state,
{
todos:state.todos.map((item,index)=>{
if(index !== action.todo.index){
return item;
}
return Object.assign({},item,{completed:action.todo.completed})
})
}
);
default:
return state;
}
}
export {reducers}
这里使用switch语句,根据不同的action.type执行不同的操作,返回的都是修改后的state树。
例子中,无论是对象还是数组,并没有直接去修改属性会增加元素,返回的都是一个新的对象或数组,这一点很重要,因为在js中对象是按地址引用的,直接修改属性或push一个元素,引用地址并没有发生变化,这会导致出现一些难以控制的情况。所以,在redux中不应该使用如:push、pop、slice等方法。对于数组可以用concat、拓展运算符、map等;对于对象可以用Object.assign()、拓展运算符等。
总结:
可以看到Redux使用的是派发/监听的设计模式,每次派发action,reducer运算结束后会执行在subscribe注册的回调函数。试想一个问题,如果我的组件之前注册了一个subscribe,然后组件销毁了,当组件又重新渲染的时候便会再次注册subscribe,那么这时派发一个action后,会怎么样?
事实证明,会执行2次,但由于第一次的组件销毁了,所以在一个已经销毁的组件上执行setState()方法必然是不合理的,此时react会抛出一个警告:
Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in the componentWillUnmount method.
意思就是:不能在一个已经卸载的组件上执行更新state的操作,这会导致内存泄漏, 应该在componentWillUnmount生命周期中取消所有订阅和异步任务。
redux本身并没有取消订阅的方法,所以实际react redux项目中,还要用到桥接二者的工具——react-redux。