react redux 简化_Redux 源码解析
前言
近日,出于对 Redux 的好奇和想对自己用过的东西知根知底之目的,做了一个 Redux 的自我检测,以便彻底了解其精髓之处。下面我会对使用 Redux 之后产生的疑问做一个清单,用问题导向往下深挖。
问题清单
- state 初始化如何让全局都能访问到?
- dispatch 之后,Redux 是如何去处理的?
- reducer 是如何处理的?
- state 中的数据被修改之后,订阅者们如何去收到更新后的数据?
- React 发送了 dispatch 之后,如何感知 state 的改变?
- applyMiddleware 中为什么一个临时变量 dispatch 被赋值了 2 次?
- 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。 解决几个常问到的疑点:
- 为什么一个临时变量 dispatch 被赋值了 2 次? 首先从第一个变量返回的 throw Error 可以看出这段代码希望在 middleware 数组被构建时, dispatch 不应该被调用,否则抛错。而在 middlewareAPI 的 dispatch 中被调用了一次但没触发这个 throw Error,是因为其实直到给 dispatch 第二次赋值时才真正调用 dispatch(我们之前解读到直到 compose 函数传入了(store.dispatch) 之后才会触发调用),所以这时 middlewareAPI 的 dispatch 并不会触发。
- 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 源码解析相关推荐
- React深入学习与源码解析笔记
***当前阶段的笔记 *** 「面向实习生阶段」https://www.aliyundrive.com/s/VTME123M4T9 提取码: 8s6v 点击链接保存,或者复制本段内容,打开「阿里云盘」 ...
- redux 思考以及源码解析
1. 基本概念 redux有以下几个基本概念: 1.1. action action: 是一个对象,对一个行为的基本描述 {type:'add',todo } 1.2 action creator 一 ...
- react native 0.50 源码解析 再出发 持续更新
1.核心类 1.1 RCTRootView 一个RCTRootView持有一个RCTBridge成员变量 RCTRootView : UIViewRCTBridge *bridge;UIViewCon ...
- Redux 源码解析系列(一) -- Redux的实现思想
文章来源: IMweb前端社区 黄qiong(imweb.io) IMweb团队正在招聘啦,简历发至jayccchen@tencent.com Redux 其实是用来帮我们管理状态的一个框架,它暴露给 ...
- Redux异步解决方案之Redux-Thunk原理及源码解析
前段时间,我们写了一篇Redux源码分析的文章,也分析了跟React连接的库React-Redux的源码实现.但是在Redux的生态中还有一个很重要的部分没有涉及到,那就是Redux的异步解决方案.本 ...
- 《React源码解析》系列完结!
前言 距离第一篇<React源码解析(一)>已经过去将近4个月的时间,由于是我第一次进行源码解析相关的写作,思路和文笔还不够成熟.一百多天以来,我基于读者反馈反思这几篇文章中的不足,同时也 ...
- 人人都能读懂的react源码解析(大厂高薪必备)
人人都能读懂的react源码解析(大厂高薪必备) 1.开篇(听说你还在艰难的啃react源码) 本教程目标是打造一门严谨(严格遵循react17核心思想).通俗易懂(提供大量流程图解,结合demo ...
- # React源码解析之fiber的初次渲染与更新(下)
React源码解析之fiber的初次渲染与更新(下) 经历一个月的学习整理,站在前人的肩膀上,对React有了一些浅薄的理解,希望记录自己的学习过程的同时也可以给大家带来一点小帮助.如果此系列文章对您 ...
- react源码解析15.schedulerLane
react源码解析15.scheduler&Lane 视频讲解(高效学习):进入学习 往期文章: 1.开篇介绍和面试题 2.react的设计理念 3.react源码架构 4.源码目录结构和调试 ...
最新文章
- python批量命名变量_python变量命名的7条建议
- 【控制】《多智能体系统一致性协同演化控制理论与技术》纪良浩老师-第12章-离散时间多智能体系统牵制一致性
- 堆、栈及静态数据区详解 转
- php数组里面写路径,使用路径在PHP数组中进行递归搜索
- Flutter-Cookbook 非官方翻译
- 交通安全与智能控制专业学计算机吗,交通安全与智能控制专业主要做什么
- Smack 3.3.1 发布,Java 的 XMPP 开发包
- Swiper插件的基本使用方法和案例
- 关于cmd如何进出文件夹
- stm32f030移植到stm32f072
- 不愧是我,一晚上教会了女神倒排索引
- 尚硅谷SpringCloud2020简单学习记录(个人用)65-73集
- 【路径规划】基于蚁群求解多旅行商MTSP问题matlab源码
- 多吃什么食物可以明目护眼?
- 重拾C语言-摄氏度与华氏度相互转换
- 北京超级云计算中心操作训练指南
- 学计算机编程难吗,电脑编程难学吗 如何才能学好电脑编程
- 2017 ACM/ICPC 北京赛区小结 By jsb @Reconquista
- 利用Python网络爬虫实现对网易云音乐歌词爬取
- maven 跳过单元测试的3种方法
热门文章
- 建造者模式Builder
- 如何在没有域的环境中搭建AlwaysOn(二)
- ECSHOP集成百度ueditor编辑器上传图片到服务器或又拍云(七牛云)
- [转]提高PR值的具体方法
- Linux上通过SUU更新Dell服务器固件
- rsycn定时同步/备份异地主机文件
- 剑指 Offer II 024. 反转链表
- 会c语言如何快速入门python,初学者如何从C语言到Python的转化(北大陈斌老师的举例 )...
- 解决nuxt.js新建项目报错的问题
- SQLyog客户端 导入sql文件乱码的解决方法