zoukankan      html  css  js  c++  java
  • Redux 核心概念

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

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

    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 的初始值。

      整个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
        }
    }
  • 相关阅读:
    flex
    两端对齐
    background-clip、background-origin、box-sizing
    animation
    transform translate transition 的区别
    如何将页脚固定在页面底部
    normalize.css
    使用 Swift 制作一个新闻通知中心插件(1)
    在 App 扩展和主 App 间共享数据
    asp.net core Cookie认证
  • 原文地址:https://www.cnblogs.com/SamWeb/p/11311358.html
Copyright © 2011-2022 走看看