内容简介:本文面对有redux使用经验,熟知redux用法且想了解redux到底是什么样的一个工具的读者,so,希望你有一定的:这会帮助你更快的理解。Redux是一个应用状态管理工具,其工作流程可以参照下图:
本文面对有redux使用经验,熟知redux用法且想了解redux到底是什么样的一个 工具 的读者,so,希望你有一定的:
- 工程结构基础
- redux(react-redux)使用基础
这会帮助你更快的理解。
redux是什么
Redux是一个应用状态管理工具,其工作流程可以参照下图:
从图中可以大概了解,通过user触发( dispatch
)的行为( action
),redux会在通过 middleware
以及 reducer
的处理后更新整个状态树( state
),从而达到更新视图 view
的目标。这就是Redux的工作流程,接下来让我们慢慢细说这之中到底发生了什么。
从index开始
找到根源
首先我们打开 redux的github仓库 ,查看整个项目的目录结构:
. +-- .github/ISSUE_TEMPLATE // GITHUB issue 模板 | +-- Bug_report.md // bug 提交模板 | +-- Custom.md // 通用模板 +-- build | +-- gitbooks.css // 未知,猜测为gitbook的样式 +-- docs // redux的文档目录,本文不展开详细 +-- examples // redux的使用样例,本文不展开详细 +-- logo // redux的logo静态资源目录 +-- src // redux的核心内容目录 | +-- utils // redux的核心工具库 | | +-- actionTypes.js // 一些默认的随机actionTypes | | +-- isPlainObject.js // 判断是否是字面变量或者new出来的object | | +-- warning.js // 打印警告的工具类 | +-- applyMiddleware.js // 神秘的魔法 | +-- bindActionCreator.js // 神秘的魔法 | +-- combineReducers.js // 神秘的魔法 | +-- compose.js // 神秘的魔法 | +-- createStore.js // 神秘的魔法 | +-- index.js // 神秘的魔法 +-- test // redux 测试用例 +-- .bablerc.js // bable编译配置 +-- .editorconfig // 编辑器配置,方便用户在使用不同IDE时进行统一 +-- .eslintignore // eslint忽略的文件目录声明 +-- .eslintrc.js // eslint检查配置 +-- .gitbook.yaml // gitbook的生成配置 +-- .gitignore // git提交忽略的文件目录声明 +-- .prettierrc.json // prettier代码自动重新格式化配置 +-- .travis.yml // travis CI的配置工具 +-- index.d.ts // redux的typescript变量声明 +-- package.json // npm 命令以及包管理 +-- rollup.config.js // rollup打包编译配置 复制代码
当然,实际上 redux
的工程目录中还包括了许多的md文档,这些我们也就不一一赘述了,我们要关注的是 redux
的根源到底在哪,那就让我们从 package.json
开始吧:
"scripts": { "clean": "rimraf lib dist es coverage", "format": "prettier --write \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"", "format:check": "prettier --list-different \"{src,test}/**/*.{js,ts}\" index.d.ts \"**/*.md\"", "lint": "eslint src test", "pretest": "npm run build", "test": "jest", "test:watch": "npm test -- --watch", "test:cov": "npm test -- --coverage", "build": "rollup -c", "prepare": "npm run clean && npm run format:check && npm run lint && npm test", "examples:lint": "eslint examples", "examples:test": "cross-env CI=true babel-node examples/testAll.js" }, 复制代码
从 package.json
中我们可以找到其npm命令配置,我们可以发现 redux
的 build(项目打包)
命令使用了 rollup
进行打包编译(不了解rollup的同学请看这里),那么我们的目光就可以转向到 rollup
的配置文件 rollup.config.js
中来寻找 redux
的根源到底在哪里,通过阅读config文件,我们能找到如下代码:
{ input: 'src/index.js', // 入口文件 output: { file: 'lib/redux.js', format: 'cjs', indent: false }, external: [ ...Object.keys(pkg.dependencies || {}), ...Object.keys(pkg.peerDependencies || {}) ], plugins: [babel()] }, 复制代码
这里为我们指明了整个项目的入口: src/index.js
,根源也就在此, 神秘的魔法 也揭开了一点面纱,接下来,不妨让我们更进一步:
import createStore from './createStore' import combineReducers from './combineReducers' import bindActionCreators from './bindActionCreators' import applyMiddleware from './applyMiddleware' import compose from './compose' import warning from './utils/warning' import __DO_NOT_USE__ActionTypes from './utils/actionTypes' 复制代码
首先是index的依赖部分,我们可以看到其使用了同目录下的 createStore、combineReducers、bindActionCreators、applyMiddleware、compose
这几个模块,同时引入了 utils
文件夹下的工具模块 warning、__DO_NOT_USE__ActionTypes
,这两个工具类显而易见一个是用来进行打印警告,另一个是用来声明不能够使用的默认 actionTypes
的,接下来看看我们的 index
到底做了什么:
function isCrushed() {} if ( process.env.NODE_ENV !== 'production' && typeof isCrushed.name === 'string' && isCrushed.name !== 'isCrushed' ) { warning( ... ) } export { createStore, combineReducers, bindActionCreators, applyMiddleware, compose, __DO_NOT_USE__ActionTypes } 复制代码
首先让我们注意到这个声明的空函数 isCrushed
,这其实是一个断言函数,因为在进行产品级(production)构建的时候,这种函数名都会被混淆,反言之如果这个函数被混淆了,其 name
已经不是 isCrushed
,但是你的环境却不是 production
,也就是说你在dev环境下跑的却是生产环境下的redux,如果出现这种情况,redux会进行提示。接下来便是 export
的时间,我们会看到,这里把之前引入了的 createStore、combineReducers、bindActionCreators、applyMiddleware、compose
以及 __DO_NOT_USE__ActionTypes
。这些就是我们在使用redux的时候,经常会用的一些API和常量。接下来让我们继续追根溯源,一个一个慢慢详谈。
createStore
首先,让我们看看我们声明 redux store
的方法 createStore
,正如大家所知,我们每次去初始化redux的 store
时,都会这样使用:
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import rootReducer from './reducers'; // 在reducers中,我们使用了combinedReducer将多个reducer合并成了一个并export // 使用 thunk 中间件让dispatch接受函数,方便异步操作,在此文不过于赘述 export default createStore(rootReducer, applyMiddleware(thunk)); 复制代码
那么 createStore
到底是怎么去实现的呢?让我们先找到 createStore
函数
export default function createStore(reducer, preloadedState, enhancer) { ... } 复制代码
接受参数
首先从其接受参数谈起吧:
-
reducer
一个函数,可以通过接受一个state tree
然后返回一个新的state tree
-
preloadedState
初始化的时候生成的state tree
-
enhancer
一个为redux
提供增强功能的函数
createStore之前
在函数的顶部,会有一大段的判断:
if ( (typeof preloadedState === 'function' && typeof enhancer === 'function') || (typeof enhancer === 'function' && typeof arguments[3] === 'function') ) { throw new Error( '...' ) } if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') { enhancer = preloadedState preloadedState = undefined } if (typeof enhancer !== 'undefined') { if (typeof enhancer !== 'function') { throw new Error('...') } return enhancer(createStore)(reducer, preloadedState) } if (typeof reducer !== 'function') { throw new Error('...') } 复制代码
通过这些判断,我们能发现 createStore
的一些小规则:
- 第二个参数
preloadedState
和第三个参数enhancer
不能同时为函数类型 - 不能存在第四个参数,且该参数为函数类型
- 在不声明
preloadedState
的状态下可以直接用enhancer
代替preloadedState
,该情况下preloadedState
默认为undefined
- 如果存在
enhancer
,且其为函数的情况下,会调用使用createStore
作为参数的enhancer
高阶函数对原有createState
进行处理,并终止之后的createStore
流程 -
reducer
必须为函数。
当满足这些规则之后,我们方才正式进入createStore的流程。
开始createStore
let currentReducer = reducer let currentState = preloadedState let currentListeners = [] let nextListeners = currentListeners let isDispatching = false 复制代码
接下来便是对函数类的初始变量的声明,我们可以清楚的看见, reducer
和 preloadedState
都被存储到了当前函数中的变量里,此外还声明了当前的监听事件的队列,和一个用来标识当前正在 dispatch
的状态值 isDispatching
。
然后在接下来,我们先跳过在源码中作为工具使用的函数,直接进入正题:
在首当其冲的 subscribe
方法之前,我们不妨先瞧瞧用来在触发 subscribe(订阅)
的监听事件 listener
的 dispatch
:
function dispatch(action) { // action必须是一个对象 if (!isPlainObject(action)) { throw new Error( 'Actions must be plain objects. ' + 'Use custom middleware for async actions.' ) } // action必须拥有一个type if (typeof action.type === 'undefined') { throw new Error( 'Actions may not have an undefined "type" property. ' + 'Have you misspelled a constant?' ) } // 如果正在dispatching,那么不执行dispatch操作。 if (isDispatching) { throw new Error('Reducers may not dispatch actions.') } // 设置dispatching状态为true,并使用reducer生成新的状态树。 try { isDispatching = true currentState = currentReducer(currentState, action) } finally { // 当获取新的状态树完成后,设置状态为false. isDispatching = false } // 将目前最新的监听方法放置到即将执行的队列中遍历并且执行 const listeners = (currentListeners = nextListeners) for (let i = 0; i < listeners.length; i++) { const listener = listeners[i] listener() } // 将触发的action返回 return action } 复制代码
根据上面的代码,我们会发现我们注册的监听事件会在状态树更新之后进行遍历调用,这个时候我们再来继续看 subscribe
函数:
function subscribe(listener) { // listener必须为函数 if (typeof listener !== 'function') { throw new Error(...) } // 如果正在dispatch中则抛错 if (isDispatching) { throw new Error( ... ) } let isSubscribed = true ensureCanMutateNextListeners() nextListeners.push(listener) return function unsubscribe() { if (!isSubscribed) { return } if (isDispatching) { throw new Error( 'You may not unsubscribe from a store listener while the reducer is executing. ' + 'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.' ) } isSubscribed = false ensureCanMutateNextListeners() const index = nextListeners.indexOf(listener) nextListeners.splice(index, 1) } } 复制代码
在这里我们就会用到一个方法 ensureCanMutateNextListeners
,这个方法是用来做什么的呢?让我们看看代码:
function ensureCanMutateNextListeners() { if (nextListeners === currentListeners) { nextListeners = currentListeners.slice() } } 复制代码
在定义变量的阶段,我们发现我们将 currentListeners
定义为了[],并将 nextLiteners
指向了这个 currentListeners
的引用(如果不清楚引用赋值和传值赋值的区别的同学请看这里),也就是说如果我改变 nextListeners
,那么也会同步改变 currentListeners
,这样会造成我们完全无法区分当前正在执行的监听队列和上一次的监听队列,而 ensureCanMutateNextListeners
正是为了将其区分开来的一步处理。
再经过这样的处理之后,每次执行监听队列里的函数之前, currentListeners
始终是上一次的执行 dispatch
时的 nextListeners
:
// 将目前最新的监听方法放置到即将执行的队列中遍历并且执行 const listeners = (currentListeners = nextListeners) 复制代码
只有当再次执行 subscribe
去更新 nextListeners
和后,再次执行 dispatch
这个 currentListeners
才会被更新。因此,我们需要注意:
- 在
listener
中执行unsubscribe
是不会立即生效的,因为每次dispatch
执行监听队列的函数使用的队列都是执行dispatch
时nextListeners
的快照,你在函数里更新的队列要下次dispatch
才会执行,所以尽量保证unsubscribe
和subscribe
在dispatch
之前执行,这样才能保证每次使用的监听队列都是最新的。 - 在
listener
执行时,直接取到的状态树可能并非最新的状态树,因为你的listener
并不能清楚在其执行的过程中是否又执行了dispatch()
,所以我们需要一个方法:
function getState() { if (isDispatching) { throw new Error( 'You may not call store.getState() while the reducer is executing. ' + 'The reducer has already received the state as an argument. ' + 'Pass it down from the top reducer instead of reading it from the store.' ) } return currentState } 复制代码
来获取当前真实完整的 state
.
通过以上代码,我相信大家已经对 subscribe
和 dispatch
以及 listener
已经有一定的认识,那么让我们继续往下看:
function replaceReducer(nextReducer) { if (typeof nextReducer !== 'function') { throw new Error('...') } currentReducer = nextReducer dispatch({ type: ActionTypes.REPLACE }) } 复制代码
这是redux抛出的一个方法,其作用是替换当前整个 redux
中正在执行的 reducer
为新传入的 reducer
,同时其会默认触发一次内置的 replace
事件。
接下来便是最后的波纹(雾,在这个方法里,其提供了一个预留给遵循 observable/reactive(观察者模式/响应式编程)
的类库用于交互的api,我们可以看看这个api代码的核心部分:
const outerSubscribe = subscribe return { subscribe(observer) { if (typeof observer !== 'object' || observer === null) { throw new TypeError('Expected the observer to be an object.') } function observeState() { if (observer.next) { observer.next(getState()) } } observeState() const unsubscribe = outerSubscribe(observeState) return { unsubscribe } }, [$$observable]() { return this } } 复制代码
这里的 outerSubscribe
就是之前redux暴露的的 subscribe
方法,当外部的类库使用暴露对象中的 subscribe
方法进行 订阅
时,其始终能通过其传入的观察者对象,获取当前最新的 state
(通过其观察者对象上的 next
和 getState
方法),同时其也将类库获取最新的state的方法放入了 redux
的监听队列 nextListeners
中,以期每次发生 dispatch
操作的时候,都会去通知该观察者状态树的更新,最后又返回了取消该订阅的方法( subscribe
方法的返回值就是取消当前订阅的方法)。
至此,createStore的面纱终于完全被揭开,我们现在终于认识了所有 createStore
的方法:
-
dispatch
用于触发action,通过reducer
将state
更新 -
subscribe
用于订阅dispatch
,当使用dispatch
时,会通知所有的订阅者,并执行其内部的listener
-
getState
用于获取当前redux
中最新的状态树 -
replaceReducer
用于将当前redux
中的reducer
进行替换,并且其会触发默认的内置REPLACE
action. -
[$$observable]([Symbol.observable])
(不了解Symbol.observable的同学可以看 这里 ),其可以提供observable/reactive(观察者模式/响应式编程)
类库以订阅redux
中dispatch
方法的途径,每当dispatch
时都会将最新的state
传递给订阅的observer(观察者)
。
结语
在工作之余断断续续的书写中通读redux源码的第一篇终于完成,通过一个方法一个方法的分析,虽然有诸多缺漏,但是笔者也算是从其中加深了对 redux
的理解,希望本文也能给诸位也带来一些读源码的思路和对 redux
的认识。
非常感谢你的阅读~
以上所述就是小编给大家介绍的《逐行阅读redux源码(一) createStore》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 【源码阅读】AndPermission源码阅读
- 【源码阅读】Gson源码阅读
- 如何阅读Java源码 ,阅读java的真实体会
- 我的源码阅读之路:redux源码剖析
- JDK源码阅读(六):HashMap源码分析
- 如何阅读源码?
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Head First Java(第二版·中文版)
Kathy Sierra,Bert Bates 著、杨尊一 编译 张然等 改编 / 杨尊一 / 中国电力出版社 / 2007-2 / 79.00元
《Head First Java》是本完整的面向对象(object-oriented,OO)程序设计和Java的学习指导。此书是根据学习理论所设计的,让你可以从学习程序语言的基础开始一直到包括线程、网络与分布式程序等项目。最重要的,你会学会如何像个面向对象开发者一样去思考。 而且不只是读死书,你还会玩游戏、拼图、解谜题以及以意想不到的方式与Java交互。在这些活动中,你会写出一堆真正的Jav......一起来看看 《Head First Java(第二版·中文版)》 这本书的介绍吧!