理解Redux的实现原理

为何要用Redux

React只是一个视图层的框架,负责把数据映射成DOM元素。但应用程序每每涉及到大量的数据交互和网络请求,修改数据的频率会很高,因此须要一种规范来约束对数据的更新,使得任何修改均可以被追踪,这样才不惧应用程序的复杂性,实现良好的调试性能和可扩展能力。Redux是一个数据流和数据容器管理工具。这篇文章中有一组生动的对比图看有无Redux时数据的处理方式。javascript

经过TodoList的例子推导Redux

下面咱们看一个简单的 todo-list 的例子。 html

//TodoApp.js
const [todos, setTodos] = useState([])
const addTodo = () => {}
const removeTodo = () => {}
const toggleTodo = () => {}

<AddTodo addTodo={addTodo} />
<TodoList removeTodo={removeTodo} toggleTodo={toggleTodo} todos={todos} /> 复制代码
//TodoList.js
{
    todos.map(todo => (
        <TodoItem removeTodo={removeTodo} toggleTodo={toggleTodo} /> )) } 复制代码

经过上图咱们知道,addTodo,removeTodo,toggleTodo这些功能函数都是经过setTodos对todos这个数据进行操做,对数据todos来讲这些操做是不透明的。为了更透明的对todos进行操做,咱们能够用这样的一种结构来描述每次对todos的操做

{
    type: 'add',
    payload: todo
}
复制代码

咱们称它为Action,每次对todos进行操做时都发出这样一个Action,就能够很清楚的看到在对todos进行了什么操做,此次操做携带的数据是什么。直接把这样一个Action对象丢给todos,todos是不知道该怎么办的,因此todos须要一个管家(dispatch)帮它处理而后把处理结果告诉它。下面让咱们用代码来实现一下dispatch。java

const dispatch = (action) => {
    const { type, payload } = action
    swicth(type) {
        case 'set':
            //set的逻辑
            break;
        case 'add':
            //add的逻辑
            break;
        case 'remove':
            //remove的逻辑
            break;
        case 'toggle':
            //toggle的逻辑
            break;
    }   
}
复制代码

有了dispatch这个管家,如今处理addTodo的业务逻辑就很简单了,只须要segmentfault

dispatch({
    type: 'add',
    payload: todo
})
复制代码

由于每一次操做都是一个Action,而每个Action都只有两个参数(type, payload),当操做频繁时每次都写上面的代码会很麻烦,因此咱们考虑构建一个创造Action的函数actionCreator,这样咱们就不用每次都手动生成Action了。由于有不少个Action,对应就会有不少个actionCreator,因此咱们考虑把全部的actionCreator放在一个单独的文件actionCreators.js里bash

//actionCreators.js
export const add = payload => ({
    type: 'add',
    payload
})

export const remove = payload => ({
    type: 'remove',
    payload
})

export const toggle = payload => ({
    type: 'toggle',
    payload
})
复制代码

而后把actionCreators.js引入到TodoList.js中,如今咱们处理addTodo就只须要网络

//TodoList.js
import * as actionCreators from './actionCreators';
dispatch(actionCreators.add(payload))
复制代码

仔细看看每次操做都须要dispatch来派发Action,咱们能够考虑再封装一次,把dispatch也隐藏起来。框架

const addTodo = payload => dispatch(actionCreators.add(payload))
复制代码

这样咱们每次处理addTodo就只须要调用addTodo函数便可。这样的封装操做有不少个,咱们能够批量实现一下,咱们但愿获得下面这样的结果异步

{
    addTodo: payload => dispatch(actionCreators.add(payload)),
    removeTodo: payload => dispatch(actionCreators.remove(payload)),
    toggleTodo: payload => dispatch(actionCreators.toggle(payload))
}
复制代码

因而咱们编写一个bindActionCreators函数来批量封装获得咱们想要的结果函数

function bindActionCreators(actionCreators, dispatch) {
  const ret = {}

  for(let key in actionCreators) {
    ret[key] = function(...args) {
      const actionCreator = actionCreators[key]
      const action = actionCreator(...args)
      dispatch(action)
    }
  }

  return ret
}
复制代码

如今咱们能够这样实现一个addTodo的操做工具

const {
    add: addTodo, 
    remove: removeTodo,
    toggle: toggleTodo
} = bindActionCreators(actionCreators, dispatch)

addTodo(payload)
复制代码

由于TodoList的逻辑很简单,因此咱们这样改造完没有看到很明显的优点,因此让咱们改造一下项目,让项目变得稍微复杂一点,咱们新添加一个incrementCount变量,每次新添加一个todo,incrementCount就会加一。

const [todos, setTodos] = useState([])
const [incrementCount, setIncrementCount] = useState(0)

const dispatch = (action) => {
    const { type, payload } = action
    swicth(type) {
        case 'set':
            //set的逻辑
            setIncrementCount(c => c + 1)
            break;
        case 'add':
            //add的逻辑
            setIncrementCount(c => c + 1)
            break;
        case 'remove':
            //remove的逻辑
            break;
        case 'toggle':
            //toggle的逻辑
            break;
    }   
}
复制代码

如今咱们看代码能够发现多个Action有一样的逻辑,须要重复编码实现,这是由于咱们是从Action的维度来执行的数据更新逻辑,可是这些Action操做都是为了更新数据,为了更加清晰,咱们能够考虑从数据的维度来整理数据的更新逻辑,咱们但愿有这样的一个reducer,它接收state和action而后返回更新后的state数据,每个数据有本身单独的reducer,而后返回合并后的多个reducer,如今让咱们用代码来实现一下。

//reducers.js
const reducers = {
    todos(state, action) {
    const { type, payload } = action
    switch(type) {
        case 'set':
            return //set的逻辑
        case 'add':
            return //add的逻辑
        case 'remove':
            return //remove的逻辑
        case 'toggle':
            return //stoggle的逻辑
        }
        return state      
    },
  incrementCount(state, action) {
    const { type } = action
    switch(type) {
      case 'set':
        return state + 1
      case 'add':
        return state + 1
    }
    return state
  }
}

function combineReducers(reducers) {
  return function reducer(state, action) {
    const changed = {}

    for(let key in reducers) {
      changed[key] = reducers[key](state[key], action)
    }

    return {
      ...state,
      ...changed
    }
  }
}

export default combineReducers(reducers)
复制代码

如今在TodoList.js中引入reducers.js,而后改写dispatch函数

const dispatch = (action) => {
    const state = {
        todo,
        incrementCount
    }

    const setters = {
        todos: setTodos,
        incrementCount: setIncrementCount
    }
    
    const newState = reducer(state, action)
    
    for(let key in newState) {
        setters[key](newState[key])
    }
}
复制代码

reducer的意义在于可以从数据字段的维度来处理action。

上面说了这么多咱们都是在处理同步的Action,如今让咱们思考一下如何处理异步的Action。最直接的想法就是咱们先处理异步的逻辑,异步结束后再派发一次Action,下面让咱们用代码来实现一下

//异步的Action
export const add = text => (dispatch, state) => {
    setTimeout(() => {
      const { todos } = state
      
      if(!todos.find(todo => todo.text === text)) {
        dispatch({
            type: 'add',
            payload: {
                id: Date.now(),
                text,
                complete: false
            }
        })
      }
    }, 3000)
}
复制代码

如今咱们的dispatch只能处理对象,不能处理异步Action的函数,因此让咱们改写一下dispatch让它能够支持对函数的处理

const dispatch = (action) => {
    const state = {
        todo,
        incrementCount
    }

    const setters = {
        todos: setTodos,
        incrementCount: setIncrementCount
    }
    
    if('function' === typeof action) {
        action(dispatch, state)
        return
    }
    
    const newState = reducer(state, action)
    
    for(let key in newState) {
        setters[key](newState[key])
    }
}
复制代码

这样咱们就实现了一个异步的Action,咱们但愿增长todo时先判断原有的todo列表中是否包含新添加todo的内容,若是是再也不添加,若是不是再添加。这时咱们进行调试若是在3s以前删掉了重复的Action,咱们会发现3s后这个重复的Action仍是被添加到了todo的列表中。这是由于add函数拿到的数据是3s前的数据,为了不这种状况的出现,咱们会考虑用函数动态的获取state里面的数据,例如

addTodo(dispatch, () => state)
复制代码

可是state这个对象老是在异步action发起以前临时构成的,若是在3s内作了一些操做,那么数据其实已经发生改变,异步Action内获取到的仍是旧的数据。在每次渲染周期state都会改变,因此咱们能够在组件以外建立一个store来存储全部的state

let store = {
    todo: [],
    incrementCount: 0
}
//TodoList组件内同步数据
useEffect(() => {
    Object.assign(store, {
        todos,
        incrementCount
    })
}, [todos, incrementCount])
复制代码

如今让咱们改写dispatch

const dispatch = (action) => {
    const setters = {
        todos: setTodos,
        incrementCount: setIncrementCount
    }
    
    if('function' === typeof action) {
        action(dispatch, () => store)
        return
    }
    
    const newState = reducer(store, action)
    
    for(let key in newState) {
        setters[key](newState[key])
    }
}
复制代码

改写异步Action

//异步的Action
export const add = text => (dispatch, getState) => {
    setTimeout(() => {
      const { todos } = getState()
      
      if(!todos.find(todo => todo.text === text)) {
        dispatch({
            type: 'add',
            payload: {
                id: Date.now(),
                text,
                complete: false
            }
        })
      }
    }, 3000)
}
复制代码

总结

咱们能够用actionCreators来生成一次操做的Action,用dispatch来派发这个Action,用reducer来更新数据,用bindActionCreators封装多个Action的派发操做,用combineReducers将多个reducer合并成一个。

实际上Redux也只有最基本的功能,它自己不具有对异步Action的处理,可是在Reudx的整个流程中,在Action被dispatch派发到达reducer以前能够通过多个中间件的处理,这些中间件能够加强dispatch的功能,好比Redux-thunk中间件就可让dispatch具有处理异步Action的能力。若是想要对 Redux Store 进行更深层次的加强定制,就须要使用 Store Enhancer,利用 Store Enhancer 能够加强 Redux Store 的 各个 方面。

Action -> dispatch -> 各类中间件 -> reducer -> store
复制代码

后记

我写文章比较少,因此逻辑可能不是很清晰,若是有问题欢迎你们在评论区中提出,咱们一块儿学习讨论。本文是学习React劲爆新特性Hooks 重构去哪儿网火车票PWA这门课后,将老师讲的内容加上一点点本身的理解写成的。顺便安利一下这门课,老师讲的超级棒!!!再推荐一本书,程墨的《深刻浅出React和Redux》,里面对Redux的原理也讲解的十分清晰。

相关文章
相关标签/搜索