Redux的使用与源码解析

前言

相信学过 React 的朋友对 Redux 一定不陌生,作为 JavaScript 的状态管理容器,它不仅能够在 react 中使用,同时也能在原生 JS 项目或者Vue项目中使用它。不过因为其设计理念与 react 相似,并且和 react 一起使用时会有较好的开发体验,以至于很多人一提到 redux 就会联想到 react

redux源码

redux 的使用

  1. 创建 Action
js 复制代码
// === redux/constant.js ===
// 该模块适用于定义action对象中type类型的常量值
// 目的只有一个:便于管理的同时防止单词写错
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'

// === redux/action.js ===
// action 定义成函数,方便 dispatch 的使用,使用时直接 dispatch(addTodo(值))
// 如果定义为 const addTodo = 'ADD_TODO',则 dispatch({type: addTodo, count: 值})
import { INCREMENT, DECREMENT } from './constant'
// 同步action,就是指action的值为Object类型的一般对象
export const createIncrementAction = (data) => ({ type: INCREMENT, data })
export const createDecrementAction = (data) => ({ type: DECREMENT, data })
// 异步action,就是指action的值为函数,异步action中一般都会调用同步action
export const createDecrementAsyncAction = (data, delay) => {
  return (dispatch) => {
    setTimeout(() => {
      // 通知redux
      dispatch(createIncrementAction(data))
    }, delay)
  }
}
  1. 创建 Reducer
js 复制代码
// === redux/reducer.js ===
/**
  state: 初始值
  action: dispatch传过来的对象
  1. 该文件是用于创建一个为Count组件服务的reducer,reducer的本质就是一个函数
  2. reducer 函数会接收到两个参数,分别为:之前状态(preState), 动作对象(action)
*/
import { INCREMENT, DECREMENT } from './constant'
const initState = 0
export default function countReducer (state = initState, action) {
  // 从action对象中获取:type,data
  const { type, data } = action
  switch (type) {
    case INCREMENT:
      return state + data
    case DECREMENT:
      return state - data
    default:
      return state
  }
}
  1. 创建 Store 用来连接 actionreducer
js 复制代码
// === redux/store.js ===
import { createStore, applyMiddleware, combineReducers } from 'redux'
// 引入reducer
import reducer from './reducer'
// 引入 redux-thunk 用于支持异步action
import thunk from 'redux-thunk'

// 创建store对象并暴露
// 如果有多个reducer,可以使用 combineReducers 整合 
// 例:const rootReducer = combineReducers({reducer1: xxx, reducer2: xxx})
export default createStore(reducer, applyMiddleware(thunk))
  1. 配合 react-reduxreact 中使用

注:redux 中提供了 subscribe 方法订阅 store 里的 state 变化,如果在 react 工程中不想使用 react-redux,则可在入口文件 index.js 中使用 subscribe 方法来更新视图

js 复制代码
// state 发生改变会自动调用回调函数,订阅
store.subscribe(() => {
  // 渲染App到页面
  ReactDOM.render(
    <BrowserRouter>
      <App />
    </BrowserRouter>, document.getElementById('root'))
})

react-redux 中提供了 connect 方法,用来连接视图(react组件)redux,使得 redux 中的 state 发生改变,能够及时通知视图的更新,由于我们平时大多数写的是函数式组件,则可以依赖 react-redux 中提供的 useSelectoruseDispatch 两个api来操作redux

和使用 connect() 一样,你首先应该将整个应用包裹在 <Provider> 中,使得 store 暴露在整个组件树中。

js 复制代码
import store from './redux/store'
ReactDOM.render(
    <Provider store={store}>
        <App />
    </Provider>, 
    document.getElementById('root')
)

在组件中使用

js 复制代码
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { 
    createIncrementAction, 
    createDecrementAction, 
    createDecrementAsyncAction 
} from '../redux/action'

function Counter() {
    const dispatch = useDispatch()
    // 如果有多个reducer,则回调函数中 state 就是根,根据对应的作用域去找对应的属性
    // 例如:combineReducers({a: xxx, b: xxx}) => useSelector(state => state.a)
    const state = useSelector(state => state)
    return <>
        <div>当前值:{state}</div>
        <button onClick={() => {
            dispatch(createIncrementAction(1))
        }}>加1</button>
        <button onClick={() => {
            dispatch(createDecrementAction(2))
        }}>减2</button>
        <button onClick={() => {
            dispatch(createDecrementAction(10))
        }}>异步加10</button>
    </>
}

Redux 源码解析

当你熟练使用 redux 以后,在没有看其源码之前,你是否会有以下一些疑问:

  1. store 是如何将 actionreducer 串联在一起?为什么我们 dispatch 一个action,它就会自动帮我们找到对应 reducer 中 switch 语句里的 case
  2. 多个 reducer 是如何整合在一起的,它生成了一颗什么样的状态树
  3. redux 的中间件是怎么运作的?
  4. 为什么配合 react-redux 使用,state的改变会引起视图的改变?

带着疑问,我们从 redux 的核心源码 createStore 讲起

createStore 做了什么?

creaeStore 是 reudx 使用的入口函数 , 源码位置:src/createStore.ts ,我们把 ts 类型标注去掉,稍作改造,实现一个简单的 createStore 并做讲解

js 复制代码
function createStore(reducer, preloadedState, enhance) {
  // reducer 必须是一个函数,有两参数 state 和 action
  let currentReducer = reducer
  // 初始状态值, currentState 就是状态树
  let currentState = preloadedState
  let currentListeners = []; // 当前订阅函数 => 源码使用 Map 构造
  // 判断中间件
  if (enhance !== undefined && typeof enhance === 'function') {
    return enhance(createStore)(reducer, preloadedState)
  }
  // dispatch 方法的实现,默认action是个对象
  function dispatch(action) {
    currentState = currentReducer(currentState, action)
    // state改变,执行订阅的函数
    currentListeners.forEach((listener) => listener());
    return action
  }

  function getState() {
    return currentState
  }

  // 订阅和取消订阅必须要成对出现
  function subscribe(listener) {
    currentListeners.push(listener);
    return () => {
      const index = currentListeners.indexOf(listener);
      currentListeners.splice(index, 1);
    };
  }

  dispatch({ type: 'init' }) // 源码这里的type是字符串拼接随机数,保证不命中我们自己写的type

  return {
    dispatch,
    getState,
    subscribe
  }
}

在调用 createStore 函数时,我们可以看到,函数内部执行了一次 dispatch,并返回一个对象。此时调用dispatch函数传入的参数是{type: 'init'} 我们以上面 redux 的使用 的代码为例

js 复制代码
  function dispatch(action) {
    // currentState 的初始值是 preloadedState, action = {type: 'init'}
    currentState = currentReducer(currentState, action)
    // state改变,执行订阅的函数
    currentListeners.forEach((listener) => listener());
    // return currentState
  }
  
  // 此时的currentReducer就是, action = {type: 'init'}
  function countReducer (state = initState, action) {
      const { type, data } = action 
      switch (type) { 
          case INCREMENT: 
          return state + data 
      case DECREMENT: 
          return state - data 
      default: 
          return state 
      } 
  }

由于 ation.type 未命中,currentReducer返回的就是 countReducer中initState的值,即整个应用的状态就是 currentState = 0 这时候,如果我们手动 dispatch一个action的话,他就会执行对应的 reducer 并把新的值赋值给 currentStae,此时 redux 的状态就发生了改变,然后依次执行订阅列表中的函数通知给组件 。那么,当我们有多个 reducer 的时候,它又是怎么工作的呢?

combineReducers 做了什么?

combineReducers 的作用是将多个reducer(函数)整合成一个reducer(函数)

js 复制代码
function combineReducers(reducers) {
  // reducers 是一个对象,{reducer1: xxx, reducer2: xxx}
  const reducerKeys = Object.keys(reducers)
  return function combination(state = {}, action) {
    let nextState = {}
    for (let i = 0; i < reducerKeys.length; i++) {
      const _key = reducerKeys[i]
      const reducer = reducers[_key]
      var previousStateForKey = state[_key];
      const nextStateForKey = reducer(previousStateForKey, action)
      nextState[_key] = nextStateForKey
    }
    return nextState
  }
}
export default combineReducers

不难看出,当我们有多个 reducer 的时候,combineReducers 会返回一个函数,因为 createStore 的第一个参数必须是函数。 假设我们现在有 userarticle 两个 reducer

js 复制代码
function user(state, action) { ... }

function article(state, action) { ... }
// combineReducers 返回一个combination函数, rootReducer就是根reducer函数
const rootReducer = combineReducers({user: user, article}) 
export default createStore(rootReducer)

当createStore内部初始化第一次 dispatch 的时候

js 复制代码
function dispatch(action) {
    currentState = currentReducer(currentState, action)
}
// 此时的 currentReducer ,也就是 rootReducer 等价于
function combination(state = {}, action) {
    // action = {type: 'init'}, state = preloadedState
    let nextState = {}
    // reducerKeys = ['user', 'article']
    for (let i = 0; i < reducerKeys.length; i++) {
      const _key = reducerKeys[i]
      // reducers 闭包作用,值为 {user, article}
      const reducer = reducers[_key]
      var previousStateForKey = state[_key];
      // 调用 user or aritcle 的reducer
      const nextStateForKey = reducer(previousStateForKey, action)
      nextState[_key] = nextStateForKey
    }
    return nextState
  }

由上可见,存在多个 reducer 的时候,createStore 自身内部初始化调用 dispatch 后,会依次调用每一个子 reducer,并返回一棵状态树({user: 值, article: 值}),当我们手动调用一次dispatch 的时候,同样也会循环执行每一个 reducer,去命中 action.type, 并把新的状态树返回给 currentState,也就是说,如果你传入的 action.type 的值同时命中多个 reducer 里switch的case,则它们的状态将都发生变化。

redux 中间件的原理是什么?

中间件的使用

js 复制代码
const store = createStore(rootReducer, applyMiddleware(xxx1, xxx2)) 
//  等价于
const store = applyMiddleware(xx1,xx2)(createStore)(rootReducer)

redux 中间件的出现意在增强 action 的能力,如果有多个中间件,用,隔开,并从依次执行

js 复制代码
// 柯里化
function applyMiddleware(...middlewares) {
  return function (createStore) {
    return function (reducer, preloadedState) {
      const store = createStore(reducer, preloadedState)
      let _dispatch = () => {
        console.log('内部dispatch, 啥也不干')
      };
      const midapi = {
        getState: store.getState,
        dispatch: (action) => {
          console.log('_dispatch', _dispatch)
          _dispatch(action)
        }
      }
      const chain = middlewares.map(middleware => middleware(midapi))

      _dispatch = compose(...chain)(store.dispatch)
      console.log('=====_dispatch=====增强后的', _dispatch)
      return { ...store, dispatch: _dispatch }
    }
  }
}

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)));
}
export default applyMiddleware

上面代码第一次看的时候是不是会有点懵?为什么这么多方法嵌套?还记得我们在创建createStore里有这么一行代码

js 复制代码
function createStore(reducer, preloadedState, enhance) {
  // ...
  if (enhance !== undefined && typeof enhance === 'function') {
    // enhance => applyMiddleware(xxx1,xx2)
    return enhance(createStore)(reducer, preloadedState)
  }
  // ...
}

当我们使用中间件的时候,createStore 会将自身传入给applyMiddleware,最终返回一个函数, 这时就可以认为原本的 createStore 发生了变化,只是在原本的基础上又包装了一层,换个形式而已

js 复制代码
// 包装后的createStore
function creaeStore(reducer, preloadedState) {
      // _createStore 是之前的引用,这里就不在复述
      const store = _createStore(reducer, preloadedState)
      let _dispatch = () => {
        console.log('内部dispatch, 啥也不干')
      };
      const midapi = {
        getState: store.getState,
        dispatch: (action) => {
          // 增强后的dispatch,刚开始是啥也不干,
          _dispatch(action)
        }
      }
      const chain = middlewares.map(middleware => middleware(midapi))
      // 一顿操作后,这里把给_dispatch重新赋值,即增强后的dispath,
      // 利用引用类型关系,midapi里的dispatch函数里的_dispatch也会随之改变
      _dispatch = compose(...chain)(store.dispatch)
      console.log('=====_dispatch=====增强后的', _dispatch)
      return { ...store, dispatch: _dispatch }
    }

写一个简单的中间件

上面的代码或许你看的有点不明白,我们写一个简单的中间件来结合讲解

js 复制代码
function createThunkMiddleware() {
  return function middleware({ dispatch, getState }) {
    return (next) => (action) => {
      /**
       * dispatch 增强后的dispatch
       * next 上一个中间件返回的dispatch, 源头是store.dispatch 
       * 
       */
      if (typeof action === 'function') {
        return action(dispatch, getState)
      }
      return next(action)
    }
  }
}
const thunk = createThunkMiddleware()
export default thunk

const chain = middlewares.map(middleware => middleware(midapi))会依次调用中间件(函数)返回一个个 middleware 函数,此时 chain 的值为

js 复制代码
chain = [
     (action) => { 
      if (typeof action === 'function') {
        // 闭包效果,midapi 中增强后的dispatch,thunk中间件提供我们手动dispatch的时候可以是一个函数,即这个action是一个函数
        return action(dispatch, getState, extraArgument)
      }
      // next 上一个中间件返回的dispatch
      return next(action)
    }
]
}

至于为何中间件是从执行的,那就归功于 compose 这个函数了

js 复制代码
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)));
}

// 看不懂?解析一下
f1 = () => {}
f2 => () => {}
f3 => () => {}
f = null
function compose(funcs = [f1, f2, f3]) {
    funcs.reduce((a, b) => {
            return (...args) => a(b(...args))
            // 第一次循环 a = f1, b = f2 , return (...args) => f1(f2(...args))
            
            // 第二次循环 a = (...args) => f1(f2(...args)) 上一次return的, b = f3
            // 则 return (...args) => a(b(...args)) 就等价于 
            //   a = function(...args) {
            //       return f1(f2(...args))
            //   }
            //   调用函数a(b),把b作为参数传入args就是函数b,此时是f3
            //    也就是说
            //   a(b(...args)) => f1(f2(b(...arg))) => f1(f2((f3(...args))))
            
            // ... 以此类推
           
        }
    )
}

compose 最终返回一个 (args) => {...}的函数,再次调用传入原始 store.dispatch,则得到经过每一个中间件依次增强后的 dispatch

总结

redux 的源码还是相对比较简单的,以上只是写了一些核心代码块,官方源码仅仅了了几百行,至于 react-redux 的源码也通俗易懂,这里暂不赘述。

相关推荐
问道飞鱼3 分钟前
【前端知识】强大的js动画组件anime.js
开发语言·前端·javascript·anime.js
k09334 分钟前
vue中proxy代理配置(测试一)
前端·javascript·vue.js
傻小胖6 分钟前
React 脚手架使用指南
前端·react.js·前端框架
程序员海军18 分钟前
2024 Nuxt3 年度生态总结
前端·nuxt.js
m0_7482567828 分钟前
SpringBoot 依赖之Spring Web
前端·spring boot·spring
web135085886351 小时前
前端node.js
前端·node.js·vim
m0_512744641 小时前
极客大挑战2024-web-wp(详细)
android·前端
若川1 小时前
Taro 源码揭秘:10. Taro 到底是怎样转换成小程序文件的?
前端·javascript·react.js
潜意识起点1 小时前
精通 CSS 阴影效果:从基础到高级应用
前端·css
奋斗吧程序媛1 小时前
删除VSCode上 origin/分支名,但GitLab上实际上不存在的分支
前端·vscode