Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【THE LAST TIME】从 Redux 源码中学习它的范式

【THE LAST TIME】从 Redux 源码中学习它的范式

作者头像
Nealyang
发布于 2020-05-05 03:30:07
发布于 2020-05-05 03:30:07
4260
举报
文章被收录于专栏:全栈前端精选全栈前端精选

THE LAST TIME

❝The last time, I have learned ❞

【THE LAST TIME】 一直是我想写的一个系列,旨在厚积薄发,重温前端。

也是给自己的查缺补漏和技术分享。

前言

「范式」概念是库恩范式理论的核心,而范式从本质上讲是一种理论体系。库恩指出:「按既定的用法,范式就是一种公认的模型或模式」

而学习 Redux,也并非它的源码有多么复杂,而是他状态管理的思想,着实值得我们学习。

讲真,标题真的是不好取,因为本文是我写的 redux 的下一篇。两篇凑到一起,才是完整的 Redux

上篇:从 Redux 设计理念到源码分析

本文续上篇,接着看 combineReducersapplyMiddlewarecompose 的设计与源码实现

至于手写,其实也是非常简单,说白了,「去掉源码中严谨的校验,就是市面上手写了」。当然,本文,我也尽量以手写演进的形式,去展开剩下几个 api 的写法介绍。

combineReducers

从上一篇中我们知道,newState 是在 dispatch 的函数中,通过 currentReducer(currentState,action)拿到的。所以 state 的最终组织的样子,完全的依赖于我们传入的 reducer。而随着应用的不断扩大,state 愈发复杂,redux 就想到了分而治之(我寄几想的词儿)。虽然最终还是一个根,但是每一个枝放到不同的文件 or func 中处理,然后再来组织合并。(模块化有么有)

combineReducers 并不是 redux 的核心,或者说这是一个辅助函数而已。但是我个人还是喜欢这个功能的。它的作用就是把一个由多个不同 reducer 函数作为 valueobject,合并成一个最终的 reducer 函数。

进化过程

比如我们现在需要管理这么一个"庞大"的 state

庞大的 state

代码语言:txt
AI代码解释
复制
let state={
    name:'Nealyang',
    baseInfo:{
        age:'25',
        gender:'man'
    },
    other:{
        github:'https://github.com/Nealyang',
        WeChatOfficialAccount:'全栈前端精选'
    }
}

因为太庞大了,写到一个 reducer 里面去维护太难了。所以我拆分成三个 reducer

代码语言:txt
AI代码解释
复制
function nameReducer(state, action) {
  switch (action.type) {
    case "UPDATE":
      return action.name;
    default:
      return state;
  }
}

function baseInfoReducer(state, action) {
  switch (action.type) {
    case "UPDATE_AGE":
      return {
        ...state,
        age: action.age,
      };
    case "UPDATE_GENDER":
      return {
        ...state,
        age: action.gender,
      };

    default:
      return state;
  }
}


function otherReducer(state,action){...}

为了他这个组成一个我们上文看到的 reducer,我们需要搞个这个函数

代码语言:txt
AI代码解释
复制
const reducer = combineReducers({
  name:nameReducer,
  baseInfo:baseInfoReducer,
  other:otherReducer
})

所以,我们现在自己写一个 combineReducers

代码语言:txt
AI代码解释
复制
function combineReducers(reducers){
    const reducerKeys = Object.keys(reducers);

    return function (state={},action){
        const nextState = {};

        for(let i = 0,keyLen = reducerKeys.length;i<keyLen;i++){
            // 拿出 reducers 的 key,也就是 name、baseInfo、other
            const key = reducerKeys[i];
            // 拿出如上的对应的 reducer:nameReducer、baseInfoReducer、otherReducer
            const reducer = reducers[key];
            // 去除需要传递给对应 reducer 的初始 state
            const preStateKey = state[key];
            // 拿到对应 reducer 处理后的 state
            const nextStateKey = reducer(preStateKey,action);
            // 赋值给新 state 的对应的 key 下面
            nextState[key] = nextStateKey;
        }
        return nextState;
    }
}

基本如上,我们就完事了。

关于 reducer 更多的组合、拆分、使用的,可以参照我 github 开源的前后端博客的 Demo:React-Express-Blog-Demo

源码

代码语言:txt
AI代码解释
复制
export type Reducer<S = any, A extends Action = AnyAction> = (
  state: S | undefined,
  action: A
) => S

export type ReducersMapObject<S = any, A extends Action = Action> = {
  [K in keyof S]: Reducer<S[K], A>
}

定义了一个需要传递给 combineReducers 函数的参数类型。也就是我们上面的

代码语言:txt
AI代码解释
复制
{
  name:nameReducer,
  baseInfo:baseInfoReducer,
  other:otherReducer
}

其实就是变了一个 statekey,然后 key 对应的值是这个 Reducer,这个 Reducerstate 是前面取出这个 keystate 下的值。

代码语言:txt
AI代码解释
复制
export default function combineReducers(reducers: ReducersMapObject) {
  //获取所有的 key,也就是未来 state 的 key,同时也是此时 reducer 对应的 key
  const reducerKeys = Object.keys(reducers)
  // 过滤一遍 reducers 对应的 reducer 确保 kv 格式么有什么毛病
  const finalReducers: ReducersMapObject = {}
  for (let i = 0; i < reducerKeys.length; i++) {
    const key = reducerKeys[i]
    
    if (process.env.NODE_ENV !== 'production') {
      if (typeof reducers[key] === 'undefined') {
        warning(`No reducer provided for key "${key}"`)
      }
    }

    if (typeof reducers[key] === 'function') {
      finalReducers[key] = reducers[key]
    }
  }
  // 再次拿到确切的 keyArray
  const finalReducerKeys = Object.keys(finalReducers)

  // This is used to make sure we don't warn about the same
  // keys multiple times.
  let unexpectedKeyCache: { [key: string]: true }
  if (process.env.NODE_ENV !== 'production') {
    unexpectedKeyCache = {}
  }

  let shapeAssertionError: Error
  try {
    // 校验自定义的 reducer 一些基本的写法
    assertReducerShape(finalReducers)
  } catch (e) {
    shapeAssertionError = e
  }
  // 重点是这个函数
  return function combination(
    state: StateFromReducersMapObject<typeof reducers> = {},
    action: AnyAction
  ) {
    if (shapeAssertionError) {
      throw shapeAssertionError
    }

    if (process.env.NODE_ENV !== 'production') {
      const warningMessage = getUnexpectedStateShapeWarningMessage(
        state,
        finalReducers,
        action,
        unexpectedKeyCache
      )
      if (warningMessage) {
        warning(warningMessage)
      }
    }
    
    let hasChanged = false
    const nextState: StateFromReducersMapObject<typeof reducers> = {}
    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)
      // 上面的部分都是我们之前手写内容,nextStateForKey 是返回的一个newState,判断不能为 undefined
      if (typeof nextStateForKey === 'undefined') {
        const errorMessage = getUndefinedStateErrorMessage(key, action)
        throw new Error(errorMessage)
      }
      nextState[key] = nextStateForKey
      // 判断是否改变,这里其实我还是很疑惑
      // 理论上,reducer 后的 newState 无论怎么样,都不会等于 preState 的
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey
    }
    hasChanged =
      hasChanged || finalReducerKeys.length !== Object.keys(state).length
    return hasChanged ? nextState : state
  }
}

combineReducers 代码其实非常简单,核心代码也就是我们上面缩写的那样。但是我是真的喜欢这个功能。

applyMiddleware

applyMiddleware 这个方法,其实不得不说,redux 中的 Middleware中间件的概念不是 redux 独有的。ExpressKoa等框架,也都有这个概念。只是为解决不同的问题而存在罢了。

「**Redux** Middleware 说白了就是对 dispatch 的扩展,或者说重写,增强 dispatch 的功能!」 一般我们常用的可以记录日志、错误采集、异步调用等。

其实关于ReduxMiddleware, 我觉得中文文档说的就已经非常棒了,这里我简单介绍下。感兴趣的可以查看详细的介绍:Redux 中文文档

Middleware 演化过程

记录日志的功能增强
  • 需求:在每次修改 state 的时候,记录下来 修改前的 state ,为什么修改了,以及修改后的 state
  • Action:每次修改都是 dispatch 发起的,所以这里我只要在 dispatch 加一层处理就一劳永逸了。
代码语言:txt
AI代码解释
复制
const store = createStore(reducer);
const next = store.dispatch;

/*重写了store.dispatch*/
store.dispatch = (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

如上,在我们每一次修改 dispatch 的时候都可以记录下来日志。因为我们是重写了 dispatch 不是。

增加个错误监控的增强
代码语言:txt
AI代码解释
复制
const store = createStore(reducer);
const next = store.dispatch;

store.dispatch = (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

所以如上,我们也完成了这个需求。

但是,回头看看,这两个需求如何才能够同时实现,并且能够很好地解耦呢?

想一想,既然我们是「增强 dispatch」。那么是不是我们可以将 dispatch 作为形参传入到我们增强函数。

多文件增强
代码语言:txt
AI代码解释
复制
const exceptionMiddleware = (next) => (action) => {
  try {
    /*loggerMiddleware(action);*/
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  } 
}
/*loggerMiddleware 变成参数传进去*/
store.dispatch = exceptionMiddleware(loggerMiddleware);
代码语言:txt
AI代码解释
复制
// 这里额 next 就是最纯的 store.dispatch 了
const loggerMiddleware = (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

所以最终使用的时候就如下了

代码语言:txt
AI代码解释
复制
const store = createStore(reducer);
const next = store.dispatch;

const loggerMiddleware = (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

store.dispatch = exceptionMiddleware(loggerMiddleware(next));

但是如上的代码,我们又不能将 Middleware 独立到文件里面去,因为依赖外部的 store。所以我们再把 store 传入进去!

代码语言:txt
AI代码解释
复制
const store = createStore(reducer);
const next  = store.dispatch;

const loggerMiddleware = (store) => (next) => (action) => {
  console.log('this state', store.getState());
  console.log('action', action);
  next(action);
  console.log('next state', store.getState());
}

const exceptionMiddleware = (store) => (next) => (action) => {
  try {
    next(action);
  } catch (err) {
    console.error('错误报告: ', err)
  }
}

const logger = loggerMiddleware(store);
const exception = exceptionMiddleware(store);
store.dispatch = exception(logger(next));

以上其实就是我们写的一个 Middleware,理论上,这么写已经可以满足了。但是!是不是有点不美观呢?且阅读起来非常的不直观呢?

如果我需要在增加个中间件,调用就成为了

代码语言:txt
AI代码解释
复制
store.dispatch = exception(time(logger(action(xxxMid(next)))))

「这也就是 applyMiddleware 的作用所在了」

我们只需要知道有多少个中间件,然后在内部顺序调用就可以了不是

代码语言:txt
AI代码解释
复制
const newCreateStore = applyMiddleware(exceptionMiddleware, timeMiddleware, loggerMiddleware)(createStore);
const store = newCreateStore(reducer)
手写 applyMiddleware
代码语言:txt
AI代码解释
复制
const applyMiddleware = function (...middlewares) {
  // 重写createStore 方法,其实就是返回一个带有增强版(应用了 Middleware )的 dispatch 的 store
  return function rewriteCreateStoreFunc(oldCreateStore) {
  // 返回一个 createStore 供外部调用
    return function newCreateStore(reducer, initState) {
      // 把原版的 store 先取出来
      const store = oldCreateStore(reducer, initState);
      // const chain = [exception, time, logger] 注意这里已经传给 Middleware store 了,有了第一次调用
      const chain = middlewares.map(middleware => middleware(store));
      // 取出原先的 dispatch
      let dispatch = store.dispatch;
      // 中间件调用时←,但是数组是→。所以 reverse。然后在传入 dispatch 进行第二次调用。最后一个就是 dispatch func 了(回忆 Middleware 是不是三个括号~~~)
      chain.reverse().map(middleware => {
        dispatch = middleware(dispatch);
      });
      store.dispatch = dispatch;
      return store;
    }
  }
}

❝解释全在代码上了 ❞

其实源码里面也是这么个逻辑,但是源码实现更加的优雅。他利用了函数式编程的compose 方法。在看 applyMiddleware 的源码之前呢,先介绍下 compose 的方法吧。

compose

其实 compose 函数做的事就是把 var a = fn1(fn2(fn3(fn4(x)))) 这种嵌套的调用方式改成 var a = compose(fn1,fn2,fn3,fn4)(x) 的方式调用。

compose的运行结果是一个函数,调用这个函数所传递的参数将会作为compose最后一个参数的参数,从而像'洋葱圈'似的,由内向外,逐步调用。

代码语言:txt
AI代码解释
复制
export default function compose(...funcs: Function[]) {
  if (funcs.length === 0) {
    // infer the argument type so it is usable in inference down the line
    return <T>(arg: T) => arg
  }

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

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

哦豁!有点蒙有么有~ 函数式编程就是烧脑?且直接。所以爱的人非常爱。

compose是函数式编程中常用的一种组合函数的方式。

方法很简单,传入的形参是 func[],如果只有一个,那么直接返回调用结果。如果是多个,则funcs.reduce((a, b) => (...args: any) => a(b(...args))).

我们直接啃最后一行吧

代码语言:txt
AI代码解释
复制
import {componse} from 'redux'
function add1(str) {
 return 1 + str;
}
function add2(str) {
 return 2 + str;
}
function add3(a, b) {
 return a + b;
}
let str = compose(add1,add2,add3)('x','y')
console.log(str)
//输出结果 '12xy'

输出

dispatch = compose<typeof dispatch>(...chain)(store.dispatch) applyMiddleware 的源码最后一行是这个。其实即使我们上面手写的 reverse 部分。

reduce 是 es5 的数组方法了,对累加器和数组中的每个元素(从左到右)应用一个函数,将其减少为单个值。函数签名为:arr.reduce(callback[, initialValue])

所以如若我们这么看:

代码语言:txt
AI代码解释
复制
[func1,func2,func3].reduce(function(a,b){
  return function(...args){
    return a(b(...args))
  }
})

所以其实就非常好理解了,每一次 reduce 的时候,callbacka,就是一个a(b(...args))function,当然,第一次是 afunc1。后面就是无限的叠罗汉了。最终拿到的是一个 func1(func2(func3(...args)))function

总结

所以回头看看,redux 其实就这么些东西,第一篇算是 redux 的核心,关于状态管理的思想和方式。第二篇可以理解为 redux 的自带的一些小生态。全部的代码不过两三百行。但是这种状态管理的范式,还是非常指的我们再去思考、借鉴和学习的。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-04-29,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全栈前端精选 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Redux系列x:源码分析
写在前面 redux的源码很简洁,除了applyMiddleware比较绕难以理解外,大部分还是 这里假设读者对redux有一定了解,就不科普redux的概念和API啥的啦,这部分建议直接看官方文档。
IMWeb前端团队
2017/12/29
1.3K0
深入学习和理解 Redux
Redux官网上是这样描述Redux,Redux is a predictable state container for JavaScript apps.(Redux是JavaScript状态容器,提供可预测性的状态管理)。 目前Redux GitHub有5w多star,足以说明 Redux 受欢迎的程度。
2020labs小助手
2020/03/05
9160
Redux源码分析
顾名思义就是将多个reducer合成一个reducer。传入的reducer是一个对象,对象的键值是reducer的名称。
IMWeb前端团队
2019/12/03
3590
Redux源码分析
redux源码解析
applyMiddleware.js import compose from './compose' /** * Creates a store enhancer that applies middleware to the dispatch method * of the Redux store. This is handy for a variety of tasks, such as expressing * asynchronous actions in a concise manner,
theanarkh
2019/03/06
1.2K0
【React】360- 完全理解 redux(从零实现一个 redux)
记得开始接触 react 技术栈的时候,最难理解的地方就是 redux。全是新名词:reducer、store、dispatch、middleware 等等,我就理解 state 一个名词。
pingan8787
2019/09/25
7880
【React】360- 完全理解 redux(从零实现一个 redux)
Redux(五):源码分析之combineReducers
定义getUndefinedStateErrorMessage()函数,用以返回错误信息。
Ashen
2020/06/01
1K0
深入理解Redux之中间件(middleware)
redux深入理解之中间件(middleware) 理解reduce函数 reduce() 方法接收一个函数作为累加器(accumulator),数组中的每个值(从左到右)开始缩减,最终为一个值。 arr.reduce([callback, initialValue]) 关于reduce的用法,这里不再做多述,可以去这里查看 看如下例子: let arr = [1, 2, 3, 4, 5]; // 10代表初始值,p代表每一次的累加值,在第一次为10 // 如果不存在初始值,那么p第一次值为1 //
糊糊糊糊糊了
2018/05/09
9080
造一个 redux 轮子
Redux 应该是很多前端新手的噩梦。还记得我刚接触 Redux 的时候也是刚从 Vue 转过来的时候,觉得Redux 概念非常多,想写一个 Hello World 都难。
写代码的海怪
2022/03/29
1.6K0
造一个 redux 轮子
学习 redux 源码整体架构,深入理解 redux 及其中间件原理
感兴趣的读者可以点击阅读。 其他源码计划中的有:express、vue-rotuer、redux、 react-redux 等源码,不知何时能写完(哭泣),欢迎持续关注我(若川)。
若川
2020/06/22
1.5K0
React面试之生命周期与状态管理
在 V16 版本中引入了 Fiber 机制。这个机制一定程度上的影响了部分生命周期的调用,并且也引入了新的 2 个 API 来解决问题。
xiangzhihong
2022/11/30
3380
redux原理是什么
相信很多人都在使用redux作为前端状态管理库进去项目开发,但仍然停留在“知道怎么用,但仍然不知道其核心原理”的阶段,接下来带大家分析一下redux和react-redux两个库的核心思想和API
xiaofeng123aa
2022/09/28
7090
Redux 原理与实现
Redux 和 React 之间并没有什么关系,脱离了 React,Redux 也可以与其它的 js 库(甚至是原生 js)搭配使用,Redux 只是一个状态管理库,但它与 React 搭配时却很好用,使开发 React 应用更加简介。而使用 Redux 库时,需要先做“配置”,因为这些代码的书写是必不可少的。下面的图是 redux 的工作流:
多云转晴
2020/02/26
4.6K0
手写一个Redux,深入理解其原理-面试进阶
Redux可是一个大名鼎鼎的库,很多地方都在用,我也用了几年了,今天这篇文章就是自己来实现一个Redux,以便于深入理解他的原理。我们还是老套路,从基本的用法入手,然后自己实现一个Redux来替代源码的NPM包,但是功能保持不变。本文只会实现Redux的核心库,跟其他库的配合使用,比如React-Redux准备后面单独写一篇文章来讲。有时候我们过于关注使用,只记住了各种使用方式,反而忽略了他们的核心原理,但是如果我们想真正的提高技术,最好还是一个一个搞清楚,比如Redux和React-Redux看起来很像,但是他们的核心理念和关注点是不同的,Redux其实只是一个单纯状态管理库,没有任何界面相关的东西,React-Redux关注的是怎么将Redux跟React结合起来,用到了一些React的API。
beifeng1996
2022/10/10
5340
Redux介绍及源码解析
这篇文章来总结一下 Redux, 便于以后的知识回顾. 有了之前 Flux 知识学习, 应该对单向数据流的状态管理有比较清晰的认识了, 同样 Redux 的出现也是受到了 Flux 的启发, 这也是我们最好要先去了解一下 Flux 的原因. 同时 Redux 利用纯函数简单明了的特点, 在 Flux 架构的基础上进行了优化和功能增强 (支持中间件、异步等), 降低了复杂度, 同时还提供强大的工具库支持 (React-Redux、Redux-Toolkit、Redux-Thunk). 下面一起来看下其具体的实现逻辑. 详细内容可以直接在官网学习.
BLUSE
2022/11/27
2.6K1
从应用到源码-深入浅出Redux
文章中的每一行代码都是笔者深思熟虑敲下的,欢迎对 Redux 感兴趣的同学共同讨论。
19组清风
2022/08/30
1.4K0
从应用到源码-深入浅出Redux
你想要的——redux源码分析
备注:例子中结合的是react进行使用,当然redux不仅仅能结合react,还能结合市面上其他大多数的框架,这也是它比较流弊的地方
can4hou6joeng4
2023/11/30
2460
Redux源码解析系列(二) -- middleware 和 applyMiddleware
本文作者:IMWeb 黄qiong 原文出处:IMWeb社区 未经同意,禁止转载 在分析源码applyMiddleware 之前,让我们先看看middleware是个啥 Redux里我们都知
IMWeb前端团队
2018/01/08
8300
Redux源码解析系列(二) -- middleware 和 applyMiddleware
阅读redux源码
提供了和双向绑定思想不同的单向数据流,应用状态可以预测,可以回溯,易于调试。使用redux之初的人可能会很不适应,改变一个状态,至少写三个方法,从这点上不如写其他框架代码易于理解,但是自从配合使用redux-logger一类的logger插件,就感觉到了redux的优势。状态改变很清晰,很容易了解发生了什么。
frontoldman
2019/09/02
8780
Flux --> Redux --> Redux React 基础实例教程
Flux思想、Redux基本概念、Redux的使用、Redux在React中的使用(同步)、Redux在React中的使用(异步,使用中间件)
书童小二
2018/09/03
4K0
Flux --> Redux --> Redux React 基础实例教程
【干货】从零实现 react-redux
在 React 诞生之初,Facebook 宣传这是一个用于前端开发的界面库,仅仅是一个 View 层。前面我们也介绍过 React 的组件通信,在大型应用中,处理好 React 组件通信和状态管理就显得非常重要。为了解决这一问题,Facebook 最先提出了单向数据流的 Flux 架构,弥补了使用 React 开发大型网站的不足。
winty
2020/03/31
1.8K0
【干货】从零实现 react-redux
相关推荐
Redux系列x:源码分析
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档