redux源码分析

一、combineReducers

我一开始就看的是这个。这个文件只有这一个方法combineReducers,即功能明显,很容易理解(哈哈,然而我也看了好久),感觉就是合多为一的作用吧,主要想后面改变更新state树的话,都通过这一个方法来更新,不错,源码看起来太香了,比看别人总结的清晰。

/**
 * @param {*} reducers 传入的是一个对象,即所有的reducer集合
 * 数据结构:
 {
    reducer1: function reducer1() {},
    reducer2: function reducer2() {}
 }
 */
export default function combineReducers(reducers) {
    // 获取 reducers 的 key值,结果为数组
    const reducerKeys = Object.keys(reducers)

    // 后面用的是这个,复制了一套reducers
    const finalReducers = {}

    // 循环,把 reducers 里面的每一项都复制到 finalReducers 里
    for (let i = 0; i < reducerKeys.length; i++) {
        const key = reducerKeys[i]

        if (typeof reducers[key] === 'function') { // 每一个 reducers[key] 都应该是function
            finalReducers[key] = reducers[key]
        }
    }

    // 获取 finalReducers 的 key值,结果为数组
    const finalReducerKeys = Object.keys(finalReducers)

    /**
     * combineReducers API 最终结果返回一个新的reducer函数
     * state:createStore会先dispatch一个初始action,来获取所有reducer的初始值
     * action:为每一次dispatch的action,用来更新数据
     */
    return function combination(state = {}, action) { // 可以试试dispatch更新reducer2的action,就知道这个函数的作用了。每次返回还都是所有的state树。
        let hasChanged = false
        const nextState = {}
        for (let i = 0; i < finalReducerKeys.length; i++) {
            const key = finalReducerKeys[i] // 获取key:reducer1、reducer2
            const reducer = finalReducers[key] // 获取key的值:function reducer1() {...}、function reducer2() {...}
            const previousStateForKey = state[key] // 刚开始,所有的reducer值都为{},dispatch之后才会改变

            const nextStateForKey = reducer(previousStateForKey, action) // 每一个reducer函数都会去调用,只有匹配到actopn.type的值才有可能变;
            if (typeof nextStateForKey === 'undefined') { // 不允许返回undefined,报错,需返回原来的state,或者null
                const errorMessage = getUndefinedStateErrorMessage(key, action)
                throw new Error(errorMessage)
            }
            nextState[key] = nextStateForKey // {}
            hasChanged = hasChanged || nextStateForKey !== previousStateForKey // 只要有一个值更新了就为true,后面就一直为true
        }
        return hasChanged ? nextState : state // 状态有更新就用新的state,没更新就用原来的state
    }
}

/**
 * 顺带去看了createStore的实现,先总结如下:
 * createStore的第一个入参其实就是combination函数(新的reducer,里面的变量有存放所有的reducer)
 * 每一次dispatch(action)前,createStore会先走 observable -> subscribe - (调用getState,获取到最新的state树) 的流程
 * 然后调用combination函数,将新获取的state以及需要dispatch的action作为参数传入,最后又返回新的state。依次循环。
 */

好了,那接下来就对createStore的代码好好说明一下。

二、createStore

弄清了两个API:dispatchgetState,经常用到的也就这俩,还有几个没弄清楚,就先记录下吧。
总结:dispatch方法返回的是自身入参的action,中间去调用reducer总函数获取最新state树。


// redux自己私有的action,用来获取所有reducer的初始状态,不允许其他地方定义或者使用这个action
export const ActionTypes = {
  INIT: '@@redux/INIT'
}

/**
 * 创建一个redux store来保留状态树
 * 改变store的唯一方式就是调用 dispatch() 来改变
 *
 * @param {Function} reducer 通过 combineReducers 返回的函数,接收当前state树 和 一个action
 * 
 * @param {any} [preloadedState] 【可选参数】
 * 如果为函数:且enhancer正常传,则走(2.2)但也许会报错,因为reducer的state参数是个对象;
 * 如果为对象:且enhancer正常传,则走(2.2)作为初始的state,给哪个reducer设置初始值,则必须key和reducer的key要一样才有效.
 *
 * @param {Function} [enhancer] 一般不会传这个参数,但是只要preloadedState为函数,就也会走(2.2),
 * 例如:经常会这样使用redux的`applyMiddleware()`来增强store,传在第二个参数里,也会正常走(2.2)。
 *
 * @returns {Store} 返回getState方法获取store树;返回dispatch来触发action;返回subscribe监听变化
 */
export default function createStore(reducer, preloadedState, enhancer) {

    // (1)如果没有传enhancer,则会将preloadedState赋给enhancer,并将preloadedState置为undefined;
    if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
        enhancer = preloadedState
        preloadedState = undefined
    }

    // (2)如果三个参数都存在的话, 或者上面将preloadedState的值赋给enhancer也算有效
    if (typeof enhancer !== 'undefined') {
        // (2.1)如果enhancer不是函数的话
        if (typeof enhancer !== 'function') {
            throw new Error('Expected the enhancer to be a function.')
        }
        // (2.2)enhancer是函数,则就会直接返回这个函数结果了
        return enhancer(createStore)(reducer, preloadedState)
    }

    if (typeof reducer !== 'function') {
        throw new Error('Expected the reducer to be a function.')
    }

    let currentReducer = reducer
    // 如果preloadedState是对象,就赋值;不是对象,最后就变为undefined;或者在上面直接return,不会走到这里了
    let currentState = preloadedState
    // 表示dispatch是否结束
    let isDispatching = false


    /**
     * 获取整个state树的值
     */
    function getState() {
        return currentState
    }

    /**
     * Dispatches an action. It is the only way to trigger a state change.
     *
     * reducer改变了state之后,监听器就会知道改变了,就会触发监听器,and the change listeners
     * will be notified.
     *
     * dispatch方法只支持普通的对象,如果想dispatch一个Promise、Observable、thunk 或者其他,可以看下redux-thunk
     * 实际上使用redux-thunk后,middleware最终也是diapatch普通对象(仅仅翻译,后面待了解redux-thunk后再进一步确定一下)
     * 
     * @param {Object} action:是一个普通的对象,type字段为一个`字符串常量`
     * @returns {Object} 返回入参action
     */
    function dispatch(action) { // action 必须是个对象,且type不能为undefined

        // 表示一个reducer更新完后才能dispatch下一个,否则报错
        if (isDispatching) {
            throw new Error('Reducers may not dispatch actions.')
        }
        try {
            isDispatching = true
            // 将之前最新的state集合 和 要dispatch的 action 传给 reducer,获取最新的state
            currentState = currentReducer(currentState, action)
        } finally {
            isDispatching = false
        }
        return action // 返回和入参一样的action对象(至于有没有用到,后面再说,现在我还不知道)
    }

    /**
     * 替换当前reducer函数
     * 如果应用程序使用了代码分割并且想动态加载一些reducer、如果为redux实现了热加载机制,这两种都需要使用这个replaceReducer
     *
     * @param {Function} nextReducer The reducer for the store to use instead.
     * @returns {void}
     */
    function replaceReducer(nextReducer) {
        if (typeof nextReducer !== 'function') {
            throw new Error('Expected the nextReducer to be a function.')
        }
        currentReducer = nextReducer
        dispatch({ type: ActionTypes.INIT })
    }

    // 当调用createStore的时候,createStore会自己先dispatch一个 `@@redux/INIT` action,拿到每一个reducer的初始值来填充store树
    dispatch({ type: ActionTypes.INIT })

    return {
        dispatch,
        subscribe, // 没弄清
        getState,
        replaceReducer, // 没弄清
        [$$observable]: observable // 没弄清
    }
}

接下来先看compose,因为源码比较少,比较简单。

三、compose

这个函数主要作用就是将多个函数连接起来,将一个函数的返回值作为另一个函数的传参进行计算,得出最终的返回值。

/**
 * 特点:从右到左执行,若需要从左往右执行,则调换顺序即可 b(a(...args))
 * 
 *
 * @param {...Function} 多个要组合的函数.
 * @returns {Function} 通过从右到左组合实参函数而得到的一个总函数
 * 例如:调用 compose(f, g, h),最后返回的是 (...args) => f(g(h(...args))).
 */

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  // 从右到左组合单参数函数,最右边的函数可以接受多个参数,因为它为得到的复合函数提供了签名
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

经过查看这个源码,理解了里面arguments对象在函数中的用法及作用:

function consoleNew() {
console.log(arguments);
}
consoleNew(function() {}, function(){}, {a: 1, b: 2}, '字符串')
arguments认识

四、applyMiddleware

applyMiddleware函数是用来绑定中间件函数的函数(这句话是我copy过来的官话)。即:通过改变dispatch的指向,一步一步完成每个中间件的执行,最终走向真正的dispatch(action),走向reducer,更新state。

一般的中间件函数middleware的形式为:

//middleware.js文件

const middleware = ({dispatch, getStore})=>(next)=>(action)=>{
  //dosomething
}
// 或
const middleware = (store)=>(next)=>(action)=>{
  //dosomething
}

这个需要和createStore结合起来一起看。

// applyMiddleWare.js

export default function applyMiddleware(...middlewares) {
  // 返回函数A,函数A返回函数B
  return (createStore) => (reducer, preloadedState, enhancer) => {
    // 函数B内部,函数B返回store
    const store = createStore(reducer, preloadedState, enhancer)
    // ...
    return {
      ...store,
      dispatch
    }
  }
}
// createStore.js

export default function createStore(reducer, preloadedState, enhancer) {
  // ...
  return enhancer(createStore)(reducer, preloadedState)
  // ...
}
// 我们一般这样调用createStore,不传enhancer参数

import { createStore } from 'redux' ;

// applyMiddleware(m1, m2, m3)的结果为函数A
const store = createStore(reducer, applyMiddleware(m1, m2, m3))

我们知道createStore函数的处理,将preloadedState = applyMiddleware(m1, m2, m3)赋值给enhancer,并将preloadedState = undefined,即会执行enhancer的函数。因此,applyMiddleware(m1, m2, m3)的结果为函数A,刚好和createStore的返回函数吻合。因此,接下来直接看函数B的内部实现。

  • enhancer(createStore):相当于调用了函数A,返回函数B;
  • A(reducer, preloadedState):相当于调用了函数B,生成store,并且绑定中间件。
return (createStore) => (reducer, preloadedState, enhancer) => {

    // 函数B内部
    const store = createStore(reducer, preloadedState, enhancer)
    // 这已经是第二次调用了,preloadedState和enhancer都为undefined
    // 因此,直接返回store。
    let dispatch = store.dispatch
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (action) => dispatch(action)
    }

    chain = middlewares.map(middleware => middleware(middlewareAPI)) // 难点一
    dispatch = compose(...chain)(store.dispatch) // 难点二

    return {
      ...store,
      dispatch
    }
}

难点一:chain = middlewares.map(middleware => middleware(middlewareAPI))

  • middlewares.map:遍历每一个中间件;
  • middleware(middlewareAPI):为每一个中间件的第一层函数传入{ getState, dispatch }对象,运行并返回得到(next)=>(action)=>{}的函数;
  • 返回(next)=>(action)=>{}的中间件函数数组。

即以一个为例,中间件middleware函数的形式为:

//middleware.js文件

const middleware = ({dispatch, getStore})=>(next)=>(action)=>{
  //dosomething
}
// 或
const middleware = (store)=>(next)=>(action)=>{
  //dosomething
}

难点二:dispatch = compose(...chain)(store.dispatch)

  • ------从右到左执行 & 分析-------
  • 前面查看过compose的源码,已经知道,是从右至左运行的,这里将store.dispatch作为最后一个中间件的参数来运行,也就是说最后一个中间件的next参数就是store.dispatch
  • 后面每一个中间件的next参数都是前一个函数的返回值(action)=>{ //... }
  • 即:最后的返回值是第一个中间件函数的返回值(action)=>{ //...中间有好多层next替换后的嵌套 }函数,将其赋给dispatch

注意因此,在这里,需要注意 千万不能在某个中间件中直接调用dispatch,不然会无限循环。因为,直接调用dispatch会从第一个中间件重新开始。(如果是dispatch另一个action,不至于无限循环;如果是dispatch(action))和一开始的action一样,那就会进入无限循环。

  • ------从左到右调用时-------
  • 之后每次调用dispatch,就是调用第一个中间件的(action)=>{ //... }函数;
  • 每个中间件函数在(action)=>{ //... }中调用next就是调用下一个中间件的(action)=>{ //... }函数;
  • 一步一步会走向调用最后一个中间件的next,会调用原生的store.dispatch,更新state。

举例拆分说明如下:

// m1、m2、m3值如下:
m3 = (next) => (action) => {
  next(action) // store.dispatch(action)
}
// 分析:
dispatch = m1(m2(m3(store.dispatch)))
dispatch(action)
m1(m2(m3(store.dispatch)))(action)
// (1)进入m1,返回next(action) next指向下面
m2(m3(store.dispatch))
// (2)进入m2,返回next(action) next指向下面
m3(store.dispatch)
// (3)进入m3,返回next(action) next为store.dispatch
store.dispatch(action)

总结一句话,只要使用了applyMiddleWare(m1, m2, ...)这个函数,都会得到一个增强型的store,主要表现为改变dispatch的指向,第一次dispatch就会进入嵌套的最外层的middleware里,即指向是从左向右顺序的第一个中间件的(action)=>{ //... },然后开始依次向下走,直到最后调用store.dispatch(action)才跳出中间件,进入reducer里,改变state(没有匹配到就不更新state)。


之前有个问题:
问:会不会无限循环呢,一直在中间件里面走,能否next跳出中间件而去进入reducer改变state呢?
答:终于弄通了,最后无论如何肯定是会进入reducer里的呀!其实就是上面总结的一段话,2020.5.7日凌晨24分终于想通了,当时给忘了createStore的dispatch的作用了,只要一dispatch,就会去总的reducer里判断state是否改变来更新state树,从而,就跳出了中间件,更新了状态,不管怎样,最终都会走向store.dispatch(action),然后进入reducer来更新状态的。

若使用了middleware,执行顺序:dispatch(action) -> middleware -> reducer

整理了一下createStore和applyMiddleware在项目中的使用,代码执行分析如图:

createStore(reducer, applyMiddleware(m1, m2, m3, ...))实现机制

再赠送一个用比画出来的:


草图 - 跟上图一样的理解

此时,我也看了下redux-thunk的源码,整理了下,点击查看

接下来看下bindActionCreators是怎么回事吧!

五、bindActionCreators

这里跟connect有关系,我先说一下connect,有两种写法:

  • 不使用装饰器@写法:这里主要表现为容器型组件
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { action1, action2 } from './actions'; // 都用export导出
// export {action1, action2}

class MyComponent extends Component {
     // ...
    this.props.action1(); // 调用自己定义的方法名
    this.props.actionNew(); // 调用自己定义的方法名
}
const mapStateToProps = (state) => state;
const mapDispatchToProps = (dispatch) => {
    action1: (params) => { // 定义时可以和action名一样
        dispatch(action1(params))
    },
    actionNew: (params) => { // 定义时也可以和action不一样
        dispatch(action2(params))
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);
  • 使用装饰器@写法:就需要用到bindActionCreators
    其实bindActionCreators这个方法相当于简化了上面的写法,函数内部帮我们实现了mapDispatchToProps的作用,不过唯一的区别就是this.props.xxx中的xxx是我们暴露出来的action函数名,而不能像上面代码在mapDispatchToProps中自定义函数名。如下:
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from './actions'; // 都用export导出
// export {action1, action2}

@connect(
    state => state,
    dispatch => bindActionCreators(actions, dispatch)
)
class MyComponent extends Component {
     // ...
    this.props.action1(); // 只能是action1
    this.props.action2(); // 只能是action2
}

export default MyComponent;

是不是代码量少了好多,关键是两种方式实现的功能是一样的,那我们何尝不尝试一下少写点代码的方式呢!我这里只是写了两个action,那如果一个页面有很多action,如果用第一种方式,那mapDispatchToProps这个函数就会很长很长,那个时候就能发现使用bindActionCreators这个方法的好处了。

  • 其实,那我觉得也可以将第一种和第二种搭配起来使用,即:不用装饰器@,和使用bindActionCreators,如下:站在原理的层面上,我觉得应该也没什么问题 - 虽然一般没人这样用,哈哈哈!!!
import React, { Component } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import * as actions from './actions'; // 都用export导出
// export {action1, action2}

class MyComponent extends Component {
     // ...
    this.props.action1(); // 调用自己定义的方法名
    this.props.actionNew(); // 调用自己定义的方法名
}
const mapStateToProps = (state) => state;
const mapDispatchToProps = (dispatch) => bindActionCreators(actions, dispatch)

export default connect(mapStateToProps, mapDispatchToProps)(MyComponent);

至于源码,总体就下面这一点点,很简单的实现,跟我们第一种不用装饰器@写法的实现是一样的,只不过bindActionCreators帮我们统一实现处理了,让我们不用一个一个去dispatch(action方法)了。

// 处理单个action,把action放进dispatch里,作为props.xxx 的 值(类似定义action1)
// 即:调用这个方法,返回的是`dispatch(action(params))`
function bindActionCreator(actionCreator, dispatch) {
  return function () {
    return dispatch(actionCreator.apply(undefined, arguments));
  };
}

// actionCreators:所有的action对象 or 一个函数
export default function bindActionCreators(actionCreators, dispatch) {
  if (typeof actionCreators === 'function') { (1)
    return bindActionCreator(actionCreators, dispatch);
  }

  // actionCreators必须是函数或者对象
  if (typeof actionCreators !== 'object' || actionCreators === null) {
    throw new Error('bindActionCreators expected an object or a function, instead received ' + (actionCreators === null ? 'null' : typeof actionCreators) + '. ' + 'Did you write "import ActionCreators from" instead of "import * as ActionCreators from"?');
  }

  // 以下是`actionCreators`为对象的一系列操作
  var keys = Object.keys(actionCreators); // 获取所有的action函数的 key
  var boundActionCreators = {}; // 创建一个空的对象
  for (var i = 0; i < keys.length; i++) { // 循环action函数的 key 数组
    var key = keys[i]; // actions对象的健
    var actionCreator = actionCreators[key]; // 健对应的每一个action函数赋值给`actionCreator`
    if (typeof actionCreator === 'function') { // 判断`actionCreator`是否为函数,是就走下面,和(1)处一样
      boundActionCreators[key] = bindActionCreator(actionCreator, dispatch);
    }
  }
  return boundActionCreators; // `actionCreators`为对象最终返回这个对象
}

好了,redux这几个API的源码已经理解了,接下来就是想对react-redux经常用到的两个API(Providerconnect)进行了解。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 229,517评论 6 539
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 99,087评论 3 423
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 177,521评论 0 382
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 63,493评论 1 316
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 72,207评论 6 410
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 55,603评论 1 325
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 43,624评论 3 444
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 42,813评论 0 289
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 49,364评论 1 335
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 41,110评论 3 356
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 43,305评论 1 371
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 38,874评论 5 362
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 44,532评论 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 34,953评论 0 28
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 36,209评论 1 291
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 52,033评论 3 396
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 48,268评论 2 375