乐趣区

Redux的核心概念实现代码与应用示例

Redux 是一种 JavaScript 的状态管理容器,是一个独立的状态管理库,可配合其它框架使用,比如 React。引入 Redux 主要为了使 JavaScript 中数据管理的方便,易追踪,避免在大型的 JavaScript 应用中数据状态的使用混乱情况。Redux 试图让 state 的变化变得可预测,为此做了一些行为限制约定,这些限制条件反映在 Redux 的三大原则中。

本文会介绍 Redux 的几个基本概念和坚持的三大原则,以及完整的回路一下 Redux 中的数据流。在了解以上这些概念之后,用自己的代码来实现一个简版的 Redux,并且用自己实现的 Redux 结合 React 框架,做一个简单的 TodoList 应用示例。希望本文对于初识 Redux 的同学有一个清晰,全面的认识。

Redux 的几个基本概念

一、数据存储 – state

Redux 就是用来管理状态数据,所以第一个概念就是状态数据,state 就是存放数据的地方,根据应用需要,一般定义成一个对象,比如:

{
    todos: [],
    showType: ‘ALL’,
    lastUpdate: ‘2019-10-30 11:56:11’
}

二、行为触发 – action

web 应用,所有的数据状态变更,都是由一个行为触发的,比如用户点击,网络加载完成,或者定时事件。在简单应用里面,我们一般都是在行为触发的时候,直接修改对应的数据状态,但是在大型复杂的应用里面,修改同一数据的地方可能很多,每个地方直接修改,会造成数据状态不可维护。

Redux 引入了 action 的概念,每个要改变数据状态的行为,都定义成一个 action 对象,用一个 type 来标志是什么行为,行为附带的数据,也都直接放在 action 对象,比如一个用户输入的行为:

{
    type: ‘INPUT_TEXT’,
    text: ‘ 今天下午 6 点活动碰头会议 ’
}

然后通过 dispatch 触发这个 action,dispatch(action)

三、行为响应 – reducer

状态,action 的概念了解了,当 action 触发的时候,肯定要修改 state 数据,在讲解 action 的时候有说过,不能直接修改 state,我们需要定义一个 reducer 来修改数据,这个 reducer 就是一个行为响应函数,他接收当前 state,和对应的 action 对象,根据不同的 action,做相应的逻辑判断和数据处理,然后返回一个新的 state。

注意,一定是返回一个新的 state,不能直接修改参数传入的原 state,这是 redux 的原则之一,后面会讲到。

function reducer (state = [], action ) {
    switch (action.type) {
        case ‘INPUT_TEXT’:
            return […state, {text: action.text, id: Math.random() }]
        default:
            return state;
    }
}

四、数据监听 – subscribe

数据的更新已经在 reducer 中完成了,在一些响应式的 web 应用中,我们往往需要监听数据状态的变化,这个时候就可以用 subscribe 了

redux 内部保存一个监听队列,listeners,可以调用 subscribe 来往 listeners 里面增加新的监听函数,每次 reducer 修改完 state 之后,会逐个执行监听函数,而监听函数可以获取已经更新过的 state 数据了

listeners = [];
subscrible(listener) {
    listeners.push(listener);
    return function () {
        let index = listeners.index(listener);
        listeners.splice(index, 1);
    }
}
dispatch(action) // 触发 action
reducer(state, action)

listeners.map(( listener) => {
    listener()
} )

Redux 的几大原则

一、单一数据原则

整个应用的数据都在 state,并且只有这一个 state,这么做的目的是方便管理,整个应用的数据就这一份,调试方便,开发也方便,可以在开发的时候用本地的数据。而且开发同构应用也很方便,比如服务端渲染,把服务端的数据全部放在 state,作为 web 端初始化时候的数据

二、state 只读

state 的数据对外只读,不能直接修改 state,唯一可以修改的方式是触发 action,然后通过 reducer 来处理。

因为所有的修改都被集中化处理,且严格按照一个接一个的顺序执行,因此不用担心竞态条件(race condition)的出现。Action 就是普通对象而已,因此它们可以被日志打印、序列化、储存、后期调试或测试时回放出来。

三、使用纯函数

先说明下什么是纯函数,纯函数指的是函数内部不修改传入的参数,无副作用,在传参一定的情况下,返回的结果也是一定的。Redux 中的 Reducer 需要设计成存函数,不能直接操作传入的 state,需要把改变的数据以一个新的 state 方式返回。

Redux 中的数据流

其实上面讲 Redux 基本概念的时候已经大概的说了下数据流向方式了,就是:view->action->reducer->state->view,用文字来表述就是,首先由于页面上的某些事件会触发 action,通过 dispatch(action)来实现,然后通过 reducer 处理,reducer(state, action)返回一个新的 state,完成 state 的更新,当然对于响应式的应用,会触发 listener(),在 listener 里面获取最新的 state 状态,完成对应视图 (view) 的更新。这就是整个 redux 中的数据流描述,如下图所示:

Redux 的实现代码(非官方)

在对 Redux 的基本概念和几大原则熟悉了之后,可以实现一个自己的 Redux 了,当然我们一般都直接用官方的 npm 包,这里自己实现的比较简单,没有做什么入参验证,异常处理之类的,主要是加深下对 Redux 的理解。下面直接贴代码了,对应的概念都有注释。

// redux.js
// 创建 state 的函数
// 传入 reducer 和初始化的 state
function createStore(reducer, initState) {
    let ref = {};
    let listeners = [];
    let currentState = initState;

    // dispath 函数,用来触发 action
    function dispatch (action) {
        // 触发的 action,通过 reducer 处理
        currentState = reducer(currentState, action)

        // 处理完成后,通知 listeners
        for (let i in listeners) {
            let listener = listener[i];
            listener();
        }
        return action;
    }

    // 返回当前的 state
    function getState () {
        return currentState;
    }

    // 订阅 state 变化,传入 listener,返回取消订阅的 function
    function subscribe (listener) {
        listeners.push(listener);
        return function () {
            let index = listeners.indexOf(listener);
            if (index > -1) {
                listeners.splice(index, 1);
            }
        }
    }
    
    ref = {
        dispatch: dispatch,
        subscribe: subscribe,
        getState: getState
    };
    return ref;
}

function combineReducers(reducers) {
    return function (state, action) {
        let finalState = {};
        let hasChanged = false;
        for (let key in reducers) {
            let reducer = reducers[key]
            if (typeof reducer === ‘function’) {
                let keyState = reducer(state && state[ key], action );
                hasChanged = hasChanged || keyState !== state[key];
                finalState[key] = keyState;
            }
        }
        return hasChanged ? finalState : state;
    }
}

export {createStore, combineReducers}

是不是觉得怎么才这么点代码,就是这么点代码,而且还包含了一个 combineReducers 辅助函数,下面再贴一点使用示例代码

// reducer 函数,用于处理 action
function reducer(state = [], action ) {
    switch(action.type) {
        case ‘INPUT_TEXT’:
            return […state, { text: action.text, key: Math.random(), isDo: false }];
        case ‘TOGGLE_TODO’:
            return state.map(( item) => {
                if (item.key === action.id) {
                    return {…item, isDo: !item.isDo};
                }
            } );
        default:
            return state;
    }
}

let store = createStore(reducer);

// 在用户输入一条 Todo 时候
console.log(store.getState());
store.dispatch({ type: ‘INPUT_TEXT’, text: ‘ 这里是一条待办事项 ’} );
console.log(store.getState());

// 在用户点击一条 Todo Item 的时候,切换完成状态
console.log(store.getState());
store.dispatch({ type: ‘TOGGLE_TODO’, id: item.key} )
console.log(store.getState());

Redux 与 React 的结合应用示例

下面,利用 Redux 结合 React 开发一个简单的 Todo 工具,页面主要功能点

1、可以添加 Todo 事项

2、点击事项会切换事项的完成状态

3、可以切换展示全部 / 已完成 / 待完成事项

这个实例是基于 react,react-redux 完成的,项目搭建用的是 create-react-app,利用 react-redux 提供的接口,将 redux 中的 state 和 action 集成到组件中,需要读者熟悉 create-react-app 的使用,以及 react-redux 的主要接口功能,以下贴出主要代码,感兴趣的同学可以自己搭建实现

首先定义好 state 数据结构和 action 以及对应的 reducer

state 包含两部分,一是 todos,待办事项列表,二是 showType,展示类型

action 包含这么三种,一是添加新的 Todo,二是切换事项完成状态,三是切换展示类型,分别定义好

actions.js

// actions.js
let nextTodoId = 0

export const addTodo = text => {
    return {
        type: ‘ADD_TODO’,
        id: nextTodoId++,
        text
    };
};

export const setShowType = showType => {
    return {
        type: “SET_SHOW_TYPE”,
        showType
    };
};

export const toggleTodo = id => {
    return {
        type: ‘TOGGLE_TODO’,
        id
    };
};

reducers.js

const todos = (state = [], action ) => {
    switch (action.type) {
        case ‘ADD_TODO’:
            return [
                …state,
                {
                    id: action.id,
                    text: action.text,
                    isDo: false
                }
            ];
        case ‘TOGGLE_TODO’:
            return state.map(todo => {
                return todo.id === action.id ? {…todo, isDo: !todo.isDo} : todo;
            } );
        default:
            return state;
    }
}

const showType = (state = ‘SHOW_ALL’, action) => {
    switch (action.type) {
        case ‘SET_SHOW_TYPE’:
            return action.showType;
        default:
            return state;
    }
}

const todoList = combineReducers({
    todos,
    showType
})
export {todoList}

至此,数据状态 redux 部分算完成了,接下来实现对应的 Component 和入口文件了,准备分这么几个组件

1、待办事项 Todo

2、输入框 AddTodo

3、待办事项列表 TodoList

4、底部展示类型切换 Tab

// component.js
import {connnect} from ‘react-redux’;
import {addTodo, setShowType, toggleTodo} from ‘./actions’

const Todo = ({ onClick, completed, text} ) => (
    <li onClick={onClick} style={{textDecoration: completed ? ‘line-through’ : ‘none’}}>
        {text}
    </li>
)

const AddTodo = ({ dispatch} ) => {
    let input;
    return (
        <div>
            <form
                onSubmit={e => {
                    e.preventDefault()
                    if (!input.value.trim() ) {
                        return;
                    }
                    dispatch(addTodo( input.value) )
                    input.value = ”
                }}
            >
                <input ref={node => {input = node} } />
                <button type=’submit’>Add Todo</button>
            </form>
        </div>
    )
}
AddTodo = connect()( AddTodo);

const TodoList =  ({ todos, onTodoClick} ) => {
    return (
        <ul>
            {todos.map( todo => (
                <Todo key={todo.id} {…todo} onClick={() => onTodoClick(todo.id) } />
            ) )}
        </ul>
    ) };
    
const getShowTodoList = (todos, showType) => {
    switch(showType) {
        case ‘SHOW_ISDO’:
            return todos.filter(item => item.isDo);
        case ‘SHOW_ACTIVE’:
            return todos.filter(item => !item.isDo);
        case ‘SHOW_ALL’:
        default :
            return todos;
    }
}

const mapStateToProps = state => {
    return {
        todos: getShowTodoList (state.todos, state.showType)
    };
};

const mapDispatchToProps = dispatch => {
    return {
        onTodoClick: id => {
            dispatch(toggleTodo( id) );
        }
    };
}

const ShowTodoList = connect(
    mapStateToProps,
    mapDispatchToProps
)(TodoList);
   
 const Tab = () => (
    <p>
        Show: {‘ ‘}
        <FilterLink filter=’SHOW_ALL’>ALL</FilterLink>
        {‘, ‘}
        <FilterLink filter=’SHOW_ACTIVE’>ACTIVE</FilterLink>
        {‘, ‘}
        <FilterLink filter=’SHOW_ISDO’>ISDO</FilterLink>
    </p>
)

export {AddTodo, ShowTodoList, Tab}

入口文件 index.js

import React from ‘react’;
import ReactDOM from ‘react-dom’;
import {Provider} from ‘react-redux’;
import {createStore} from ‘./redux’;
import todoList from ‘./reducers’
import {AddTodo, ShowTodoList, Tab} from ‘./component’

let store = createStore(todoApp);

ReactDOM.render(
    <Provider store={store}>
        <div>
            <AddTodo />
            <ShowTodoList />
            <Tab />
        </div>
    </Provider>
    , document.getElementById(‘root’));

主要代码完成,npm start 运行,功能截图如下

文章同步发布:https://www.geek-share.com/detail/2783420870.html

参考文章:

原生实现一个 react-redux 的代码示例

用 React 实现一个完整的 TodoList 的示例代码

退出移动版