redux源码分析(一)
一、redux是什么?
redux是一个状态管理工具,随着 JavaScript 单页应用开发日趋复杂,我们需要管理应用的state(数据)也越来越多, 这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如**的路由,被选中的标签,是否显示加载动效或者分页器等等。管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。这时我们就需要一个专门来管理状态的工具来对这些state进行管理,像flux,react,vuex等等库就应运而生。
二、redux与react什么关系?
很多初学者都以为redux是react的一个插件,其实不是,redux是一个独立的状态管理库,它可以在react中使用,也可以在vue中使用,甚至是在flutter中使用。只是redux在react中应用比较广泛而已,所以这两没有直接关联。
三、核心概念
这些state需要一个全局对象来进行管理,而这个对象就是store,一个js对象树,store就像一个仓库,里面存放着需要管理的state,所有state都被放在同一个store中,redux推荐单一store树,当然,你也可以在一个应用中创建多个store,不过不推荐这么做,这么做会适得其反,使项目更加难以维护和调试,除此之外,因为一个store是可以存放多个state的,所以没必要再多出一个store。
- state:state本质就是一个js对象,用来保存数据的,与普通js对象的唯一区别,它不能直接被修改,只能通过reducer来修改。
- action:action也是一个对象,用来描述state如何修改的,action的type属性是必须的,通过type来告诉reducer怎么修改state,除此之外action还可以携带一些载荷,这可能在修改数据时用得上。
- reducer:state和action都是一个对象,它们之间并没有联系,而reducer就是一个使state与action产生关联的函数,reducer是一个纯函数,纯函数就是给定相同输入,永远产出相同输出,函数不依赖外部变量,也不改变输入的数据,没有副作用。reducer接受两个参数,当前的state和action,返回一个新的state。
四、简单使用
import { createStore } from 'redux'
function userReducer(state ={name:'张三'},action) {
return state
}
const store = createStore(userReducer)
store.getState() // {name:'张三'}
可以看到,我们通过调用createStore函数,传入一个reducer函数,返回一个store对象,也就是我们开始说的仓库。
打开redux的源码,createStore的源码如下(为了简洁我把注释与中间代码删了)
import $$observable from 'symbol-observable'
import ActionTypes from './utils/actionTypes'
import isPlainObject from './utils/isPlainObject'
export default function createStore(reducer, preloadedState, enhancer) {
...
return {
dispatch,
subscribe,
getState,
replaceReducer,
[$$observable]: observable
}
}
createStore接收三个参数,第一个为reducer,第二个为初始state,第三个为一个增强函数,比如我们使用redux-thunk中间件的时候,需要通过applyMiddleware函数来包裹中间,applyMiddleware的返回值就是第三个参数,第二和第三个参数是可选的。可以很清楚看到当我们执行完createStore函数后返回一个对象,这个对象有五个属性,接下来我们分别看看这五个属性是什么。
export default function createStore(reducer, preloadedState, enhancer) {
if (
(typeof preloadedState === 'function' &&
typeof enhancer === 'function') ||
(typeof enhancer === 'function' && typeof arguments[3] === 'function')
) {
throw new Error(
'It looks like you are passing several store enhancers to ' +
'createStore(). This is not supported. Instead, compose them ' +
'together to a single function'
)
}
if (
typeof preloadedState === 'function' &&
typeof enhancer === 'undefined'
) {
enhancer = preloadedState
preloadedState = undefined
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.')
}
return enhancer(createStore)(reducer, preloadedState)
}
if (typeof reducer !== 'function') {
throw new Error('Expected the reducer to be a function.')
}
let currentReducer = reducer
let currentState = preloadedState
let currentListeners = []
let nextListeners = currentListeners
let isDispatching = false
...
}
我们先来看上面一段代码,开始的几个判断都是用来做参数判断的,我们先不管,紧接着,createStore内部声明了五个变量,分别是currentReducer,它的值为我们创建store是传入的reducer;然后是currentState ,初始值赋值为第二个参数,但是第二个参数我们一般不传,也就是说此时它的值是undefined;第三个变量是currentListeners,初始化为一个空数组,这里面是用来存放监听者的,(忘记说了,补充一下,redux也是采用响应式设计,基于订阅-发布模式来的,currentListeners里面存的就是订阅者);第四个变量nextListeners存得也是监听者,是下一次的,初始值赋值为currentListeners;第五个变量是一个标记,初始化为false,表示没有dispatch正在执行。
function ensureCanMutateNextListeners() {
if (nextListeners === currentListeners) {
nextListeners = currentListeners.slice()
}
}
接着是上面这段代码,这个函数的作用是判断currentListeners与nextListeners是否指向同一个对象,如果指向同一个对象,则将currentListeners拷贝一份赋值给nextListeners。因为开始声明的时候nextListeners就是被赋值为currentListeners。实际的修改发生在这个新的数组,这样确保之前就存在的监听器,在该次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
}
这是store对象的五个属性之一,可以看到这是一个函数,并且函数的返回值是currentState,也就是在createStore函数里面声明的那个currentState变量,看到这里我们想到了什么?没错,就是闭包,createStore是外函数,getState是内函数,内函数的引用被外部拿到,并且内函数里面引用了外函数的局部变量,典型的闭包。由此可见,redux就是通过闭包来存储state的,其实闭包就是一个对象,这个对象被引起闭包的函数(也就是内函数)的[[scopes]]属性拿到,[[scopes]]是一个不可见属性,由js引擎内部调用,[[scopes]]属性类似于数组,保存着当前函数的作用域链。在此我们不做过多的深入讨论,后面有时间会专门出一篇关于闭包的讨论。
回到redux上,我们看到getState函数很简单,首先是判断,如果正在dispatch action,也就是正在调用dispatch的时候不会获取state,则抛出一个错误,否则返回当前的state。
function subscribe(listener) {
if (typeof listener !== 'function') {
throw new Error('Expected the listener to be a function.')
}
if (isDispatching) {
throw new Error(
'You may not call store.subscribe() while the reducer is executing. ' +
'If you would like to be notified after the store has been updated, subscribe from a ' +
'component and invoke store.getState() in the callback to access the latest state. ' +
'See https://redux.js.org/api-reference/store#subscribe(listener) for more details.'
)
}
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)
}
}
subscribe也是一个函数,这个函数接受一个函数为参数,然后返回一个函数。subscribe是用来添加订阅者的,我们来看一下subscribe内部的实现,首先是两个if判断,第一个是要求传入的参数是一个函数,如果不是函数则抛出一个错误,至于为什么要是函数,这得看订阅-发布模式的具体实现,因为redux是使用数组来存储监听者的,发布消息的时候,直接遍历数组,调用函数,如果你自己写订阅发布模式的时候,你也可以写成对象,然后发布消息的时候通过调用对象的方法来进行广播也可以。第二个判断是用来检测是否正在分发action的阶段,如果正在执行dispatch方法,则不允许手动添加订阅者。
接下来定义一个局部变量isSubscribed,赋值为true,代码执行到这一步,说明传入的参数是合理的,且满足不是在dispatch阶段,接着调用ensureCanMutateNextListeners方法,此方法的作用是将currentListeners的值拷贝一份给nextListeners、因为开始这两变量指向同一个数组。然后将listener推入nextListeners数组。
最后subscribe函数执行完返回一个函数,嗯?似曾相识,对,这里又是一个闭包,闭包里包含闭包,subscribe本身就产生了一个闭包,而它的返回值也产生一个闭包。
接下来我们看看这个返回的函数,从名字上看,它就是用来取消订阅的,这里设计得相当巧妙,一般来说,我们要取消订阅可能会这么做,看下面的伪代码
function unsubscribe(listener) {
let index = nextListeners.indexOf(listener)
if(~index) {
nextListeners.splice(index,1)
}
}
然后我们把unsubcribe作为store的一个属性,在外面通过调用store.unsubscribe来取消订阅。这么做理论上来说没问题,但是当我们传入的是一个匿名函数时,我们在函数外面是没法拿到它的引用的,也就没法取消订阅。而redux在这里使用了闭包的特性,在每次添加listener的时候都返回一个闭包函数,闭包函数里保存了订阅者的引用,如果需要取消订阅,只需要执行subscribe返回的函数即可。
let unsubscribe = store.subscribe(() => {
//TODO
}) //添加订阅
unsubscribe() //取消订阅
其实,说完上面这些,subscribe函数的作用已经很清楚来,它就是用来添加订阅者,并返回一个取消此次添加的函数。
接下来我们看看disptach的定义
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?'
)
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.')
}
try {
isDispatching = true
currentState = currentReducer(currentState, action)
} finally {
isDispatching = false
}
const listeners = (currentListeners = nextListeners)
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i]
listener()
}
return action
}
dispatch是用来分发action的函数,dispatch是唯一改变state的方式。首先dispatch接受一个action作为参数。接着判断action的类型,action必须是一个纯对象,第二个判断。action的type类型不能为undefined,第三个判断表示如果有dispatch正在进行没完成,则不能进行下一次dispatch。
然后将isDispatching的值设置为true,表示正在处于dispatch阶段,然后调用currentReducer,并传入currentState和action,currentReducer就是我们开始调用createStore是传入的reducer函数,reducer的返回值赋值给currentState,从而改变state,在这里,这段代码使用try...catch包裹,是因为,我们写的某个reducer可能报错,这样就会导致当前函数后面的代码无法执行,isDispatching将一直未true,这样就会导致其他reducer也无法执行,所以在finally里将isDispatching的值设置为false。保证不会因为一个reducer有问题而导致所有reducer无法执行,同时getState方法也不能获取state。
const listeners = (currentListeners = nextListeners)
紧接着,我们看到这一行代码,将nextListeners的值先赋值给currentListeners,此时nextListeners与currentListeners的引用又指向同一个对象,所以每次添加订阅者都会调用ensureCanMutateNextListeners。然后将nextListeners赋值给listeners,listeners是一个数组,保存着订阅者,然后遍历这个数组,分别执行订阅者的回调函数,进行广播。所以dispatch的作用就是通过调用reducer来改变state,然后进行广播,告诉订阅者,state发生了改变。
function replaceReducer(nextReducer) {
if (typeof nextReducer !== 'function') {
throw new Error('Expected the nextReducer to be a function.')
}
currentReducer = nextReducer
dispatch({ type: ActionTypes.REPLACE })
}
顾名思义,这是一个用来替换当前reducer的函数,参数是一个新的reducer,判断传入的reducer是不是一个函数,不是的话抛出错误,然后将传入的nextReducer赋值给currentReducer,最后调用dispatch进行初始化state。ActionTypes.REPLACE是什么呢?
const randomString = () =>
Math.random()
.toString(36)
.substring(7)
.split('')
.join('.')
const ActionTypes = {
INIT: `@@redux/INIT${randomString()}`,
REPLACE: `@@redux/REPLACE${randomString()}`,
PROBE_UNKNOWN_ACTION: () => `@@redux/PROBE_UNKNOWN_ACTION${randomString()}`
}
export default ActionTypes
可以看到其实就是一个随机字符串,这么多的目的就是为了让我们的reducer走默认值,
function user(state = initState, action) {
switch (action.type) {
case SET_USER:
return state
case SET_LOGIN:
const user = { ...state }
user.isLogin = action.isLogin
return user
default:
return state
}
}
上面的代码是一个简单的reducer,第一次调用dispatch的时候,action.type出入一个随机数,就是为了让其走default分支,从而达到初始化state。
最后一个是对象是observable,这是一个预留的接口,暂时生产中还没有用上,在此不做讲解,而且也可以看到这个对象的键是一个Symbol对象,因为Symbol是独一无二的,而且是redux库内部的依赖,同时以Symbol为键的属性也是无法通过Object.keys获取的,也没把饭for in遍历到,所以我们在外面是无法获取到这个方法的。
在返回store对象之前,还有一个操作,就是在内部调用了disptach方法,进行了state的初始化。
dispatch({ type: ActionTypes.INIT })
与ActionTypes.REPLACE相同,ActionTypes.INIT也是一个随机数。
总结:至此,createStore方法已经讲解完,如果不考虑reducer拆分,和异步action,其实这里已经就完成redux的基本功能了。按照代码来说,就是一个函数,接受另一个函数,并返回一个闭包对象,使用闭包来管理数据,通过这个对象提供的方法来修改闭包里的变量。
水平有限,如有说得不正确的地方,可以私聊一起讨论!
下一篇:combineReducers讲解
推荐阅读
-
java错误分析之junit测试错误(实验一)
-
STL源码分析之第二级配置器
-
Mybaits 源码解析 (十二)----- Mybatis的事务如何被Spring管理?Mybatis和Spring事务中用的Connection是同一个吗?
-
Tomcat源码分析三:Tomcat启动加载过程(一)的源码解析
-
spring-boot-2.0.3不一样系列之源码篇 - run方法(三)之createApplicationContext,绝对有值得你看的地方
-
ORACLE常见错误代码的分析与解决(一)
-
ORACLE常见错误代码的分析与解决(一)
-
Mysql死锁如何排查:insert on duplicate死锁一次排查分析过程
-
前端Vue源码分析-逻辑层分析
-
并发编程之ThreadLocal源码分析