欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

redux源码分析(一)

程序员文章站 2022-07-16 14:54:49
...

一、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讲解

 

 

相关标签: redux