Redux 原理

139 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

Flux

首先,复习一下 Flux 四大元素

  • Dispacher:调度器,接收到Action,并将它们发送给Store。
  • Action:动作消息,包含动作类型与动作描述。
  • Store:数据中心,持有应用程序的数据,并会响应Action消息。
  • View:应用视图,可展示Store数据,并实时响应Store的更新。

单向数据流

Store 的数据更新一般有两个

  1. 用户交互带来的数据更新
  2. 又一些比如定时器或者其他的事件系统导致的,数据更新

其实就是从 View 去修改 Store; 但是在 Redux 中需要经过 Dispatcher 去修改 Store

image.png

单一方向数据流还具有以下特点:

  • 集中化管理数据: 常规应用可能会在视图层的任何地方或回调进行数据状态的修改与存储,而在Flux架构中,所有数据都只放在Store中进行储存与管理。
  • 可预测性 : 在双向绑定或响应式编程中,当一个对象改变时,可能会导致另一个对象发生改变,这样会触发多次级联更新。对于Flux架构来讲,一次Action触发,只能引起一次数据流循环,这使得数据更加可预测。
  • 方便追踪变化: 所有引起数据变化的原因都可由Action进行描述,而Action只是一个纯对象,因此十分易于序列化或查看。

Flux的工作流

从上面的章节中我们大概知道了Flux中各个角色的职责, 现在来看一下 flux 的工作流

图中有一个Action Creator的概念,其实他们就是用于辅助创建Action对象,并传递给Dispatcher:

image.png

Flux与React

在靠近视图的顶层结构中,有一个特殊的视图层,在这里我们称为视图控制器( View Controller ),它用于从Store中获取数据并将数据传递给视图层及其后代,并负责监听Store中的数据改变事件。

当接受到事件时,首先视图控制器会从Store获取最新的数据,并调用自身的setStateforceUpdate函数,这些函数会触发View的render与所有后代的re-render方法。

通常我们会将整个Store对象传递到View链的顶层,再由View的父节点依次传递给后代所需要的Store数据,这样能保证后代的组件更加的函数化,减少了Controller-View的个数也意味着使更好的性能。

Redux

Redux是JavaScript应用可预测的状态管理容器,它具有以下特性:

  • 可预测性,使用Redux能帮助你编写在不同的环境中编写行为一致、便于测试的程序。
  • 集中性,集中化应用程序的状态管理可以很方便的实现撤销、恢复、状态持久化等。
  • 可调试,Redux Devtools提供了强大的状态追踪功能,能很方便的做一个时间旅行者。
  • 灵活,Redux适用于任何UI层,并有一个庞大的生态系统。

它还有三大原则:

  1. 单一数据源。整个应用的State储存在单个Store的对象树中。

  2. State状态是只读的。不应该直接修改State,而是通过触发Action来修改它。

    Action是一个普通对象,因此它可以被打印、序列化与储存。

  3. 使用纯函数进行修改状态。为了指定State如何通过Action操作进行转换,需要编写reducers纯函数来进行处理。reducers通过当前的状态树与动作进行计算,每次都会返回一个新的状态对象。

与Flux的不同之处

Redux受到了Flux架构的启发,但在实现上有一些不同:

  • Redux并没有 dispatcher。它依赖纯函数来替代事件处理器,也不需要额外的实体来管理它们。Flux尝尝被表述为:(state, action) => state,而纯函数也是实现了这一思想。
  • Redux为不可变数据集。在每次Action请求触发以后,Redux都会生成一个新的对象来更新State,而不是在当前状态上进行更改。
  • Redux有且只有一个Store对象。它的Store储存了整个应用程序的State。'

基本元素

  1. Store
  2. Action
  3. Reducer:不仅仅是逻辑,还可以指定仓库的初始数据

action 和 reducer 是用户编写的,作为一个库我们只用负责 Store 就好啦

核心原理:发布订阅

store 提供3个 API:

  1. 订阅: register
  2. 更新数据:dispatch
  3. 获取数据:getState

在 register 的时候,把重新渲染页面的回调函数 注册到 store 中;

在 dispatch 的时候,就依次调用该回调,让组件更新

💡 忽然想到了在 Vue2 中,是劫持`get`方法,自动订阅这个数据的更新;

几个工具方法

bindActionCreators

在第一章中有看到 Action Creator 这个成员, 在写代码过程中,也会有这样的工厂方法

那么一般而言,我们分为两步

  1. action = XXActionCreator(payload)
  2. dispatch(action)

bindActionCreators 的作用就是整合这两步

实现原理: 返回值是一些包裹了 dispatch 方法的函数

combineReducers

在 redux 中,除了 store 本身是单例的,传给 store 的 reducer 本身也是单例的;

在 实际项目中, reducer 一般为根据实际场景分别在不同的文件中编写,例如:userReducer, goodsReducer …

所以 combineReducers 的作用就是把所有的 reducer 整合到一个中

实现原理:把一个 Action,把全部的 reducer 都走一边

简单手写 Redux

createStore

function createStore(reducer:any, initState?:any){
    let state = initState || undefined;
    const listeners:Array<()=>void> = [];
    function getState(){
        return state;
    }
    function dispatch(action:any){
        const newState = reducer(state, action);
        state = newState;
        listeners.forEach(listener=>listener());
    }
    function subscribe(listener:()=>void){
        listeners.push(listener)
        return ()=>{
            listeners.splice(listeners.IndexOf(listener), 1)
        }
    }
    const store = {
        getState,
        dispatch,
        subscribe
    }
    dispatch({type:"@redux/init"})
    return store
}

我们可以关注到 dispatch({type:"@redux/init"}) ,为什么需要提前派发一个内置的action呢?

因为在使用 redux 的时候, 我们有两处来声明这个仓库的数据结构初始值

  1. createStore(reducer, initState):这里的 InitState,可以看到上面代码的 let state = initState;

已经让这一处的代码生效了

  1. function userReducer(oldState={state:”logout”}, action){ …. }: 这里的 oldState 也可以通过这种 js语言来声明一个默认值

    而在此处声明默认值更加贴合我们的使用习惯,因为这样可以让 state数据 在代码编写层面上达到模块化的功能,并且让第一次 dispatch 的时候的 oldState 更加符合预期

    那么 dispatch({type:"@redux/init"}) 可以让所有reducer 执行一次,但是因为都会命中 switch 中的 default, 也就是 return oldState , 这样就让默认值生效了

💡 typescript 怎么拿到 Store 这个数据类型
type Reverse<T> = (arg: any) => T;
function returnResultType<T>(arg: Reverse<T>): T {
    return {} as T;
}
const res = returnResultType(createStore);
export type Store = typeof res;

bindActionCreators

用法

  1. 最开始的调用: dispatch({type:"ADD"})
  2. 升级一个工厂函数: dispatch(createAdd())
  3. 将dispatch再包裹进去
function createAdd(n){
	return {
    	type:"ADD",
      	payload: n
    }
}

const dispatchAdd = bindActionCreators(createAdd, dispatch);
<button onClick={()=>dispatchAdd(n)}>CLCIK</button>

实现

function bindActionCreator(actionCreator, dispatch){
    return function(...args){
        // 这里不用箭头函数,保证 apply 可以绑定到正确的 this
       dispatch(actionCreator.apply(this, args))
    }
}
function bindActionCreators(actionCreators, dispatch){
    const bindActionCreators:any = {};
    for(const key in actionCreators){
        const actionCreator = actionCreators[key];
        bindActionCreators[key] = bindActionCreator(actionCreator, dispatch)
    }
    return bindActionCreators
}

combineReducers

用法

function counterReducer(oldState = 0, action:{type: Action} ){
    switch(action.type){
        case "ADD":
            return oldState + 1;
        case "SUB":
            return oldState - 1;
				default:
						return oldState
    }
}

function userReducer(oldState:any, action:any){
    switch(action.type){
        case "LOGIN":
            return {state: "login"}
        case "LOGOUT":
            return {state: "logout"};
				default:
						return oldState
    }
}

const reducer = combineReducers({
    counterReducer,
    userReducer
})

let store = createStore(reducer);
export default store;

实现

// 这里的 reducers 是一个 key-value对象, 需要整合成一个 函数(reducer)
function combineReducers(reducers:any){
    const resultReducer = function(state:any = {}, action:any){
        let nextState:any = {};
        for(let key in reducers){
        // 所以要求,每个 reducer 都要有兜底返回
        // 每个 action 都要跑全部 reducer
        // 并且对于 re-render 其实是没有区分模块的,会全部重新渲染
            nextState[key] = reducers[key](state[key], action)
        }
        return nextState;
    }
    return resultReducer
}

export default combineReducers;