前言

近日,出于对 Redux 的好奇和想对自己用过的东西知根知底之目的,做了一个 Redux 的自我检测,以便彻底了解其精髓之处。下面我会对使用 Redux 之后产生的疑问做一个清单,用问题导向往下深挖。

问题清单

  1. state 初始化如何让全局都能访问到?
  2. dispatch 之后,Redux 是如何去处理的?
  3. reducer 是如何处理的?
  4. state 中的数据被修改之后,订阅者们如何去收到更新后的数据?
  5. React 发送了 dispatch 之后,如何感知 state 的改变?
  6. applyMiddleware 中为什么一个临时变量 dispatch 被赋值了 2 次?
  7. applyMiddleware 中 middlewareAPI 的 dispatch 为什么要用匿名函数包裹?

Redux 源码目录简介

Redux 暴露出来的五个接口:

  • createStore (会创建一个 store 及其相应的 dispatch 和 subscribe 操作)
  • combineReducers (合并多个 reducer 为一个总的 reducer)
  • bindActionCreators (返回包裹 dispatch 的函数可以直接使用。 一般用在 mapDispatchToProps 里)
  • applyMiddleware (提供中间件,如 redux-thunk、redux-logger)
  • compose (combineReducers 中会用到的工具函数)

我们将通过上述接口来一一解答我们提出的问题。ps:以下源码均是简化之后的代码

dispatch 之后,Redux 是如何去处理的?
state 中的数据被修改之后,页面如何去收到更新后的数据?

这些答案在 createStore.js 中,先来看看代码结构:

createStore(reducer, preloadedState, enhancer) {// 转换参数if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {enhancer = preloadedStatepreloadedState = undefined}let currentReducer = reducer // 当前的reducer,支持通过 store.replaceReducer 方式动态替换 reducer,为代码热替换提供可能。(之后会写一篇 react-redux 的文章来解读)let currentState = preloadedState // 当前的statelet currentListeners = [] // 存储更新函数的数组let nextListeners = currentListeners // 下次dispatch将会触发的更新函数数组let isDispatching = false //类似一把锁,如果正在dispatch action,那么就做一些限制function getState() {// 返回当前的state, 可以调用store.getState()获取到store中的数据,...}function subscribe(listener) {// 订阅一个更新函数(listener),实际上的订阅操作就是把listener放入一个listeners数组// 该方法会返回一个 unSubscribe() 函数,以便从数组中删除该监听函数。// 但是注意,这两个操作都是在dispatch不执行时候进行的。因为dispatch执行时候会循环执行更新函数,要保证listeners数组在这时候不能被改变...}function dispatch(action) {// 接收action,调用reducer根据action和当前的state,返回一个新的state// 循环调用listeners数组,执行更新函数,函数内部会通过store.getState()获取state,此时的state为最新的state,完成页面的更新...}return {dispatch,subscribe,getState,}}

我们知道页面可以通过 store.getState() 去获取当前的最新状态,而页面如果修改了数据会发送一个 dispatch(type) ,而这个操作便是问题的关键:

function dispatch(action) {if (!isPlainObject(action)) {throw new Error('Actions must be plain objects. ' +'Use custom middleware for async actions.')}if (typeof action.type === 'undefined') {throw new Error('Actions may not have an undefined "type" property. ' +'Have you misspelled a constant?')}// 正在dispatch的话不能再次dispatch,也就是说不可以同时dispatch两个actionif (isDispatching) {throw new Error('Reducers may not dispatch actions.')}try {isDispatching = true// 获取到当前的statecurrentState = currentReducer(currentState, action)} finally {isDispatching = false}
// 此处的会将 subscribe() 里订阅的监听依次跑一遍,数组里面都是需要获取数据的函数。const listeners = (currentListeners = nextListeners)// 循环执行当前的linstenerfor (let i = 0; i < listeners.length; i++) {const listener = listeners[i]// 此处并没有将最新状态作为参数传递的原因是因为在监听器中,我们会直接调用 store.getState() 方法去拿到最新的状态,此处只是起到通知作用。这也是数据被更改之后,页面收到通知而去更新数据的方式。listener()}return action}

解答以上问题:

Q: dispatch 之后,Redux 是如何去处理的?state 中的数据被修改之后,页面如何去收到更新后的数据?

A: 首先会利用当前的 reducer、state 以及传入的参数 action 得到新的 state, 然后通过触发监听数组中的函数,让函数中的使用的 store.getState() 再次触发,起到通知数据更新的作用。

基本上 createStore.js 讲完了,接下来看一下 combineReducers.js 来解决以下问题:

reducer 是如何处理的?// 获取到所有reducer的名字,组成数组
const reducerKeys = Object.keys(reducers)
// 最终合成的 reducers 集合
const finalReducers = {}// 遍历所有的 reducer 使 finalReducers 变成 key( reducer 名字) : value( reducer 执行函数)的对象。
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)// 该函数为 combineReducers.js 核心,主要思路是对传入的 reducer 进行对比,如果任何一个 reducer 返回的 state 与之前的 state 不同,则会返回全新的 state 。(其实这里有个疑问,该函数如果只改了一个 reducer 的 state 数据,都会返回一个全新的 state,为什么会这样处理?)
return function combination (state ={}, action){let hasChanged = falseconst nextState ={}for (let i =0; i < finalReducerKeys.length; i++) {const key = finalReducerKeys[i]const reducer = finalReducers[key]// 获取每个 reducer 的旧 stateconst previousStateForKey = state[key]// 根据旧 state 推出新的 state (纯函数的优势:相同的 state 和 action 返回的新 state 也会是不变的。)const nextStateForKey = reducer(previousStateForKey, action)if (typeof nextStateForKey ==='undefined') {const errorMessage = getUndefinedStateErrorMessage(key, action)throw newError(errorMessage)}// 最后一步 判断是否和之前的 state 是否一致。nextState[key]= nextStateForKeyhasChanged = hasChanged || nextStateForKey !== previousStateForKey}return hasChanged ? nextState : state
}

前面的都比较容易理解,相对比较绕的就是 applyMiddleware.js,阅读前需了解柯理化 和 node 中间件相关知识, 简单来说就是把一个带有多个参数的函数转换成一系列的嵌套函数。它返回一个新函数,这个新函数期望传入下一个参数。解读 applyMiddleware.js 来回答以下问题:

为什么一个临时变量 dispatch 被赋值了 2 次?
middlewareAPI 的 dispatch 为什么要用匿名函数包裹?

接下来我们逐一分析源码:

function applyMiddleware(...middlewares) {return (createStore) =>(reducer, preloadedState) => {const store = createStore(reducer, preloadedState)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: (action, ...args) => dispatch(action, ...args)}const chain = middlewares.map(middleware => middleware(middlewareAPI))dispatch = compose(...chain)(store.dispatch)return {...store,dispatch}}
}

可以看到整体构造传入中间件(middlewares),内部函数返回正常的 store 和改造过后的 dispatch。而改造过后的 dispatch 是由 compose(...chain)(store.dispatch) 生成的,首先 compose(...chain) 来自于暴露的第五个接口 compose.js ,它其实就是我们所谓的柯理化处理函数,而 chain 则是一个类似下面的数组形式:

    next => action => {return next(action)}

compose(...chain) 只是将数组中的函数拼接起来,并未执行。且 compose 最后返回的仍然是一个层层包裹的函数。如下:

const composedFunc = (next3) => {return((next2) => {return func1(func2(next2))})(func3(next3))
}

而真正执行的时候就是在传入这个 dispatch 参数时 compose(...chain)(dispatch),这个 dispatch 正是上面的 next3,next2 就是 func3(next3) 的返回值,依次类推,得到如下:

func1(func2(func3(dispatch)));console.log("coreFun1 run");((action) => {console.log("coreFunc2 run");((action) => {console.log("coreFunc3 run");dispatch(action);})(action);})(action);
}

根据 node 中间件洋葱模型来看,所有的中间件处理了 action 之后 会往里传递,然后最后在最内层触发 dispatch 之后在将其结果作为参数往外传,最终得到一个全新的函数。

再顺着源码往上看,

const store = createStore(reducer, preloadedState)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: (action, ...args) => dispatch(action, ...args)}
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch) // 第二次赋值

这段也主要是为了给每个 middleware 基本的 getState 和 dispatch。 解决几个常问到的疑点:

  1. 为什么一个临时变量 dispatch 被赋值了 2 次? 首先从第一个变量返回的 throw Error 可以看出这段代码希望在 middleware 数组被构建时, dispatch 不应该被调用,否则抛错。而在 middlewareAPI 的 dispatch 中被调用了一次但没触发这个 throw Error,是因为其实直到给 dispatch 第二次赋值时才真正调用 dispatch(我们之前解读到直到 compose 函数传入了(store.dispatch) 之后才会触发调用),所以这时 middlewareAPI 的 dispatch 并不会触发。
  2. middlewareAPI 的 dispatch 为什么要用匿名函数包裹? 目的就是如果每个 middleware 对 dispatch 有所改变,middleware 里面的 dispatch 也会相应做出改变(如一问中所说,compose(...chain)(store.dispatch) 触发了 middlewareAPI 的 dispatch 被调用)。

至此解决完 applyMiddleware 的相关问题,redux 的解析也到此结束(因为 bindActionCreators 的使用场景局限,这里就不展开讲。 ),如有疑问或者解读错误的地方,还望大佬们指正。

参考链接:

读源码理解 Redux Middleware 中间件 - FreewheelLee 的文章

redux 中间件的原理——从懵逼到恍然大悟 - Dell Lee 的文章

简单梳理 Redux 的源码与运行机制 - Nero 的文章

react redux 简化_Redux 源码解析相关推荐

  1. React深入学习与源码解析笔记

    ***当前阶段的笔记 *** 「面向实习生阶段」https://www.aliyundrive.com/s/VTME123M4T9 提取码: 8s6v 点击链接保存,或者复制本段内容,打开「阿里云盘」 ...

  2. redux 思考以及源码解析

    1. 基本概念 redux有以下几个基本概念: 1.1. action action: 是一个对象,对一个行为的基本描述 {type:'add',todo } 1.2 action creator 一 ...

  3. react native 0.50 源码解析 再出发 持续更新

    1.核心类 1.1 RCTRootView 一个RCTRootView持有一个RCTBridge成员变量 RCTRootView : UIViewRCTBridge *bridge;UIViewCon ...

  4. Redux 源码解析系列(一) -- Redux的实现思想

    文章来源: IMweb前端社区 黄qiong(imweb.io) IMweb团队正在招聘啦,简历发至jayccchen@tencent.com Redux 其实是用来帮我们管理状态的一个框架,它暴露给 ...

  5. Redux异步解决方案之Redux-Thunk原理及源码解析

    前段时间,我们写了一篇Redux源码分析的文章,也分析了跟React连接的库React-Redux的源码实现.但是在Redux的生态中还有一个很重要的部分没有涉及到,那就是Redux的异步解决方案.本 ...

  6. 《React源码解析》系列完结!

    前言 距离第一篇<React源码解析(一)>已经过去将近4个月的时间,由于是我第一次进行源码解析相关的写作,思路和文笔还不够成熟.一百多天以来,我基于读者反馈反思这几篇文章中的不足,同时也 ...

  7. 人人都能读懂的react源码解析(大厂高薪必备)

    人人都能读懂的react源码解析(大厂高薪必备) 1.开篇(听说你还在艰难的啃react源码) ​ 本教程目标是打造一门严谨(严格遵循react17核心思想).通俗易懂(提供大量流程图解,结合demo ...

  8. # React源码解析之fiber的初次渲染与更新(下)

    React源码解析之fiber的初次渲染与更新(下) 经历一个月的学习整理,站在前人的肩膀上,对React有了一些浅薄的理解,希望记录自己的学习过程的同时也可以给大家带来一点小帮助.如果此系列文章对您 ...

  9. react源码解析15.schedulerLane

    react源码解析15.scheduler&Lane 视频讲解(高效学习):进入学习 往期文章: 1.开篇介绍和面试题 2.react的设计理念 3.react源码架构 4.源码目录结构和调试 ...

最新文章

  1. python批量命名变量_python变量命名的7条建议
  2. 【控制】《多智能体系统一致性协同演化控制理论与技术》纪良浩老师-第12章-离散时间多智能体系统牵制一致性
  3. 堆、栈及静态数据区详解 转
  4. php数组里面写路径,使用路径在PHP数组中进行递归搜索
  5. Flutter-Cookbook 非官方翻译
  6. 交通安全与智能控制专业学计算机吗,交通安全与智能控制专业主要做什么
  7. Smack 3.3.1 发布,Java 的 XMPP 开发包
  8. Swiper插件的基本使用方法和案例
  9. 关于cmd如何进出文件夹
  10. stm32f030移植到stm32f072
  11. 不愧是我,一晚上教会了女神倒排索引
  12. 尚硅谷SpringCloud2020简单学习记录(个人用)65-73集
  13. 【路径规划】基于蚁群求解多旅行商MTSP问题matlab源码
  14. 多吃什么食物可以明目护眼?
  15. 重拾C语言-摄氏度与华氏度相互转换
  16. 北京超级云计算中心操作训练指南
  17. 学计算机编程难吗,电脑编程难学吗 如何才能学好电脑编程
  18. 2017 ACM/ICPC 北京赛区小结 By jsb @Reconquista
  19. 利用Python网络爬虫实现对网易云音乐歌词爬取
  20. maven 跳过单元测试的3种方法

热门文章

  1. 建造者模式Builder
  2. 如何在没有域的环境中搭建AlwaysOn(二)
  3. ECSHOP集成百度ueditor编辑器上传图片到服务器或又拍云(七牛云)
  4. [转]提高PR值的具体方法
  5. Linux上通过SUU更新Dell服务器固件
  6. rsycn定时同步/备份异地主机文件
  7. 剑指 Offer II 024. 反转链表
  8. 会c语言如何快速入门python,初学者如何从C语言到Python的转化(北大陈斌老师的举例 )...
  9. 解决nuxt.js新建项目报错的问题
  10. SQLyog客户端 导入sql文件乱码的解决方法