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

 

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