Redux基本概念
Redux用做状态管理,有三个基本的原则
1,整个应用的状态(state)都存在一个普通的JS对象中。比如管理用户的用户列表,可以如下表示
const state = [ { id: 1, name: 'sam', age: '20' }, { id: 2, name: 'josn', age: '21' } ]
2, 状态是只读的,你不能直接改变状态。改变状态的唯一方法是发送(dispatch)一个action。action也是一个普通的jJS对象,用来描述怎样改变状态,因此它有一个type属性, type描述你要对state执行什么样的操作,通常t它是一个描述动作的字符串,比如'increment', increment 表示增加,一看到这个type,就知道状态要做何种变化了。
{ type: 'increment' }
那增加多少呢?aciton 是个对象,可以任意添加属性啊,想增加多少,再加一个属性就好了。
{ 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的? reducer更新完了状态,那外界怎么知道?又怎么获取呢?这些问题都指向了Redux 另外一个核心的对象store. 那store对象就要保存state,暴露dispatch方法,同时,状态改变后,还要通知外界,并且提供方法,让外界获取到最新状态。那怎么创建这个store呢?为了复用,还是要写一个函数,这个函数返回store对象,因为每一个项目都要创建store。因为是函数,它拥用自己的作用域,在其内部声明的变量,不能被外界访问,state就可以声明为私有变量,那返回的store对象就要有dipsatch方法,通知外界的方法,和外界获取state的方法。那函数要不要接受参数呢?那就是reducer
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 }; }
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开发来说,首先要想好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 的初始值。
整个counter.js 如下
// state const initialState = { counter: 5 }; // action const add = { type: 'ADD' }; const minus = { type: 'MINUS' }; const reset = { type: 'RESET' }; // 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; } } // 创建store const store = Redux.createStore(counter); // btn 添加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); }) // 渲染状态到页面。 const stateDiv = document.getElementById('counter'); function render() { stateDiv.innerHTML = store.getState().counter; } render(); store.subscribe(render);
当写action 的时候,你可能用到action creator. 它也没有别的意思,就是一个函数返回action. 为什么要写一个函数返回action呢?考虑Atm取钱的场景,它有 100, 500, 1000 和输入任意值。都是取钱这一个操作(type 一致),只是携带的数据不一致而已。如果你一个一个写action 的话,那就要写4个, 为什么不封装一下呢?type 一致,携带的数据不一致,不一致的数据由函数传递进来,然后返回这个action, 这个函数就是action creator. 再比如,我们页面上再添加两个add 按钮,一个加5, 一个加10. 如果一个一个写action,就是
{ type: 'ADD', data: 5 } { type: 'ADD', data: 10 } // 默认加1 { type:'ADD', data: 1 }
多重复了三遍type:'ADD', 那就要写一个函数了,把不变的封装起来,可变的作为参数传递
function add(data) { return { type: 'ADD', data } }
这个函数就是action creator. 再进一步,可以还有 bindActionCreators, Redux 甚至也暴露了一个bindActionCreators() 函数出来,它就是把actionCreator 和dispatch函数绑定到一起,因为action 始终都是要dispatch的,把action creator 和dipatch 接合一起,返回一个函数,我们直接调用这个绑定dispatch 的函数就可以了。
const boundAdd = data=> store.dispatch(add(data)) // boundAdd就是一个bindActionCreator
使用这两个方法对我们的应用程序进行改造一下, 首先html 页面加上两个按钮
<body style="text-align: center"> <!-- 显示状态 --> <h1 id="counter"></h1> <button type="button" class="btn btn-primary" id="add">Add</button> <button type="button" class="btn btn-primary" id="add5">Add5</button> <button type="button" class="btn btn-primary" id="add10">Add10</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>
js 中修改的地方进行了加粗和注释
const initialState = { counter: 5 }; // action creator function add(data) { return { type: 'ADD', data } } const minus = { type: 'MINUS' }; const reset = { type: 'RESET' }; // bindActionCreator const boundAdd = data=> store.dispatch(add(data))
// ADD 要加action.data function counter(state = initialState, action) { switch(action.type) { case 'ADD': return { ...state, counter: state.counter + action.data } case 'MINUS': return { ...state, counter: state.counter - 1 } case 'RESET': return { ...state, counter: 5 }; default: return state; } } const store = Redux.createStore(counter); // dispatch action 直接调用boundAdd 函数就好 document.getElementById('add').addEventListener('click', () => { boundAdd(1) }) document.getElementById('add5').addEventListener('click', () => { boundAdd(5) }) document.getElementById('add10').addEventListener('click', () => { boundAdd(10) }) document.getElementById('minus').addEventListener('click', () => { store.dispatch(minus); }) document.getElementById('reset').addEventListener('click', () => { store.dispatch(reset); }) const stateDiv = document.getElementById('counter'); function render() { stateDiv.innerHTML = store.getState().counter; } render(); store.subscribe(render);
使用reducer 的时候,绝大多数情况下会用到combineReducers,因为在createStore只接受一个reducer作为参数。如果整个应用的状态处理都放到一个reducer 中,那这个reducer 就太过庞大,代码也很难维护了,所以肯定是把状态的处理分为不同的一个个小reducer, 每一个小reducer 处理各自的状态更为合理,但reducer 分散,那又不能传递给createStore 函数了,这时候就需要用到combineReducers了,看名字就知道,它是把reducer 合并起来,形成一个大的reducer , 正好可以传给createStore 来创建store.
首先说一下它的用法,它接受一个对象作为参数,对象的属性,你可以随便写,但最好还是有点意义,它的值呢,一定是对应到每一个小reducer, 返回值是一个reducer. 比如现在我们的程序,如果要使用combineReducers, 就要这样写
const rootReducer = Redux.combineReducers({
numberChange: counter
})
使用combineReducers之后, 程序的state也会发生变化,把rootReducer 传递到createStore中,然后console.log(store.getState()); 你会发现state 多了一层numberChange, numberChange 下面才是原来的counter. 当我们使用combineReducers的时候,state也会变成combineReducers的参数的形式, numberChange 直接指向了counter reducer 处理的状态 {couter: 0}
再写一个reducer, 来描述一个操作,比如点击add的时候,页面上显示'加1', 这个例子虽然有点幼稚,但也能体现一个combineReducers的用法。这个reducer 的初始状态呢,就是一个空字符串,对应的的action 呢还是'加', '减', '重置', 那reducer 就如下所示
function desc(state='', action) { switch(action.type) { case '加': return action.data case '减': return '减1'; case '重置': return '重置'; default: return state; } }
store 的创建就变成了如下形式
const rootReducer = Redux.combineReducers({
numberChange: counter,
actionDesc: desc
})
刚才说了,state 也会发生变化,要store.getState().numberChange.counter 才以取到以前的counter. store.getState().actionDesc 才能取到文字描述, render 函数修改如下
const spanDes = document.getElementById('desc'); function render() { console.log(store.getState()) stateDiv.innerHTML = store.getState().numberChange.counter; spanDes.innerHTML = store.getState().actionDesc; }
页面中加了一个 h6, 用于显示
<body style="text-align: center"> <!-- 显示状态 --> <h1 id="counter"></h1> <h6>操作的文字描述: <span id="desc"></span></h6> <button type="button" class="btn btn-primary" id="add">Add</button> <button type="button" class="btn btn-primary" id="add5">Add5</button> <button type="button" class="btn btn-primary" id="add10">Add10</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>
点击的按钮的时候,还要多加一个dispatch
document.getElementById('add').addEventListener('click', () => { boundAdd(1); store.dispatch({type: '加', data: '加1'}); }) document.getElementById('add5').addEventListener('click', () => { boundAdd(5); store.dispatch({type: '加', data: '加5'}) }) document.getElementById('add10').addEventListener('click', () => { boundAdd(10); store.dispatch({type: '加', data: '加10'}) }) document.getElementById('minus').addEventListener('click', () => { store.dispatch(minus); store.dispatch({type: '减'}) }) document.getElementById('reset').addEventListener('click', () => { store.dispatch(reset); store.dispatch({type: '重置'}) })
最终整个js 文件如下
// state const initialState = { counter: 5 }; // action creator function add(data) { return { type: 'ADD', data } } const minus = { type: 'MINUS' }; const reset = { type: 'RESET' }; // bindActionCreator const boundAdd = data=> store.dispatch(add(data)) // reducer, chrome 直接支持... 操作对象, ADD 要加action.data function counter(state = initialState, action) { switch(action.type) { case 'ADD': return { ...state, counter: state.counter + action.data } case 'MINUS': return { ...state, counter: state.counter - 1 } case 'RESET': return { ...state, counter: 5 }; default: return state; } } function desc(state='', action) { switch(action.type) { case '加': return action.data case '减': return '减1'; case '重置': return '重置'; default: return state; } } const rootReducer = Redux.combineReducers({ numberChange: counter, actionDesc: desc }) const store = Redux.createStore(rootReducer); console.log(store.getState()); // btn 添加click事件,dispatch action 直接调用boundAdd 函数就好 document.getElementById('add').addEventListener('click', () => { boundAdd(1); store.dispatch({type: '加', data: '加1'}); }) document.getElementById('add5').addEventListener('click', () => { boundAdd(5); store.dispatch({type: '加', data: '加5'}) }) document.getElementById('add10').addEventListener('click', () => { boundAdd(10); store.dispatch({type: '加', data: '加10'}) }) document.getElementById('minus').addEventListener('click', () => { store.dispatch(minus); store.dispatch({type: '减'}) }) document.getElementById('reset').addEventListener('click', () => { store.dispatch(reset); store.dispatch({type: '重置'}) }) // 渲染状态到页面。 const stateDiv = document.getElementById('counter'); const spanDes = document.getElementById('desc'); function render() { console.log(store.getState()) stateDiv.innerHTML = store.getState().numberChange.counter; spanDes.innerHTML = store.getState().actionDesc; } render(); store.subscribe(render);
页面效果如下
combinerReducers 有时不太好理解,写一个简易的实现原理,可能更明白一点。首先它是一个函数,然后返回一个reducer
function combineReducers(reducers) { return function combination(state, action) { } }
当我们dispatch action的时候,它要遍历所有的reducer, 找到匹配的action 那个reducer, 调用它来改变状态。正好参数reducers 中就一一对应的存在reducer. 但是对象又不好遍历,所以只能使用Object.keys 取出它的keys 然后进行遍历
function combineReducers(reducers) { const reducerKeys = Object.keys(reducers); // 取出参数reducers 的keys, 以便进行遍历获取到真正的reducer 进行调用 return function combination(state = {}, action) { for (let i = 0; i < reducerKeys.length; i++) { // 进行真正的遍历 } } }
但在遍历之前,我们要先创建一个新的对象,因为调用reducer 都要返回一个新的对象。
function combineReducers(reducers) { const reducerKeys = Object.keys(reducers); // 取出参数reducers 的keys, 以便进行遍历获取到真正的reducer 进行调用 return function combination(state = {}, action) { const nextState = {} // reducer 每次都要返回一个新的对象,所以创建一个新的对象 for (let i = 0; i < reducerKeys.length; i++) { // 进行真正的遍历 } } }
真正遍历开始,先取出每一个key,然后根据key,从参数reducer中找出真正的reducer 函数,从state中找出key对应的state, 使用acition 和state 调用 reducer , 返回的状态,再赋值给nextState, 更新state, 遍历完成后,返回整个的nextState
function combineReducers(reducers) { const reducerKeys = Object.keys(reducers); // 取出参数reducers的keys, 以便进行遍历获取到真正的reducer进行调用 return function combination(state = {}, action) { const nextState = {} // reducer 每次都要返回一个新的对象,所以创建一个新的对象 for (let i = 0; i < reducerKeys.length; i++) { // 进行真正的遍历 const key = reducerKeys[i]; // 先取出每一个key const reducer = reducers[key]; // 根据key,从参数reducer中找出真正的reducer 函数 const previousStateForKey = state[key]; // 根据key,从state中找出key对应的state const nextStateForKey = reducer(previousStateForKey, action); // 调用reducer,返回新的状态 nextState[key] = nextStateForKey; // 新的状态赋值给nextState 更新state } return nextState; // 返回整个的nextState } }