Redux基本概念

  Redux用做状态管理,有三个基本的原则

  1,整个应用的状态(state)都存在一个普通的JS对象中。比如管理用户的列表,

const state = [
  {id: 1, name: 'sam', age: '20'},
  {id: 2, name: 'josn', age: '21'}
]

  2,状态是只读的,不能直接修改状态,只能发送(dispatch)一个action,表明要修改状态。action也是一个普通JS对象,用来描述怎样改变状态。它有一个type属性, 要对state执行什么样的操作,通常它是一个描述动作的字符串,比如'increment',  increment 表示增加,一看到这个type,就知道状态要做何种变化了。其次还有一个可选的payload, 表示增加多少啊

{ type: 'increment', wantAddNum: 10 }

  此时,action 也可以看成一个载体,它把应用中的数据传递出去.   

  3, 真正改变状态的地方是纯函数reducer。action表明了要改变状态,也有state,它两个就可以计算出一个新对象(状态)。再强调一下,reducer是一个纯函数,不要改变参数传递进来的state, 永远都要返回新的state对象。

function counter(state, action) {
    switch (action) {
        case 'increment': 
            return state + action.wantAddNum;
        default:
            return state;
    }
}

   但谁来dispatch action?dispatch action后,reducer怎么拿到当前的state和这个action?Redux还有另外一个对象store,创建这个对象的时候,接受reducer函数,同时内部持有一个currentState的变量,再在内部定义dispatch方法。dispatch方法是一个闭包,接受action,同时它又能获取currentstate和reducer,最后把dispatch暴露出去,

function createStore(reducer, initialState) {
    let currentState = initialState; // 存储内部状态

    function dispatch(action) {
        currentState = reducer(currentState, action);
    }

    return { dispatch };
}

  但state改变了,外界怎么获取到?提供一个订阅方法,谁想获取state,就添加一个订阅。

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);
        listeners.forEach(listener => listener()); // dispatch action后,状态改变,所有的监听函数都要执行
    }
return { getState, subscribe, dispatch }; }

  写一个简单的计数器,点击加号,加1,点击减号,减1, 最后还有一个重置按钮。纯粹学习Redux,可以用script标签引入。当使用script标签引入时,window对象会有一个Redux属性. 新建 一个rudex.html, 引入bootstrap  css 和redux, 并新建一个counter.js

<!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>

image

   state就是一个counter, 可以初始化为0, 那么initialState和state的形态就如下

const initialState = { counter: 0 }

  action就三个,加,减,重置, 

const add = { type: 'ADD' }
const minus = { type: 'MINUS' }
const reset = { type: 'RESET' }

  再就是reducer 了, 分别加1, 减1, 重置为初始状态

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,就可以创建store了。

const store = Redux.createStore(counter, initialState);

  点击按钮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,显示到页面上。

const stateDiv = document.getElementById('counter');
function render() { stateDiv.innerHTML = store.getState().counter; }
store.subscribe(render)

  createStore()调用时,Redux内部会dispatch一个action("@@redux/INIT"),没有switch action.type处理它,它就走了default分支,因此一定要写default分支。其次,这种模式,可以用来初始化state 

function counter(state = initialState, action) { //... }

  store的创建就变成了

const store = Redux.createStore(counter);

  写action时,可能会用到action creator来创建action。为什么要创建action呢?考虑Atm取钱的场景,它有100, 500, 1000和输入任意值,都是取钱这一个操作(type一致),只是携带的数据不一致。如果一个一个写action,那就要写4个,  

 { type: 'ADD', data: 100 },
 { type: 'ADD', data: 500 },
 { type: 'ADD', data: 1000 }

  为什么不封装一个函数呢?不一致的数据由函数参数接受,然后返回action对象,这个函数就是action creator. 

function addData(data) {
    return { type: 'ADD', data }
}

  再进一步,还有bindActionCreators,  把action creator和dispatch函数绑定到一起,返回一个函数,因为action始终都是要dispatch的,直接调用返回函数就可以了

const boundAdd = data => store.dispatch(addData(data))  // boundAdd就是一个bindActionCreator

  页面加上两个按钮

<button type="button" class="btn btn-primary" id="add5">Add5</button>
<button type="button" class="btn btn-primary" id="add10">Add10</button>

image

 

  js中修改的地方

function addData(data) { // action creator
    return { type: 'ADD', data }
}

const boundAdd = data => store.dispatch(addData(data)) // bindActionCreator

case 'ADD':
    return { ...state, counter: state.counter + action.data }

document.getElementById('add').addEventListener('click', () => { boundAdd(1); })
document.getElementById('add5').addEventListener('click', () => { boundAdd(5); })
document.getElementById('add10').addEventListener('click', () => { boundAdd(10); })

  随着应用的增大,如果整个应用的状态都放到一个reducer 中,那这个reducer 就太大了,需要拆分reducer,每一个reducer对应应用中某一块的状态。如果要想改变某一块的状态,就要在对应的reducer中监听action,然后改变state。dispatch action不用变,这个action,会到所有的reducer中。但reducer分散,又不能传递给createStore函数了,因为它只接受一个reducer。这时就要用到combineReducers了,它接受一个对象作为参数,每一个key对应的就是一个小reducer, 返回一个根reducer

const rootReducer = Redux.combineReducers({
    numberChange: counter
})
const store = Redux.createStore(rootReducer); 
console.log(store.getState());

  使用combineReducers之后, 程序的state也发生了变化,state 多了一层numberChange, numberChange 下面才是原来的counter。再写一个reducer来描述一下操作,比如点击add或minus,页面上显示‘加或减’, 这个reducer 的初始状态呢,就是一个空字符串

function desc(state = '', action) {
    switch (action.type) {
        case 'ADD':
            return  '' + action.data
        case 'MINUS':
            return '减1';
        case 'RESET':
            return '重置';
        default:
            return state;
    }
}

   rootReducer的创建就变成了如下形式

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() {
    stateDiv.innerHTML = store.getState().numberChange.counter;
    spanDes.innerHTML = store.getState().actionDesc;
}

  页面中加了一个 h6, 用于显示

 <h6>操作的文字描述: <span id="desc"></span></h6>

  combinerReducers有时不太好理解,写一个简易的实现,可能更明白一点。首先它是一个函数,接受一个对象,然后返回一个reducer 

function combineReducers(reducers) {
    return function combination(state, action) {}
}

  dispatch action时,它遍历所有的reducer,  调用它来改变状态。和action无关的reducer也会被调用,再次证明需要defaut分支,返回state。当然在遍历之前,创建一个新的对象,来保存调用reducer产生的新对象,新的的状态。

function combineReducers(reducers) {
    const reducerKeys = Object.keys(reducers);  // 对应reducer的keys,

    return function combination(state = {}, action) {
        const nextState = {}  // reducer都是返回一个新的对象,创建一个新的对象进行保存
        for (const key of reducerKeys) { // 进行真正的遍历 
            const reducer = reducers[key]; // 取出每一个小的reducer函数

            const previousStateForKey = state[key]; // 根据key,从state中找出key对应的state
            const nextStateForKey = reducer(previousStateForKey, action); // 调用reducer,返回新的状态
            nextState[key] = nextStateForKey;  // 新的状态赋值给nextState 更新state
        }
        return nextState; // 返回新的整个的state
    }
}

  Redux开发,就是先想好state长啥样,有哪些action,然后写reducer,根据action创建新state。

 

posted @ 2019-08-10 11:39  SamWeb  阅读(814)  评论(0)    收藏  举报