网页从远古时代的『webpage』尤为是一种静态页面的存在方式,发展到当下拥有着复杂的功能与交互逻辑的面向「客户端」更愿意被称之为『webapp』的形态的整个过程当中,网页的开发再也不是简单的界面拼凑来显示静态的内容,而是要经过维护和管理页面上的各类状态
,例如服务端返回的数据、本地临时存储的数据、视图界面该被隐藏或者显示、路由状态等等,来决定用户在不一样的交互下,网页该怎样正确的显示预期的结果。而整个『webapp』能够看作一个大型的状态机,当管理这些庞大且又复杂的 states 时,很容易出现不可预测甚至会对一些状态的改变发生『失控』的情景:当一个界面改动而更新了某个 model,而这个model又更新另外一个 model,最终产生的结果是与该另外一个model相关的界面产生了不可预知的变动...这在拥有着双向数据绑定的前端框架的项目里尤为的面临着难以维护的局面。而redux经过基于单向数据流的模式,背靠其遵循的三大原则,确保每一次它在改变各类状态以后,其结果是可预测的。前端
该对象被称做一个状态树。例如,经过一个对象来描述一个 todo list 的状态能够以下:react
{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] }
即,当前状态下所包含的todo项列表以及过滤列表的显示方式(显示全部列表项包括完成与未完成等)git
改变states的方法只能经过 dispatch an action, action 是一个纯object,用来描述发起了何种action以及它附带的额外数据:github
如下是一个完成 todo list 中某一项的actionweb
{ type: 'COMPLETE_TODO', index: 1 }
即,该动做完成了索引为1的todo项。数据库
所谓纯函数,就是单纯地将参数传入并加工输出成一个能够预测的返回值,在这个过程当中没有产生任何反作用。这里的反作用包括但不限于对原传参的改动、发起对数据库的操做以及随之产生的对DOM结构的变动。纯函数返回的值老是可预测的
,而非纯函数则更多的机会产生前面提到的状态的不可控性
。redux
//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没有被改写,这就保证了它的输出结果是可预测的:app
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; } };
经过建立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