React 数据流方案专题(Redux、MobX)

Redux专题内容简介:

  • Redux核心
    它是JavaScript的状态容器(就是一个JavaScript对象),提供可预测化的状态管理。在这个容器当中可以保存很多元素的状态。
    在大型项目中,当状态发生变化的时候,使用redux使之变得可预测。当项目当中的状态发生问题的时候,我们可以很容易地定位到问题出现在哪里。

    Redux的核心概念及其工作流程:
            Store:存储状态的容器,是一个JavaScript对象
            View:视图,也就是我们常见的HTML页面,他要想响应store中状态的变化,则需要通过subscribe方法来为视图设置状态的变化订阅。要想对store中的状态进行修改,则需要通过dispatch方法来发布通知要对状态进行的操作Actions,进而调用相对应的Reducers方法以更新store的状态。
            Actions:对象,描述对状态进行怎样的操作
            Reducers:函数,操作状态并返回新的状态(最后会被更新到Store对象中)
    工作流程为:
    1、创建store对象:首先使用Redux.createStore(reducer) 方法创建状态容器Store
    2、创建reducer函数:reducer函数有两个参数,分别为 state 和 action。在reducer函数中,我们可以通过action参数,通过switch判断语句来判断针对所触发(dispatch)的action要进行的状态操作,从而修改并返回修改后的state。该函数会被作为store对象创建时的第一个参数。
    3、声明initialState作为默认的初始状态,它是一个对象,可以作为reducer函数的state参数的默认值。
    4、定义描述要进行的状态变更的action对象
    5、通过在视图中触发相应的操作时,在操作监听的回调函数中通过调用store对象的dispatch(action)(发布通知)方法通知触发相应的action,以此来改变store中的状态。
    6、当页面交互事件触发后就会触发相应的action被reducer接收到,从而作出相对应的状态变更。
    7、当store中的状态发生了改变后,需要同步视图,这是通过调用store.subscribe(callback)来实现的,在callback回调当中进行我们视图的相应操作或者页面内容更新。需要用到当前相应的状态数据可以通过调用store.getState().xxx来获得。store.getState()就是获取store对象中存储的状态。

    总结:redux使用时用到的核心API 如下,
    1)创建Store状态容器
            const store = Redux.createStore(reducer);
    2)创建用于状态处理的reducer函数
            function reducer(state=initialState, action) {...}
    3)获取store容器中的状态
            store.getState()
    4)订阅状态
            store.subscribe(function(){...})
    5)触发Action
            store.dispatch(action); action是一个描述要进行怎样的状态操作的对象,如,{type: 'description...'}
     
  • React + Redux
    在React中使用Redux解决的问题: 组件架构中的数据流向是单向的,只能由父传子,如果没有很好的组织管理这些数据,就会容易造成数据混乱,出现问题时难以定位。
    在react项目中加入redux的好处:使用redux管理数据,由于Store独立于组件,使得数据管理独立于组件,解决了组件与组件之间的数据传递困难的问题。
    工作流程:
    1)组件通过 dispatch 方法触发 action
    2)Store 接受 action, 并将action 分发给 Reducer
    3)Reducer 根据 action 类型对状态进行更改并将更改后的状态返回给 Store
    4)组件订阅了 Store 中的状态,Store 中的状态更新会同步到组件

    要想使之完美结合,我们需要用到 Provider 组件与 connect 方法: react-redux 
    也就是通过Provider组件作为最外部组件,将store 放在全局的组件可以获取的地方。在需要用到Provider提供的store的组件中,引入并使用react-redux的 connect 方法,这个方法内部会帮助我们去订阅store,
    1)不再需要我们手动的去调用store.subscribe方法来订阅状态数据变更。当store中的state状态发生变化的时候,它会帮我们重新渲染这个组件。
    2)通过connect 方法可以拿到store中的状态,使用connect方法的第一个参数(mapStateToProps),可以通过connect当中的方法将store中的状态映射到组件的props属性中,这样一来,我们就可以在组件当中通过props属性来获取组件中的状态了。
    3)通过connect 方法我们还可以拿到 dispatch 方法。

    使用connect方法的第二个参数(mapDispatchToProps)简化组件视图:
    将dispatch方法映射到组件的props属性中。他是一个函数,这个函数要求返回一个对象,在这个返回的对象当中定义什么,那么这些内容都会被映射到组件的props属性中,在组件定义时直接使用props的结构,可以大大简化组件视图的定义。

    bindActionsCreators方法的使用:它是redux当中定义的方法,可以帮助我们生成dispatch发送通知的函数。要告知bindActionsCreators我们要生成的方法名叫什么,要触发的action 是什么。它的第一个参数是一个对象,第二个参数就是dispatch方法,也就是上述connect方法中获得的dispatch函数。bindActionsCreators 方法的返回值就是一个对象。从而方便我们不依赖dispatch方法,抽离重复代码,也就是ActionCreators。

    Action 传递参数:
    在定义ActionCreator的方法定义时可以接受参数,返回包含payload属性的对象,在调用时使用参数进行传递并使用即可。

    拆分合并Reducer:
    需要借助 redux 中提供的 combineReducers 方法,那么reducer最终的返回对象数据结构则发生了变化:
    combineReducers({
        counter: CounterReducer,
        modal: ModalReducer
    })
    
    // 最后的reducer返回对象为: { counetr: {count: 0}, modal: {show: false}}

  • Redux 中间件
    中间件允许我们扩展redux应用程序。它本质上是一个函数,action处理的流程会先经过中间件处理后再交给reducer,让reducer继续处理这个action。
  • 开发Redux中间件
    开发redux中间件有模版代码,他本质就是一个函数。
    // 柯里化函数形式
    export default store => next => action => {}

    只有把创建好的中间件注册给store,那他才能够在redux的工作流程中生效。它的注册需要使用到redux的一个 applyMiddleware 方法,这个方法就是用来注册中间件的,该方法的返回值放在 createStore 方法的第二个参数中,这样一来,当组件去触发action的时候,这个中间件的代码就可以得到执行了。
    如果注册的中间件有多个,那么它的执行顺序是如何的呢?
    它会按照注册的顺序分别调用相对应的中间件,当第一个中间件调用执行完成以后,它里面会调用next方法返回action对象传递给下一个中间件,依次类推。
    注意:中间件必须要调用next(action),把action 外后面传递,后面的中间件以及最后的reducer才能正确的接收到action并执行。

    值得注意的是:
    1、当前的中间件函数是不关心你想执行什么样的异步操作的,只关心你执行的是不是异步操作。
    2、如果你执行的是异步操作,你在触发action的时候给中间件传递一个函数, 如果执行的是同步操作,就传递action对象。
    3、异步操作代码要写在传递进来的函数当中。
    4、当前这个中间件函数在调用你传递进来的函数时,要将dispatch 方法传递进来。
     

    export default ({dispatch}) => next => action => {
        if(typeof action === 'function') {
            return action(dispatch)
        }
        next(action)
    }

  • Redux实践 综合案例
    工作中常见常用中间件:
    1) redux-thunk。运行我们在redux工作流中加入异步代码。
    2)redux-saga。解决的问题是可以将异步操作从action creator 中抽离出来,放在一个单独的文件中。takeEvery 接收action。put 触发action。delay 设置执行延迟时间。
    合并用到all方法,传入一个数组。
    3)redux-actions。帮我门解决的问题是redux流程中大量的样板代码读写很痛苦,使用redux-actions 可以简化 action 和 reducer 的处理。
    // 使用 redux-actions 来创建 action
    import { createAction } from 'redux-actions'
    
    const increment_action = createAction('increment') // 第一个字符串就是原来我们所创建的action对象中 type 的属性值
    const decrement_action = createAction('decrement')
    这里的 createAction就相当于我们之前自己所定义ActionCreator 函数。
    redux-actions 中还有一个方法是用来为上边定义的action 创建对应的 reducer 处理函数逻辑的,它就是 handleActions 方法。该方法的返回值就是我们之前自己创建的那个reducer 函数。
    // counter reducer 使用 redux-actions 来实现
    import { handleActions as createReducer } from 'redux-actions'
    import { increment_action, decrement_action } from '../actions/counter.action'
    
    const initialState = {count: 0}
    
    const counterReducer = createReducer({
        [increment_action]: (state, action) => ({ count: state.count + 1}),
        [decrement_action]: (state, action) => ({ count: state.count - 1})
    }, initialState)
    
    export default counterReducer
    shopping购物车案例进本实现思路:
    服务器的异步请求交由redux-saga处理;
    页面本地store的数据交由redux 的 reducer 来处理;
    action由redux-actions来简化创建。

Redux源码实现:核心逻辑
redux中主要是一个createStore方法,该方法接收三个参数,createStore(reducer, preloaddedState, enhancer)。
reducer 是根据action的 类型来对 store 当中的状态进行更改。
preloadedState 是预先存储的初始化state 状态对象。
enhancer 是对store的功能进行增强,是我们所说的插件应用。
 

// redux的核心逻辑 createStore 方法

function createStore(reducer, preloadedState, enhancer) {
    // 保存初始化store 状态对象, 需要在后续的方法中使用到,所需需要在方法中形成闭包,以便该值不会在createStore方法调用后被回收
    var currentState = preloadedState;
    var listeners = [];

    // 定义getState 方法,返回当前store 中的currentState 对象
    function getState() {
        return currentState;
    }

    // 定义 dispatch 方法,用于触发action, 从而把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上,最后遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
    function dispatch (action) {
        // 把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上
        currentState = reducer(action);
        // 遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
        for(let i = 0; i<= listeners.length; i++) {
            var listener = listeners[i]
            listener()
        }
    }
    
    // 定义subscribe 方法,用于为store 添加的订阅者
    function subscribe(listener) {
        listeners.push(listener)
    }

    return {
        getState, // 获取store的状态
        dispatch, // 触发action
        subscribe // 订阅状态
    }
    
}

上面的代码实现过去简单粗暴,对参数的类型没有做限制,但参数不符合的时候没有错误提示,整个运行会直接挂掉。所以这里我们需要改进一下,对一些细节问题作容错处理。
1、 reducer 参数的类型必须是一个函数,该函数有两个参数,state 和 action 分别指示 store 中的状态以及本次action的对象。
2、是判断 dispatch 方法中的参数对象,action 中是否是对象,且对象中包含type 属性。

// redux的核心逻辑 createStore 方法

function createStore(reducer, preloadedState, enhancer) {

    // 约束reducer 参数类型,它必须是一个function 类型
    if(typeof reducer !== 'function') throw new TypeError('reducer must be a function');

    // 保存初始化store 状态对象, 需要在后续的方法中使用到,所需需要在方法中形成闭包,以便该值不会在createStore方法调用后被回收
    var currentState = preloadedState;
    var listeners = [];

    // 定义getState 方法,返回当前store 中的currentState 对象
    function getState() {
        return currentState;
    }

    // 定义 dispatch 方法,用于触发action, 从而把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上,最后遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
    function dispatch (action) {
        // 判断action 是否为对象
        if(!isPlainObject(action)) throw new TypeError('action must be a plain object');

        // 判断action 对象中是否有type 属性
        if(typeof action.type === 'undefined') throw new Error('action object must contain a type property');
        // 把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上
        currentState = reducer(action);
        // 遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
        for(let i = 0; i<= listeners.length; i++) {
            var listener = listeners[i]
            listener()
        }
    }
    
    // 定义subscribe 方法,用于为store 添加的订阅者
    function subscribe(listener) {
        listeners.push(listener)
    }

    return {
        getState, // 获取store的状态
        dispatch, // 触发action
        subscribe // 订阅状态
    }
    
}

// 判断obj参数是否为对象
function isPlainObject(obj) {
    // 排除基本数据类型和null
    if(typeof obj !== 'object' || obj === null) return false;
    
    // 区分数组和对象。采用原型对象对比的方式,对象的原型和其最顶层的原型类型是相同的,都是 Object
    var proto = obj;
    while(Object.getPrototypeOf(proto)!==null) {
        proto = Object.getPrototypeOf(proto)
    }
    
    return Object.getPrototypeOf(obj) === proto;
}

Rudex 源码之 enhancer 参数:
可以让createStore 这个方法的调用者对 返回的store 对象进行功能上的增强。 enhancer 参数可以不传,但是如果传了的话,那它必须得是一个具有固定格式的函数。

// redux的核心逻辑 createStore 方法

function createStore(reducer, preloadedState, enhancer) {

    // 约束reducer 参数类型,它必须是一个function 类型
    if(typeof reducer !== 'function') throw new TypeError('reducer must be a function');
    

    // 判断enhancer 参数有没有传递
    if(typeof enhancer !== 'undefined') {
        // 判断enhancer 是不是一个函数
        if(typeof enhancer !== 'function') throw new TypeError('enhancer must be a function');
        return enhancer(createStore)(reducer, preloadedStore);
    }
    // 保存初始化store 状态对象, 需要在后续的方法中使用到,所需需要在方法中形成闭包,以便该值不会在createStore方法调用后被回收
    var currentState = preloadedState;
    var listeners = [];

    // 定义getState 方法,返回当前store 中的currentState 对象
    function getState() {
        return currentState;
    }

    // 定义 dispatch 方法,用于触发action, 从而把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上,最后遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
    function dispatch (action) {
        // 判断action 是否为对象
        if(!isPlainObject(action)) throw new TypeError('action must be a plain object');

        // 判断action 对象中是否有type 属性
        if(typeof action.type === 'undefined') throw new Error('action object must contain a type property');
        // 把action 交给 reducer 进行处理,并且将reducer处理过后的返回值更新到 currentState 的引用上
        currentState = reducer(action);
        // 遍历所有已注册的订阅者,触发相对应的后续操作,如页面更新等
        for(let i = 0; i<= listeners.length; i++) {
            var listener = listeners[i]
            listener()
        }
    }
    
    // 定义subscribe 方法,用于为store 添加的订阅者
    function subscribe(listener) {
        listeners.push(listener)
    }

    return {
        getState, // 获取store的状态
        dispatch, // 触发action
        subscribe // 订阅状态
    }
    
}

// 判断obj参数是否为对象
function isPlainObject(obj) {
    // 排除基本数据类型和null
    if(typeof obj !== 'object' || obj === null) return false;
    
    // 区分数组和对象。采用原型对象对比的方式,对象的原型和其最顶层的原型类型是相同的,都是 Object
    var proto = obj;
    while(Object.getPrototypeOf(proto)!==null) {
        proto = Object.getPrototypeOf(proto)
    }
    
    return Object.getPrototypeOf(obj) === proto;
}

enhancer 参数的例子:模仿实现了类似redux-thunk

function enhancer(createStore) {
    return function(reducer, preloadedState) {
        var store = createStore(reducer, preloadedStore);
        var dispatch = store.dispatch
        function _dispatch(action) {
            if(typeof action === 'function') {
                action(dispatch)
            }
            dispatch(action)
        }
        return {
            ...store,
            dispatch: _dispatch
        }
    }
​​​​​​​}

enhancer的目的地是让用redux这个库的人可以对返回的store 进行功能上的一些增强操作,例如允许加入异步操作代码。 

Redux源码之 applyMiddleware
中间件就是允许我们在action 发出之后, reducer 接收到action 之前,让我们去做一些事情。
本质上 redux 中间件 就是对 dispatch 这个方法进行增强。
例如logger 日志中间件
 

// logger.middleware.js
function logger(store) {
    return (next) => {
        return (action) => {
            console.log(action)
            next(action)
        }
    }
}

又如thunk 中间件允许我们执行异步代码操作
 

// thunk.middleware.js

function thunk(store) {
    return (next) => {
        return (action) => {
            console.log(action)
            next(action)
        }
    }
}

applyMiddleware 方法是如何让中间件先起作用的。
 

function applyMiddleware(...middlewares) {
    return function (createStore) {
        return function (reducer, proloadedState) {
            // 这里的返回应该是跟上边提到过的enhancer是一样的
            // 创建 store 
            var store = createStore(reducer, preloadedState);
            // 构建中间件所需的阉割版的store API
            var middlewareAPI = {
                getState: store.getState,
                dispatch: store.dispatch
            };

            // 调用中间件的第一层函数,传递阉割版的store 对象
            var chain = middlewares.map(middleware => middleware(middlewareAPI));

            var dispatch = compose(...chain)(store.dispatch);
            // 返回增强过后的store
            return {
                ...store,
                dispatch
            }
        }
    }
}

function compose() {
    var funcs = [...arguments];
    return function (dispatch) {
        // 中间件需要按顺序调用,所以在座位参数传递的时候需要进行倒序处理一下。
        for(var i = funcs.length; i>=0; i--) {
            dispatch = funcs[i](dispatch);
        }
        return dispatch;
    }
}


Redux源码实现之 bindActionCreators
 

function increment() {
    return { type: 'increment'}
}

function decrement() {
    return { type: 'decrement'}
}

var actions = bindActionsCreators({increment, decrement}, store.dispatch);
// 这样处理过后,就可以在订阅者侦听处理函数中,使用actions.increment() 或者 actions.decrement() 来触发action 显得更加的自然。
// bindActionsCreators 方法定义
function bindActionsCreators(actionCreators, dispatch) {
    var boundActionCreators = {};
    for (var key in actionCreators) {
        // 为 key 构造闭包保护
        (function(key){
            boundActionCreators[key] = function() {
                // 对外触发action
                dispatch(actionCreators[key]())
            }
        })(key);
    }
    return boundActionCreators;
}

Redux源码实现之 combineReducers
        我们可以把大的reducer 拆分为一个个 小的 reducer,然后再让我们通过combineReducers 这个 API 把一个个小的 reducer 组合成一个大的的 reducer。它的用法是: var rootReducer = combineReducers({ couter: counterReducer, model: modelReducer }); 他的最终返回值是一个 reducer 函数。它有两个参数,一个是state, 一个是action。

function combineReducers(reducers) {
    // 需要完成两件事:
    // 1、检查reducer 类型, 必须为函数
    var reducerKeys = Object.keys(reducers);
    for(var i = 0; i<= reducerKey.length; i++){
        var key = reducerKey[i];
        if(typeof reducers[key] !== 'function') throw new TypeError('reducer must be a function');
    }
    // 2、调用一个一个小的reducer, 将reducer中 返回的状态存储在一个新的大的对象中。
    
    return function(state, action) {
        var nextState = {};

        // 循环执行一个个reducer
        for(var i = 0; i<= reducerKey.length; i++){
            var key = reducerKey[i];
            var reducer = reducers[key];
            var previousStateForKey = state[key];
            // 把处理后的返回值存储在nextState中
            nextState[key] = reducer(previousStateForKey, action);
        }
        return nextState;
    }
}



Redux Toolkit (redux工具集)
​​​​​​​它是官方对redux的二次封装,用于高效Redux开发,使得Redux的使用变得更简单。
 

npm install @reduxjs/toolkit
npm install react-redux

状态切片的概念:stateSlice
对于状态切片,我们可以认为它就是原本我们在redux中的那一个个的小的reducer函数。

在Redux 中,原本 Reducer 函数和 Action 对象需要分别创建,现在则通过状态切片替代,它会返回Reducer函数和 Action对象。

// 创建todos 状态切片
import { createSlice } from '@reduxjs/toolkit'
const TODO_SLICE_KEY = 'todos'
const { reducer: TodosReducer, actions } = createSlice({
  name: TODO_SLICE_KEY,
  initialState: [],
  reducers: {
    addTodo: (state, action) => {
      state.push(action.payload)
    }
  }
})

export const { addTodo } = actions
export default TodosReducer

使用工具集创建Store
 

import { configureStore } from '@reduxjs/toolkit'
import TodosReducer, { TODO_SLICE_KEY } from './todos.slice'

export default configureStore({
  reducer: {
    [TODO_SLICE_KEY]: TodosReducer
  },
  devTools: process.env.NODE_ENV !== 'production'
})


配置Provider触发Action

import React from 'react'
import { useDispatch, useSelector } from 'react-redux'
import { addTodo, TODO_SLICE_KEY } from '../../store/todos.slice'
import { deleteTodo } from '../../store/todos.slice'
export default function (props) {
  const dispatch = useDispatch()
  const todos = useSelector(state => state[TODO_SLICE_KEY])

  return (
    <section className='main'>
      <button
        onClick={() => {
          dispatch(addTodo({ title: 'test task' }))
        }}
      >
        添加任务
      </button>
      <ul className='todo-list'>
        {todos.map((todo, index) => (
          <li className='completed' key={todo.id}>
            <div className='view'>
              <input className='toggle' type='checkbox' />
              <label>{todo.title}</label>
              <span>{todo.id}</span>
              <button
                className='destroy'
                onClick={() => dispatch(deleteTodo({ index, todo }))}
              >
                delete
              </button>
            </div>
          </li>
        ))}
      </ul>
    </section>
  )
}


Action预处理
当action 被触发后,可以通过prepare 方法对action 进行预处理,处理完成之后再交给Reducer,prepare 方法必须返回对象。

// 创建todos 状态切片
import { createSlice } from '@reduxjs/toolkit'
export const TODO_SLICE_KEY = 'todos'
const { reducer: TodosReducer, actions } = createSlice({
  name: TODO_SLICE_KEY,
  initialState: [],
  reducers: {
    addTodo: {
      reducer: (state, action) => {
        state.push(action.payload)
      },
      prepare: todo => {
        console.log('addTodo>>>', todo)
        return {
          payload: { id: Math.random(), ...todo }
        }
      }
    },
    deleteTodo: {
      reducer: (state, action) => {
        console.log('deleteTodo reducer>>>', state, action)
        state.splice(action.payload.deleted, 1)
      },
      prepare: todo => {
        console.log('deleteTodo>>>', todo)
        return {
          payload: { deleted: todo.index }
        }
      }
    }
  }
})

export const { addTodo, deleteTodo } = actions
export default TodosReducer

执行异步操作方式:

1)createAsyncThunk方法:该方法的作用是用来创建用于执行异步操作的Action Creator函数。

在第二个参数中通过thunkAPI的dispatch方法来在获取数据后触发保存数据到本地的操作。

// 创建todos 状态切片
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'

export const TODO_SLICE_KEY = 'todos'

export const loadTodos = createAsyncThunk(
  'todos/loads',
  (payload, thunkAPI) => {
    axios
      .get(payload)
      .then(response => thunkAPI.dispatch(setTodos(response.data)))
  }
)

const { reducer: TodosReducer, actions } = createSlice({
  name: TODO_SLICE_KEY,
  initialState: [],
  reducers: {
    addTodo: {
      reducer: (state, action) => {
        state.push(action.payload)
      },
      prepare: todo => {
        console.log('addTodo>>>', todo)
        return {
          payload: { id: Math.random(), ...todo }
        }
      }
    },
    deleteTodo: {
      reducer: (state, action) => {
        console.log('deleteTodo reducer>>>', state, action)
        state.splice(action.payload.deleted, 1)
      },
      prepare: todo => {
        console.log('deleteTodo>>>', todo)
        return {
          payload: { deleted: todo.index }
        }
      }
    },
    setTodos: (state, action) => {
      console.log('setTodos reducer>>>', state, action)
      action.payload.forEach(todo => state.push(todo))
    }
  }
})

export const { addTodo, deleteTodo, setTodos } = actions
export default TodosReducer


2)createAsyncThunk方法:因为方法本身返回的就是一个Action Creator函数,实际上我们是可以接收这个Action的,而不需要另外再触发一个新的Action。所以可以在第二个参数的函数中返回一个Promise。
这里我们需要在创建状态切片的时候,配置一个extraReducers 属性,用于创建接收异步操作结果的Reducer。 The recommended way of using extraReducers is to use a callback that receives a ActionReducerMapBuilder instance.

// 创建todos 状态切片
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'
import axios from 'axios'

export const TODO_SLICE_KEY = 'todos'

export const loadTodos = createAsyncThunk(
  'todos/loads',
  // (payload, thunkAPI) => {
  //   axios
  //     .get(payload)
  //     .then(response => thunkAPI.dispatch(setTodos(response.data)))
  // }
  payload => axios.get(payload).then(response => response.data)
)

const { reducer: TodosReducer, actions } = createSlice({
  name: TODO_SLICE_KEY,
  initialState: [],
  reducers: {
    addTodo: {
      reducer: (state, action) => {
        state.push(action.payload)
      },
      prepare: todo => {
        console.log('addTodo>>>', todo)
        return {
          payload: { id: Math.random(), ...todo }
        }
      }
    },
    deleteTodo: {
      reducer: (state, action) => {
        console.log('deleteTodo reducer>>>', state, action)
        state.splice(action.payload.deleted, 1)
      },
      prepare: todo => {
        console.log('deleteTodo>>>', todo)
        return {
          payload: { deleted: todo.index }
        }
      }
    },
    setTodos: (state, action) => {
      console.log('setTodos reducer>>>', state, action)
      action.payload.forEach(todo => state.push(todo))
    }
  },
  extraReducers: {
    [loadTodos.fulfilled]: (state, action) => {
      console.log('loadTodos.fulfilled reducer>>>', state, action)
      action.payload.forEach(todo => state.push(todo))
    },
    [loadTodos.pending]: (state, action) => {
      console.log('loadTodos.pending reducer>>>', state, action)
      return state
    }
  }
})

export const { addTodo, deleteTodo, setTodos } = actions
export default TodosReducer

为工具集@reduxjs/toolkit 配置中间件:configureStore 和 getDefaultMiddlware 
通过配置,可以添加我们自己的中间件。值得注意的是reduxjs/toolkit 它自己本身就已经内置了一些中间间,所以我们在配置自己的中间件时需要把本来内置的中间件获取到再跟我们自己要添加的中间件组合到一起,设置给 store 的 middleware 属性,它的值是一个数组。

customizing the included middleware

import { configureStore, getDefaultMiddleware } from '@reduxjs/toolkit'
import TodosReducer, { TODO_SLICE_KEY } from './todos.slice'
import logger from 'redux-logger'

export default configureStore({
  reducer: {
    [TODO_SLICE_KEY]: TodosReducer
  },
  middleware: [...getDefaultMiddleware(), logger],
  devTools: process.env.NODE_ENV !== 'production'
})

实体适配器  与 createEntityAdapter 方法

实体就是我们抽象出来的数据,我们可以理解实体适配器就是一个放置数据的容器,将状态放入实体适配器中,实体适配器提供操作状态的各种方法,简化操作。简化我们对数据的常规操作,增删改查等。

如何创建实体适配器 createEntityAdapter
createEntityAdapter 方法的返回值就是一个实体适配器。在实体适配器中它给我们提供了一些方法,如:getInitialState() ,addOne(state, action.payload) ,addMany(state, action.payload) 等。

Object.keys(entityAdapter)

[
'selectId', 
'sortComparer', 
'getInitialState', 
'getSelectors', 
'removeAll', 
'addOne', 
'addMany', 
'setOne', 
'setMany', 
'setAll', 
'updateOne', 
'updateMany', 
'upsertOne', 
'upsertMany', 
'removeOne', 
'removeMany'
]

getInitailState 返回值就是一个字典: 

我们在创建状态切片时配置的状态初始值就应该设置成该方法的放回值。
 

实体适配器要求每一个实体必须拥有id属性作为唯一标识,如果实体中的唯一标识字段不叫做 id,需要使用 selectId 进行声明。selectId 它的值是一个函数,参数是一个实体,返回值是实体中唯一标识字段的值。

 

const todosAdapter = createEntityAdapter({ selectId: todo => todo.cid })

状态选择器

提供从实体适配器中获取状态的快捷途径。

import {
  createSelector
} from '@reduxjs/toolkit'

const { selectAll } = todosAdapter.getSelectors()
export const selectTodos = createSelector(
  state => state[TODO_SLICE_KEY],
  selectAll
)


Mobx专题简介

  • mobx是一个简单的可扩展的状态管理库,无样板代码风格简约。
  • Mobx通常与react配合使用,但也可以与angular 和 vue 配合使用。
  • 目前版本为MobX 6,不推荐使用装饰器方法,可以在支持ES5环境中运行。

核心概念:

observable:被MobX 跟踪的状态。

action:允许修改状态的方法,在严格模式下只有action 方法被允许修改状态。

computed:根据现有状态衍生出来的状态。

flow:执行副作用,它是generator 函数,可以更改状态值。

工作流程:

资源下载:

mobx 核心库

mobx-react-lite 仅支持函数组件

mobx-react 既支持函数组件,也支持类组件

yarn add mobx mobx-react-lite

 

计数器案例: 在组件中显示数值状态,单击【+1】按钮使数值加1,单击【-1】按钮使数值减1。
1、创建状态的类,及其修改状态数值的方法

export default class CounterStore {

    constructor() {
        this.count = 0;
    }

    increment() {
        this.count += 1;
    }

    decrement() {
        this.count -= 1;
    }
}

2、让MobX 可以追踪状态的变化

        2.1 通过使用 observable 标识状态,使状态可观察。

        2.2 通过 action 标识局改状态的方法,状态只有通过action 方法修改后才会通知视图更新。

这里就需要用到 mobx库中的 makeObservable 方法。以及 action 和 observable 标识。

import { action, observable, makeObservable} from 'mobx'

export default class CounterStore {

    constructor() {
        this.count = 0;
        makeObservable(this, {
            count: observable,
            increment: action,
            decrement: action
        })
    }

    increment() {
        this.count += 1;
    }

    decrement() {
        this.count -= 1;
    }
}

makeObservable方法的第一个参数是this, 指向的是类的实例对象本身。标识类实例对象中的属性及方法。 第二个参数是一个配置对象,在配置中指定哪些属性是状态可以被观察,哪些方法是action。

3、创建Store 类的实例对象,并将实例对象传递给组件使用。

4、在组件中通过store 实例对象获取状态以及操作状态的方法。

5、在组件中使用到的 MobX 管理的状态发生变化后,使视图更新。通过 observer 方法包裹组件实现该目的。

import React from 'react'
import { observer } from 'mobx-react-lite'
function Counter ({ counterStore }) {
  return (
    <section>
      <h4>计数器案例</h4>
      <button onClick={() => counterStore.decrement()}>-1</button>
      <span
        style={{
          width: '100px',
          display: 'inline-block',
          textAlign: 'center',
          fontSize: '20px'
        }}
      >
        {counterStore.count}
      </span>
      <button onClick={() => counterStore.increment()}>+1</button>
    </section>
  )
}

export default observer(Counter)
// import * as React from 'react'
import React from 'react'
import Todos from './components/Todos'
import Counter from './components/Counter'
import CounterStore from './store/CouterStore'

const counterStore = new CounterStore()

function App () {
  React.useEffect(() => {
    console.log('useEffect')
  }, [])
  return (
    <div>
      <Todos />
      <Counter counterStore={counterStore} />
    </div>
  )
}

export default App

如果在组件中将store 进行简化,结构出来每一个属性方法单独使用,那么就需要我们方法中的this指向要始终指向store 实例对象。因为简化后结构出来的修改状态的方法的this 指向出现了问题,我们可以通过在makeObservable的时候,使用action.bound 标识 修改状态的方法,让其强制绑定this,使得 this 指向 Store 实例对象。

import { action, observable, makeObservable } from 'mobx'
export default class CounterStore {
  constructor () {
    this.count = 0
    makeObservable(this, {
      count: observable,
      increment: action.bound,
      decrement: action.bound
    })
  }
  increment () {
    this.count += 1
  }
  decrement () {
    this.count -= 1
  }
}
import React from 'react'
import { observer } from 'mobx-react-lite'
function Counter ({ counterStore }) {
  const { count, increment, decrement } = counterStore
  return (
    <section>
      <h4>计数器案例</h4>
      <button onClick={decrement}>-1</button>
      <span
        style={{
          width: '100px',
          display: 'inline-block',
          textAlign: 'center',
          fontSize: '20px'
        }}
      >
        {count}
      </span>
      <button onClick={increment}>+1</button>
    </section>
  )
}

export default observer(Counter)

这样一来,this 的指向问题解决了,我们在组件当中无论怎么使用修改状态的方法都不会出现问题了。

总结:状态变化更新视图的必要条件

1、状态属性必须被标记为 observable

2、更改状态的方法必须被标记为 action

3、组件必须通过 observer 方法进行包裹

三个条件缺一不可。

如何创建和管理 RootStore 

为什么需要:因为一个应用中可存在多个 Store ,多个 Store 最终要通过 RootStore 管理,在每个组件都需要用到 RootStore 。

这个需求我们可以通过 react 提供给我们的 context 上下文管理的相关方法来配合实现。

它们是 createContext 和 useContext 方法。

// root.store.js
import React from 'react'
import CounterStore from './CouterStore'
import { createContext, useContext } from 'react'
class RootStore {
  constructor () {
    this.counterStore = new CounterStore()
  }
}

const rootStore = new RootStore()
const RootContext = createContext()

export const RootStoreProvider = ({ children }) => {
  return (
    <RootContext.Provider value={rootStore}>{children}</RootContext.Provider>
  )
}

export const useRootStore = () => {
  return useContext(RootContext)
}
// import * as React from 'react'
import React from 'react'
import Todos from './components/Todos'
import Counter from './components/Counter'
import CounterStore from './store/CouterStore'

import { RootStoreProvider } from './store/root'

// const counterStore = new CounterStore()

function App () {
  React.useEffect(() => {
    console.log('useEffect')
  }, [])
  return (
    <RootStoreProvider>
      <Todos />
      <Counter />
    </RootStoreProvider>
  )
}

export default App

Todo 任务列表案例

异步获取服务器数据或者进行其他异步操作时,需要把方法设置成generator 函数*开头的异步处理函数, 函数内部使用 yield 等待处理结果返回。 并且在构建状态管理库的时候,把实例对象的这些异步处理函数用 flow 标识为会产生副作用的函数。 

// store/Todo.js
import { action, makeObservable, observable } from 'mobx'
import axios from 'axios'

export default class Todo {
  constructor (todo) {
    this.id = todo.id
    this.title = todo.title
    this.isEditing = false
    this.isCompleted = todo.isCompleted || false
    makeObservable(this, {
      title: observable,
      isCompleted: observable,
      isEditing: observable,
      modifyTodoIsCompleted: action.bound,
      modifyTodoIsEditing: action.bound,
      modifyTodoTitle: action.bound
    })
  }

  modifyTodoIsCompleted () {
    this.isCompleted = !this.isCompleted
  }

  modifyTodoIsEditing () {
    this.isEditing = !this.isEditing
  }

  modifyTodoTitle (title) {
    this.title = title
    this.isEditing = false
  }

  // 修改远端服务器数据
  * modifyTodoFromServer (todo) {
    let response = yield axios.put(
      `http://localhost:3001/todos/${todo.id}`,
      todo
    )
    this.title = response.data.title
    this.id = response.data.id
  }
}
// store/TodoStore.js
import { action, computed, flow, observable, makeObservable } from 'mobx'
import axios from 'axios'
import Todo from './Todo'

export default class TodoStore {
  constructor () {
    this.todos = []
    this.filterCondition = 'All'

    makeObservable(this, {
      todos: observable,
      filterCondition: observable,
      loadTodos: flow,
      addTodo: action.bound,
      createdId: action.bound,
      removeTodo: action.bound,
      unCompletedTodosCount: computed,
      changeFilterCondition: action.bound,
      filterTodos: computed,
      clearCompletedTodos: action.bound
    })
    this.loadTodos()
  }
  * loadTodos () {
    let response = yield axios.get('http://localhost:3001/todos')
    response.data.forEach(todo => this.todos.push(new Todo(todo)))
  }

  addTodo (title) {
    this.todos.push(new Todo({ title, id: this.createdId() }))
  }
  // 创建自增ID
  createdId () {
    console.log(this.todos.length)
    if (!this.todos.length) return 1
    return this.todos.reduce((id, todo) => (id < todo.id ? todo.id : id), 0) + 1
  }

  removeTodo (id) {
    this.todos = this.todos.filter(todo => todo.id !== id)
  }

  get unCompletedTodosCount () {
    return this.todos.filter(todo => !todo.isCompleted).length
  }

  changeFilterCondition (condition) {
    this.filterCondition = condition
  }
  get filterTodos () {
    switch (this.filterCondition) {
      case 'Active':
        return this.todos.filter(todo => !todo.isCompleted)
      case 'Completed':
        return this.todos.filter(todo => todo.isCompleted)

      default:
        return this.todos
    }
  }

  clearCompletedTodos () {
    this.todos = this.todos.filter(todo => !todo.isCompleted)
  }
}
// store/index.js

import React from 'react'
import { createContext, useContext } from 'react'
import CounterStore from './CouterStore'
import TodoStore from './TodoStore'

class RootStore {
  constructor () {
    this.counterStore = new CounterStore()
    this.todoStore = new TodoStore()
  }
}

const rootStore = new RootStore()
const RootContext = createContext()

export const RootStoreProvider = ({ children }) => {
  return (
    <RootContext.Provider value={rootStore}>{children}</RootContext.Provider>
  )
}

export const useRootStore = () => {
  return useContext(RootContext)
}
// components/Todos/index.js
import React from 'react'
import TodoApp from './TodoApp'
import TodoHeader from './Header'
import TodoFooter from './Footer'
import TodoMain from './Main'

export default function (props) {
  return (
    <TodoApp>
      <TodoHeader />
      <TodoMain />
      <TodoFooter />
    </TodoApp>
  )
}
// components.Todos/TodoApp.js 样式组件
import styled from '@emotion/styled'

export default styled.section`
  background: #fff;
  margin: 130px auto 40px auto;
  position: relative;
  min-width: 230px;
  max-width: 540px;
  box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1);
  & input::-webkit-input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
  }

  & input::-moz-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
  }

  & input::input-placeholder {
    font-style: italic;
    font-weight: 300;
    color: #e6e6e6;
  }

  & h1 {
    position: absolute;
    top: -155px;
    width: 100%;
    font-size: 100px;
    font-weight: 100;
    text-align: center;
    color: rgba(175, 47, 47, 0.15);
    -webkit-text-rendering: optimizeLegibility;
    -moz-text-rendering: optimizeLegibility;
    text-rendering: optimizeLegibility;
  }
  & .new-todo,
  & .edit {
    position: relative;
    margin: 0;
    width: 100%;
    font-size: 24px;
    font-family: inherit;
    font-weight: inherit;
    line-height: 1.4em;
    border: 0;
    color: inherit;
    padding: 6px;
    border: 1px solid #999;
    box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2);
    box-sizing: border-box;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
  & .new-todo {
    padding: 16px 16px 16px 60px;
    border: none;
    background: rgba(0, 0, 0, 0.003);
    box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03);
  }

  & .main {
    position: relative;
    z-index: 2;
    border-top: 1px solid #e6e6e6;
  }

  & .todo-list {
    margin: 0;
    padding: 0;
    list-style: none;
  }

  & .todo-list li {
    position: relative;
    font-size: 24px;
    border-bottom: 1px solid #ededed;
  }

  & .todo-list li:last-child {
    border-bottom: none;
  }

  & .todo-list li.editing {
    border-bottom: none;
    padding: 0;
  }

  & .todo-list li.editing .edit {
    display: block;
    width: 506px;
    padding: 12px 16px;
    margin: 0 0 0 43px;
  }

  & .todo-list li.editing .view {
    display: none;
  }

  & .todo-list li .toggle {
    text-align: center;
    width: 40px;
    /* auto, since non-WebKit browsers doesn't support input styling */
    height: auto;
    position: absolute;
    top: 0;
    bottom: 0;
    margin: auto 0;
    border: none; /* Mobile Safari */
    -webkit-appearance: none;
    appearance: none;
  }

  & .todo-list li .toggle:after {
    content: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E');
    display: block;
    margin-top: 11px;
  }

  & .todo-list li .toggle:checked:after {
    content: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E');
  }

  & .todo-list li label {
    word-break: break-all;
    padding: 15px 60px 15px 15px;
    margin-left: 45px;
    display: block;
    line-height: 1.2;
    transition: color 0.4s;
  }

  & .todo-list li.completed label {
    color: #d9d9d9;
    text-decoration: line-through;
  }

  & .todo-list li .destroy {
    display: none;
    position: absolute;
    top: 0;
    right: 10px;
    bottom: 0;
    width: 40px;
    height: 40px;
    margin: auto 0;
    font-size: 30px;
    color: #cc9a9a;
    margin-bottom: 11px;
    transition: color 0.2s ease-out;
  }

  & .todo-list li .destroy:hover {
    color: #af5b5e;
  }

  & .todo-list li .destroy:after {
    content: '×';
  }

  & .todo-list li:hover .destroy {
    display: block;
  }

  & .todo-list li .edit {
    display: none;
  }

  & .todo-list li.editing:last-child {
    margin-bottom: -1px;
  }

  & .footer {
    color: #777;
    padding: 10px 15px;
    height: 20px;
    text-align: center;
    border-top: 1px solid #e6e6e6;
  }

  & .footer:before {
    content: '';
    position: absolute;
    right: 0;
    bottom: 0;
    left: 0;
    height: 50px;
    overflow: hidden;
    box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6,
      0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6,
      0 17px 2px -6px rgba(0, 0, 0, 0.2);
  }

  & .todo-count {
    float: left;
    text-align: left;
  }

  & .todo-count strong {
    font-weight: 300;
  }

  & .filters {
    margin: 0;
    padding: 0;
    list-style: none;
    position: absolute;
    right: 0;
    left: 0;
  }

  & .filters li {
    display: inline;
  }

  & .filters li button {
    color: inherit;
    padding: 0 7px;
    text-decoration: none;
    border: 1px solid transparent;
    border-radius: 3px;
  }

  & .filters li button:hover {
    border-color: rgba(175, 47, 47, 0.1);
  }

  & .filters li button.selected {
    border-color: rgba(175, 47, 47, 0.2);
  }

  & .clear-completed,
  & html .clear-completed:active {
    float: right;
    position: relative;
    line-height: 20px;
    text-decoration: none;
    cursor: pointer;
  }

  & .clear-completed:hover {
    text-decoration: underline;
  }

  & .info {
    margin: 65px auto 0;
    color: #bfbfbf;
    font-size: 10px;
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
    text-align: center;
  }

  & .info p {
    line-height: 1;
  }

  & .info a {
    color: inherit;
    text-decoration: none;
    font-weight: 400;
  }

  & .info a:hover {
    text-decoration: underline;
  }
  & button {
    margin: 0;
    padding: 0;
    border: 0;
    background: none;
    font-size: 100%;
    vertical-align: baseline;
    font-family: inherit;
    font-weight: inherit;
    color: inherit;
    -webkit-appearance: none;
    appearance: none;
    -webkit-font-smoothing: antialiased;
    -moz-osx-font-smoothing: grayscale;
  }
`
// components/Todos/Main.js

import React from 'react'
import { observer } from 'mobx-react-lite'
import { useRootStore } from '../../store'
import Todo from './Todo'

function Main () {
  const { todoStore } = useRootStore()
  const { filterTodos } = todoStore

  return (
    <section className='main'>
      <ul className='todo-list'>
        {filterTodos.map(todo => (
          <Todo todo={todo} key={todo.id} />
        ))}
      </ul>
    </section>
  )
}

export default observer(Main)
// components/Todos/Todo.js

import React from 'react'
import { observer } from 'mobx-react-lite'
import TodoCompleted from './TodoCompleted'
import TodoEditing from './TodoEditing'
import TodoRemove from './TodoRemove'
import classname from 'classnames'
import Editing from './Editing'

function Todo ({ todo }) {
  return (
    <li
      className={classname({
        completed: todo.isCompleted,
        editing: todo.isEditing
      })}
    >
      <div className='view'>
        <TodoCompleted todo={todo} />
        <TodoEditing todo={todo} />
        <TodoRemove id={todo.id} />
      </div>
      <Editing todo={todo} />
    </li>
  )
}

export default observer(Todo)
// components/Todos/Editing.js

import React, { useEffect, useRef } from 'react'
function Editing ({ todo }) {
  const ref = useRef(null)
  const { modifyTodoIsEditing, modifyTodoTitle, title, isEditing } = todo
  useEffect(() => {
    if (isEditing) {
      ref.current.focus()
    }
  }, [isEditing])

  return (
    <input
      ref={ref}
      contentEditable={isEditing}
      onDoubleClick={modifyTodoIsEditing}
      onBlur={() => modifyTodoTitle(ref.current.value)}
      className='edit'
      defaultValue={title}
    />
  )
}

export default Editing

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值