Redux随笔

发布时间 2023-04-18 13:24:57作者: 丢?的牧羊人

Redux 是 JavaScript 应用的状态容器,提供可预测的状态管理,也就是说Redux不单单只能在React中使用,可以在Vue、Angular等框架中当成状态容器来使用,也可以单独使用,如同JQuery就是一个库,而不是像Vuex这种需要依赖Vue的状态管理容器。

参考:https://zhuanlan.zhihu.com/p/20597452

在学习Redux前先看下Redux架构的工作流程图。Store代表容器,UI表示视图,订阅了State;Dispatch则起到发布Action更新State,订阅了State的视图就会更新页面。在这个过程中,State是不能被直接修改的,只能通过Dispatch发布Action更新State,然后Action会被Reducer接受并进一步判断操作,执行对State的处理,最终返回新的状态,然后同步到视图层上,整个过程是单向数据流的。

 

1. 基础示例应用

让我们看一个Redux应用最小的示例 - 计数器应用程序;示例里使用了script标签去加载了Redux库,用了基础的JS、HTML去写UI

将示例的代码按步骤拆开,可以看到首先是定义了状态的默认值

// 应用状态初始状态值
const initialState = {
  count: 0
}

 接着,定义了一个reducer方法,接受两个固定形式参数,一个是初始状态,另外一个是描述发生了什么的action对象。当应用启动时,还没有任何状态,所以我们提供initialState 作为该 reducer 的默认值。

// 创建一个“reducer”函数来确定应用程序中发生某些事情时的新状态
function reducer(state = initialState, action) {
  // Reducers 通常会查看发生的action 的 type 来决定如何更新状态
  switch (action.type) {
    case 'increment':
      return { count: state.count + 1 };
    case 'decrement':
      return { count: state.count - 1 };
    default:
      // 如果 reducer 不关心这个action type,原样返回现有状态
      return state
  }
}

紧接着通过将reducer作为参数传递给createStore创建store实例,如果存在多个reducer,需要通过combineReducers合并处理;然后在将返回的函数作为createStore的实参创建store实例;最后是定义Action对象。

// 如果存在多个reducer,需要合并处理
// const combineReducer = Redux.combineReducers({ reducer });
// var store = Redux.createStore(combineReducer);

// 3. 创建 store 对象
var store = Redux.createStore(reducer);

// 4. 定义 action
var increment = { type: "increment" };
var decrement = { type: "decrement" };

最后就是通过dispatch发布action更新state

// 5. 获取按钮 给按钮添加点击事件
document.getElementById("plus").onclick = function () {
    // 6. 触发action
    store.dispatch(increment);
};

document.getElementById("minus").onclick = function () {
    // 6. 触发action
    store.dispatch(decrement);
};

// 7. 订阅 store
store.subscribe(() => {
    // console.log(store.getState().reducer.count); // 合并多个reducer

    // 获取store对象中存储的状态
    // console.log(store.getState());
    document.getElementById("count").innerHTML = store.getState().count;
});

一个完成的示例就完成了,虽然内容有限,但是它确实展示了真正的 Redux 应用程序的所有工作部分,并且无论是多复杂的过程,都是基于以上基础进行扩展的。

 

2. Redux的核心API

createStore:接受函数参数,创建Store状态容器;

combineReducers:如果有多个reducer,通过combineReducers合并;

bindActionCeators:bindActionCeators函数接受action和dispatch,并返回一个对象,实际是将dispatch和每一个action建立了关系;

applyMiddleware:接受函数参数;使用包含自定义功能的middleware来扩展Redux的方式,比如用到的redux-thunk、redux-saga;

compose:接受多个函数,并且从右到左组合成最终函数返回;当需要多个middleware的时候需要用到它

getState:Store实例上的函数,可以获取状态;

subscribe:订阅状态,是Store实例上的函数,接受一个回调函数作为参数,当容器中的状态发生改变就会触发回调;

dispatch:发布action,Store实例上的函数,接受action对象;

 

3. createStore原理

createStore接受reducer创建Store状态容器,那么createStore到底是怎么实现的?,让我们从源码上来理解下

function createStore(reducer, preloadedState, enhancer) {
  // ... 省略
  
    function dispatch(){}
    function subscribe(){}
    function replaceReducer(){}
    function getState(){}
    function observable(){}

    dispatch({ type: ActionTypes.INIT })   // 默认会发起一次

    return {
        dispatch,
        subscribe,
        replaceReducer,
        getState,
        [$$observable]: observable,
    }  
} 

reducer也就是自定义的reducer;preloadedState会作为自定义reducer的默认状态,如果自定义的reducer有传入了默认值,也会以preloadedState优先;enhancer注册插件,比如后续会讲到的redux-thunk、redux-saga;然后createStore函数最终返回一个对象,到这里清楚的知道,我们调用的dispatch、subscribe、getState其实就是返回的对象的属性值。

从上面我们可以清除的知道,createStore函数其实只做了两件事,声明函数和发起一次dispatch({ type: ActionTypes.INIT }),那么dispatch具体又做了哪些事,请往下看;

function dispatch(action) {
    // ... 省略

    try{
        isDispatching = true
        // currentState是preloadedState
        currentState = currentReducer(currentState, action)
    }finally{
        isDispatching = false
    }
    
    // nextListeners 是 subscribe 发布的回调函数,每执行dispatch就会执行
    const listeners = (currentListeners = nextListeners)
    for (let i = 0; i < listeners.length; i++) {
        const listener = listeners[i]
        listener()
    }
    
    return action
}

 currentReducer是我们传递的reducer(赋值处理,没有任何改动),这里也就解释了为什么自定义的reducer必须要有两个函数,同时,每当执行dispatch发布action的时候,也会处理subscribe发布的回调函数,并且,通过getState函数可以拿到最新的计算结果,从上面可以知道,reducer的结果最终会赋值给currentState,getState函数其实就是将currentState返回了。

我们也可以通过replaceReducer替换之前注册reducer,只需要将自定义的reducer传入replaceReducer中即可实现,replaceReducer的实现也很简单

function replaceReducer(nextReducer) {
    // ... 省略

    currentReducer = nextReducer    // reducer 替换成新的了

    dispatch({ type: ActionTypes.REPLACE })
}

于observable,还没有明确的说明,作者也只是提及了和ES提案有关:https://github.com/tc39/proposal-observable

 

 4. combineReducers && bindActionCeators使用和原理

1. combineReducers

随着应用变得越来越复杂,可以考虑将 reducer 函数 拆分成多个单独的函数,拆分后的每个函数负责独立管理 state 的一部分,这也是前面说的如果存在多个reducer情况,需要通过combineReducers合并,跟着下面的例子,深入了解下combineReducers的使用和工作原理。


combineReducers接受一个对象形参,也就是我们自定义的reducer函数,并且最终返回一个函数,返回的函数同样接受两个形参(state、action,是不是和自定义reducer一样),那么combineReducers是如何工作的呢?让我们跟着源码来分析。

function combineReducers(reducers) {
    const reducerKeys = Object.keys(reducers)
    const finalReducers = {}
    for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i]

        // ... 省略
        
        if (typeof reducers[key] === 'function') {
            finalReducers[key] = reducers[key]
        }
    }
    
    const finalReducerKeys = Object.keys(finalReducers)

    // This is used to make sure we don't warn about the same
    // keys multiple times.
    let unexpectedKeyCache

    let shapeAssertionError
    try{
        assertReducerShape(finalReducers)    // 验证reducers是否正常返回结果
    } catch (e) {
        shapeAssertionError = e
    }

    return function combination(state = {}, action) {
        // ... 省略
    }
}

 从上面可以分析得出combineReducers做了四件事,1. 收集reducer到finalReducers,2.定义了unexpectedKeyCache和shapeAssertionError,3. 执行assertReducerShape,为了验证自定义reducer是否正常返回结果,最后就是返回combination函数,也就是传入createStore其实就是combination函数,可以看到combination函数和自定义reducer很相似,都是接受一个默认状态和action,那combination又是如何工作的?

function combination(state = {}, action) {
    if (shapeAssertionError) {
        throw shapeAssertionError
    }

    // .. 省略

    let hasChanged = false
    const nextState = {}
    // 遍历执行每个reducer
    for (let i = 0; i < finalReducerKeys.length; i++) {
        const key = finalReducerKeys[i]
        const reducer = finalReducers[key]
        const previousStateForKey = state[key]
        const nextStateForKey = reducer(previousStateForKey, action)
      
        // ... 省略
        nextState[key] = nextStateForKey
        hasChanged = hasChanged || nextStateForKey !== previousStateForKey
        }
        hasChanged =
        hasChanged || finalReducerKeys.length !== Object.keys(state).length
        return hasChanged ? nextState : state
}    

其实combination就是遍历了每个reducer得到新的状态,然后保存到nextState,而previousStateForKey就是上一次状态,是createStore中的currentState根据key值得到,action就是通过dispatch发布的行为,reducer接受到然后执行state的处理,最终返回nextState覆盖currentState,以上就是combination的原理。

 

2. bindActionCreators

一般情况下开发者是可以直接在在Store实例上调用dispatch,不过当需要将action creator往下传到组件上时,并且不像让组件察觉到Redux的存在,甚至不希望直接把dispatch或者Redux Store传给它时,这时候就需要bindActionCreators出场了。可能上面描述有些复杂,结合下面的内容会更加好理解。

跟着例子来看下bindActionCreators;

根据上面的两个例子对比,在结合上面那句话,可以知道,bindActionCreators的一个作用就就是规避了我们直接在组件上使用dispatch来派发action,只需要将自定义的action和dispatch作为bindActionCreators的实参,最后返回对象,这样我们只需要在组件内调用同action同名的函数就行,也可以传入一个函数,不过该函数就只能是一个action函数,并且也只能是一个action。让我们通过源码来分析下bindActionCreators的工作流程吧

function bindActionCreator(actionCreator, dispatch) {
    return function() {
        return dispatch(actionCreator.apply(this, arguments))
    }
}

function bindActionCreators(actionCreators, dispatch) {
    if (typeof actionCreators === 'function') {
        return bindActionCreator(actionCreators, dispatch)
    }
    
    // ...省略

    // 传入对象
    const boundActionCreators = {}
    for (const key in actionCreators) {
        const actionCreator = actionCreators[key]
        if (typeof actionCreator === 'function') {
            boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)
        }
    }

    return boundActionCreators
}

根据上面的源码知道bindActionCreators不仅可以接受函数,也可以接受;并且都通过bindActionCreator处理了一层,所以 如果传入的是函数,就会当成是处理单个action,如果是对象,处理的是多个action,并将最终的结果返回,如此一来也就解释了我们开头那段话。

 

5. redux如何处理异步?

我们知道dispatch发布action后,reducer会立即计算并返回新的state,这是同步的过程,如上图所示,但如果想在 Action 发起之后, 过一段时间再执行 reducer 计算 state, 即 异步计算 state, 该如何操作呢?或许可以直接在组件中的异步函数函数中使用dispatch,如同例2一样,需要显示的在组件中注入dispatch,不过事实上是异步函数也可能是我们状态管理的一部分,更好的管理方式应该是交给redux来管理会更合适;

 

上图和首图的区别在于上图增加了中间件(middleware),首先是dispatch 发布的值先到达 middleware,middleware调用完成后再 dispatch 一个正真的 action 对象;

所以middleware是一种作为扩展redux应用的方式存在的,它可以通过包装store的dispatch来达到自己想要的目的,也即是说middleware是通过扩展store来实现自己想要的功能,它允许dispatch action 时执行额外的逻辑;暂停、修改、延迟、替换或停止 dispatch 的 action;编写可以访问 dispatch 和getState的额外代码;教dispatch 如何接受除普通 action 对象之外的其他值,例如函数和 promise,通过拦截它们并 dispatch 实际 action 对象来代替。

如果有多个middleware可以一起使用,形成middleware链表,每个middleware都不需要关心前后middleware的信息。

reducer有多种异步 middleware,每一种都允许你使用不同的语法编写逻辑。最常见的异步 middleware 是 redux-thunkredux-saga、 redux-observable

根据例3,redux使用中间件的需要先通过applyMiddleware注册,然后再将结果传给createStore,那applyMiddleware又是如何工作的?

function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

function applyMiddleware(...middlewares) {
    return createStore => (...args) => {
        const store = createStore(...args)
        let dispatch = () => {
            throw new Error(
                'Dispatching while constructing your middleware is not allowed. ' +
          'Other middleware would not be applied to this dispatch.'
          )
        }

        const middlewareAPI = {
            getState: store.getState,
            dispatch: (...args) => dispatch(...args)
        }

        const chain = middlewares.map(middleware => middleware(middlewareAPI))
        dispatch = compose(...chain)(store.dispatch)

        return {
            ...store,
            dispatch
        }
    } 
}

applyMiddleware其他就是返回了一个函数,然后接收createStore返回新函数,新函数接收自定义reducer,并返回对象;这里的dispatch是经过处理的,这也就解释了开头的那段话:middleware允许dispatch action 时执行额外的逻辑;暂停、修改、延迟......

1. 组合串联middleware

const chain = middlewares.map(middleware => middleware(middlewareAPI)) // 重写dispatch dispatch = middleware(middleware(middleware(store.dispatch))) dispatch = compose(...chain)(store.dispatch)

 applyMiddleware重点在于compose重写了dispatch,compose将多个middleware串成一个函数,上一个middleware和下一个middleware通过next建立关系,并将结果对dispatch重新赋值,每次执行dispatch虽然是从最外层的middleware开始的,但是会将action通过next传下一个middleware,最后一个middleware中才是真正触发store.dispatch,然后将上一个middleware的结果作为下一个middleware的参数,这样就依次执行了每个middleware,所以redux中间件模型也被称为洋葱模型。

6. 扩展 

Redux Toolkit 是 Redux 官方强烈推荐,开箱即用的一个高效的 Redux 开发工具集。它旨在成为标准的 Redux 逻辑开发模式。