Redux用来做状态管理,有三个基本原则
1,无论应用的状态简单还是复杂,整个应用的状态(state)都只存在一个普通的js 对象中,这个对象称为状态树。简单的状态,比如计数器应用,它只有一个state
const state = 0;
复杂的状态,比如用户列表,state 可能就是一个数组包含对象了
const state = [ { id: 1, name: 'sam', age: '20' }, { id: 2, name: 'josn', age: '21' } ]
2, 状态state 是只读的,我们不能直接改变状态。改变状态的唯一方法是发送(dispatch)一个action。action 也是一个普通的js 对象,用来描述怎样改变状态,因此规定它必须有一个type属性, type 来描述状态将要做什么样的改变,你要对state执行什么样的操作。通常type是一个描述动作的字符串,比如'increment', increment 表示增加,一看到这个type,就知道状态要做何种变化了。
{ type: 'increment' }
那增加多少呢?action 可以携带数据啊!aciton 只是一个js 对象,除了必须的type 之外,你可以添加任何的属性,想要携带数据(增加的数字)作为另外一个属性就好了。
{ type: 'increment', wantAddNum: 10 }
action 就变成了一个载体,它把应用中的数据传递给了state.
3, 改变状态,使用纯函数reducer. action 作为一个普通的对象,它自己是不可能改变另外一个对象state的, action 只是描述了一个操作,具体这个操作怎么改变状态的,它不管。那怎样改变状态的呢?也很简单,你可能已经想到了,就是写一个函数,函数可以接受对象作为参数,然后对对象进行操作,返回新的对象。那就可以把state 和action传给一个函数,来计算新的state.,这个函数称之为reducer. 这里要注意,reducer 是一个纯函数,千万不要改变参数传递进来的state, 永远都要返回新的state对象。
function counter(state, action) { switch (action) { case 'increment': return state + action.wantAddNum; default: return state; } }
好了,现在我们有了两个对象state和action, 一个函数reducer, 那这三个毫不相干的内容是怎么联系到一起的?dispatch action 谁来dispatch?dispatch atcion以后,它怎么到reducer中?reducer 除了action之外,还有一个state, 它又是怎么获取到state的? 这些问题都指向了Redux 另外一个核心的对象store. 当然,store 也不是凭空出现的,需要创建。Redux库暴露出一个createStore 函数,它接受reducer 然后返回store.
当使用reducer作为参数的时候,createStore 函数内部肯定能够调用reducer 去改变状态,那就是aciton 和state 怎么传递到createStore里面, 供reducer 使用。首先是action, createStore 返回的store 对象暴露了一个dispatch方法,它接受action, store.dispatch(action). 这样就把action 传递 进去了。state 怎么传到createStore 里面,更准确的说是初始的state, 整个应用的初始state怎么传递到createStore 里面? 有两种方式,最简单的一种是createStore 函数,可以接受一个可选的参数intialState, 直接把初始的state 传递进去就可以了。至此createStore 内部获取到了action, state, 和reducer,终于接合到一起,可以改变state 了。
但这就引出了另外一个问题,state改变了,外界是怎么获取到的呢?还是要找store对象,它有一个方法,store.getState(), 那我什么时候调用getState()来获取更新后的state呢?因为dispatch action 之后, redux 更新state是不确定的,可能需要很长时间呢?store对象的最后一个方法, subscribe(), 它接受一个回调函数,可以在该函数中调用getState() 来获取更新后的状态了。只要dispatch 一个action, 应用的状态发生改变,subscribe中的回调就会执行,确保获取到最新的state. 现在整个redux 的流程就完成了。
根据描述,我们也可以写一个最简单的redux,就是有一个createStore函数,接受reducer, intialState 作为参数,然后返回一个对象,这个对象有getState(), subscribe(), dispatch() 方法
function createStore(reducer, initialState) { let currentState = initialState; // 存储内部状态 let listeners = []; // 存储监听的函数(subscribe 中的回调函数) function getState() { return currentState; } function subscribe(listener) { listeners.push(listener); } function dispatch(action) { currentState = reducer(currentState, action); // dispatch action 后,状态改变,所有的监听函数都要执行。使用了forEach listeners.forEach(listener => listener()); } return { getState, subscribe, dispatch }; }
所以,对于Redux开发来说,首先要想好state,应用中有哪些状态和action,要做哪些操作来改变state. 然后reducer, 根据我们的action 怎么处理state.有了reducer 之后,createStore 创建store, 然后就可以使用store.dispatch 来更新状态,store.subscribe 和store.getState() 来获取更新后的状态。
理论说了这么多,可以实践一下了,写一个简单的计数器,点击加号,加1,点击减号,减1, 最后还有一个重置按钮。为了不受其它框架和库的干扰,这里使用纯html, Redux 用script 标签引用,当使用script 标签引入Redux库之后,window 对象上会有一个Redux 属性. 新建 一个rudex.html, 引入bootstrap css 和redux, 并新建一个counter.js 书写js代码, html文件如下
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>redux</title> <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.0.0/css/bootstrap.min.css"> <script src="https://cdn.bootcss.com/redux/4.0.4/redux.js"></script> </head> <body style="text-align: center"> <!-- 显示状态 --> <h1 id="counter">0</h1> <button type="button" class="btn btn-primary" id="add">Add</button> <button type="button" class="btn btn-success" id="minus">Minus</button> <button type="button" class="btn btn-danger" id="reset">Reset</button> <script src="./counter.js"></script> </body> </html>
开始写js 代码,首先想state, state 很简单,就是一个counter, 可以初始化为0, 那么initialState 和state的形态就如下
const initialState = { counter: 0 }
再想action, 三个按钮,一个加,一个减,一个重置,功能各不相同,那就三个action
// action const add = { type: 'ADD' } const minus = { type: 'MINUS' } const reset = { type: 'RESET' }
再就是reducer 了, 分别加1, 减1, 重置为初始状态
// reducer, chrome 直接支持... 操作对象 function counter(state, action) { switch(action.type) { case 'ADD': return { ...state, counter: state.counter + 1 } case 'MINUS': return { ...state, counter: state.counter - 1 } case 'RESET': return {
...state,
counter: 0
}; default: return state; } }
借着这个简单的reducer, 再重复一下reducer 的注意事项。 reducer 必须返回一个新的对象,而不是对参数中的state 进行修改,就算你更改了, Redux 也会忽略你做的任何更改。再者,由于reducer 必须返回一个新的state对象替换旧的state, 所以旧state中的所有属性都必须先复制到新的state 中,最简单的复制方式,就是使用... 分割符。然后action 要更改哪一个属性状态,直接写到分割符的下面,作为新state 对象的属性,再给它赋一个新值,这样就会覆盖掉旧state 中的该属性的值,完成state的更新。
有了reducer ,就可以创建store了。
const store = Redux.createStore(counter, initialState);
现在就可以点击按钮,dispatch action了。给三个按钮绑定click 事件来dispatch action.
document.getElementById('add').addEventListener('click', () => { store.dispatch(add); }) document.getElementById('minus').addEventListener('click', () => { store.dispatch(minus); }) document.getElementById('reset').addEventListener('click', () => { store.dispatch(reset); })
最后就是获取state,反馈页面, 在subscribe方法中注册一个监听函数, 这个函数只负责把最新的状态赋值给页面元素。
const stateDiv = document.getElementById('counter'); function render() { stateDiv.innerHTML = store.getState().counter; } store.subscribe(render)
现在我们做一个简单的改变,把 <h1 id="counter"> 的元素的内容0 去掉,让它直接从状态中获取初始值。然后把initialState中的conter 改为5
<h1 id="counter"></h1>
const initialState = { counter: 5 }
刷新页面,你会发现,页面上没有状态显示了。这是怎么一回事?因为我们并没有渲染状态到页面元素上,我们只定义了render 函数,并且是在subscribe 中调用了,由于没有点击触发状态的改变,subscribe中函数并不会执行,我们只能手动调用一次render 函数。在store.subscribe(render) 下面手动调用一次, render() , 再刷新页面,你会发现状态显示5,也就是说 store 里面的状态已经变成5了。它是怎么把初始状态转变成store 里面的状态?其实在createStore 创建store 对象的时候,Redux 内部dispatch了一个action("@@redux/INIT"), action 触发,肯定会调用counter reducer , 由于我们的reducer 没有处理这个action,它走了switch 的default 分支,也说是说,在creatorStore的时候,reducer 中的defualt 分支永远会被调用,这就给我们提供了初始化state的第二 种方式, 直接在default 分支中提供默认值,或使用Es6 的默认参数。
// reducer, chrome 直接支持... 操作对象 function counter(state = initialState, action) { switch(action.type) { case 'ADD': return { ...state, counter: state.counter + 1 } case 'MINUS': return { ...state, counter: state.counter - 1 } case 'RESET': return { ...state, counter: 5 }; default: return state; } }
这时createStore 方法中initialState 就可以去掉了。
const store = Redux.createStore(counter);
这里也是在写reduer 时要注意的一个点,每一个reducer 都要有一个default 分支,一是提供没有处理的action, 二是提供整个store 的初始值。