网页从远古时代的『webpage』尤其是一种静态页面的存在方式,发展到当下拥有着复杂的功能与交互逻辑的面向「客户端」更愿意被称之为『webapp』的形态的整个过程中,网页的开发不再是简单的界面拼凑来显示静态的内容,而是要通过维护和管理页面上的各种状态
,例如服务端返回的数据、本地临时存储的数据、视图界面该被隐藏或者显示、路由状态等等,来决定用户在不同的交互下,网页该怎样正确的显示预期的结果。而整个『webapp』可以看做一个大型的状态机,当管理这些庞大且又复杂的 states 时,很容易出现不可预测甚至会对一些状态的改变发生『失控』的情景:当一个界面改动而更新了某个 model,而这个model又更新另一个 model,最终产生的结果是与该另一个model相关的界面产生了不可预知的变更...这在拥有着双向数据绑定的前端框架的项目里尤其的面临着难以维护的局面。而redux通过基于单向数据流的模式,背靠其遵循的三大原则,确保每一次它在改变各种状态之后,其结果是可预测的。
redux遵循的三个原则
所有和webapp相关的states都可以存放在一个对象内
该对象被称作一个状态树。例如,通过一个对象来描述一个 todo list 的状态可以如下:
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
即,当前状态下所包含的todo项列表以及过滤列表的显示方式(显示所有列表项包括完成与未完成等)
在状态树里的states只能是可读
改变states的方法只能通过 dispatch an action, action 是一个纯object,用来描述发起了何种action以及它附带的额外数据:
以下是一个完成 todo list 中某一项的action
{
type: 'COMPLETE_TODO',
index: 1
}
即,该动作完成了索引为1的todo项。
状态的变更是通过纯函数来完成的
所谓纯函数,就是单纯地将参数传入并加工输出成一个可以预测的返回值,在这个过程中没有产生任何副作用。这里的副作用包括但不限于对原传参的改动、发起对数据库的操作以及随之产生的对DOM结构的变更。纯函数返回的值总是可预测的
,而非纯函数则更多的机会产生前面提到的状态的不可控性
。
//pure function
function square(x){
return x * x;
}
function squareAll(items){
return items.map(square);
}
// Impure functions
function square(x){
updateXInDatabase(x);
return x * x;
}
function squareAll(items){
for (let i = 0; i < items.length; i++) {
items[i] = square(items[i]);
}
}
在redux里面,我们需要通过一个纯函数来描述状态是如何被改变的,这个纯函数接受一个初始的状态,以及改变这个状态的actions,并且返回一个新的状态。这种方法称之为reducer。
来看一个简单的reducer,一个纯函数,没什么特别的:
const counter = (state = 0, action) => {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
reducer通过返回一个新的state来确保上一个阶段的state没有被改写,这就保证了它的输出结果是可预测的:
expect(
counter(2, { type: 'DECREMENT' })
).toEqual(1);
expect(
counter(0, { type: 'INCREMENT' })
).toEqual(1);
需要留意的是,对于state为数组以及对象的这种情况,我们更要避免直接改变state本身而引起的副作用:
我们可以通过一个deep-freeze
的库来确保数组或者对象类型的state不能被更改,以便来检测我们写的reducer是否会产生副作用,所以当reducer被定义成如下,
const initialState = [];
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
state.push({
index: action.index,
text: action.text,
completed: false
});
return state;
default:
return state;
}
};
//冻住initialState,使其无法被更改
deepFreeze(initialState);
expect(
todos(initialState, {
type: 'ADD_TODO',
index: 0,
text: 'redux',
})
).toEqual([{
index: 0,
text: 'redux',
completed: false
}]);
我们发现reducer函数中的数组push方法未能生效,因为一个被冻住的变量无法被更改。此时如果将产生副作用的push方法改为concat,
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return state.concat({
index: action.index,
text: action.text,
completed: false
});
default:
return state;
}
};
可将以上concat的写法用ES6的...
spread方法代替为,
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
index: action.index,
text: action.text,
completed: false
}
];
default:
return state;
}
};
该reducer返回了一个新的state,符合纯函数的概念。类似的,借助slice
数组方法,可以实现同样纯函数式的删除todo或者是在指定位置插入todo,
const todos = (state = [], action) => {
switch (action.type) {
case 'REMOVE_TODO':
return [
...state.slice(0, action.index),
...state.slice(action.index + 1)
];
case 'INSERT_TODO':
return [
...state.slice(0, action.index),
{
index: action.index,
text: action.text,
completed: false,
},
...state.slice(action.index + 1)
];
default:
return state;
}
};
同样当state为object类型时,我们也可以通过ES6 spread来实现纯函数式的reducer,当标记某个todo项完成时,
const todos = (state = {}, action) => {
switch (action.type) {
case 'TOGGLE_TODO':
return {
...state,
completed: !action.completed
}
default:
return state;
}
};
等同于,
const todos = (state = {}, action) => {
switch (action.type) {
case 'TOGGLE_TODO':
return Object.assign({}, state, {
completed: !action.completed
});
default:
return state;
}
};
Redux Store --- 一个让redux三原则融汇贯通的对象
通过创建store对象,我们可以调用其getState()
、dispatch(action)
、subscribe(listener)
来依次获取当前state
、执行一次action
、注册当state被改变时的回调
,以创建一个简单的计数器为例,
import { createStore } from Redux
//创建一个store,reducer作为参数传入
const store = createStore(counter);
//执行一个action
store.dispatch({ type: 'INCREMENT' });
//当state被改变时,在回调内重新渲染DOM,执行render()
let unsubscribe = store.subscribe(render);
//取消回调函数的注册
unsubscribe();
关于redux store一个很重要的点在于,整个运用了redux的应用里,有且只有一个store,当我们处理不同业务逻辑下的数据时,我们需要通过不同的reducers来处理而不是对应到多个store。所以这么一看来reducer的比重会比较大,我们可以利用redux 提供的 combineReducers()
合并多个reducers到一个根reducer。这样组织reducers的方式有点类似react里一个根组件下有多个子组件。
createStore的源码非常简洁,我们可以用不到20行的代码来简单重现其背后的逻辑,帮助我们更好的理解store,
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
};
};
dispatch({});
return { getState, dispatch, subscribe };
};
为了能够在任何时刻返回对应的state,我们需要一个state变量来记录,getState()只需要负责返回它。
dispatch方法则负责把需要执行的action传给reducer,返回新的state,并同时执行注册过的回调函数。注册的回调可能会有多个,我们通过一个数组来保存即可。subscribe通过返回一个thunk函数,来实现unsubscribe。最后为了能够让store.getState()可以获得初始的state,直接dispatch一个空的action即可让reducer返回initialState。
redux可以和react很好的结合一起使用,我们只需要把react对应的ReactDOM.render()方法写在subscribe回调里,而为了更优雅的在react内书写redux,redux官方提供了react-redux
redux的源码非常简单,它只有2kb大小,更多有关redux的介绍可以参考如下,
参考
redux
redux-cookbook
Getting Started with Redux by the author of Redux
react-redux
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。