Redux源码解析

写在开始

本篇主要结合react-native 使用redux的过程,说明使用redux的方法和原理,揭秘Redux单向数据流更新的机制原理,适合有一定Redux基础的同学。

Redux 工作图

Redux原理图


上图是redux的工作原理图,具体每一步骤的实现请见下面的详细分析。

Redux需要懂的七大元素

  • combineReducers()
  • createStore()
  • <Provider/>
  • connect()
  • mapStateToProps()
  • mapDispatchToProps()
  • action

一. combineReducers

将应用整体state划分为不同的reducer,最终合并为rootReducer ===>combineReducers()
1.1reducer

reducer 就是一个方法,接收旧的 state和当前操作 action,返回新的 state。需要注意的是reducer是纯函数,永远不要在reducer里做这些操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如Date.now()Math.random()
1.2combineReducers()用法
const rootReducer = combineReducers({
    localCounter:localCounter,
    serverCounter:serverCounter
})
1.3combineReducers()作用
  • 将子reducer用key-value的形式组成rootReducer,value是该子reducer的实现方法。
  • 返回值:一个总reducer,内部包含所有子reducer
1.4combineReducers()关键源码
function combineReducers(reducers) {
  var reducerKeys = Object.keys(reducers);
  var finalReducers = {};
  for (var i = 0; i < reducerKeys.length; i++) {
    var key = reducerKeys[i];
    ...
    if (typeof reducers[key] === 'function') {
      //reduer数组
      finalReducers[key] = reducers[key];
    }
  }
   //reducer的key
  var finalReducerKeys = Object.keys(finalReducers);
  ...
  //返回一个function,该方法接收state和action作为参数,其实返回值就是rootReducer
  //遍历reducers数组,将action传入每个reducer方法中得到的新状态,与旧状态对比是否变化,若变化则返回新状态,若没有变化则返回旧状态
  return function combination() {
   ...
    var hasChanged = false;
    var nextState = {};
    //遍历reducer数组,执行每个reducer的方法
    for (var i = 0; i < finalReducerKeys.length; i++) {
      var key = finalReducerKeys[i];
      var reducer = finalReducers[key];
      var previousStateForKey = state[key];
      //传入旧state,action,得到新state
      var nextStateForKey = reducer(previousStateForKey, action);
      ...
      nextState[key] = nextStateForKey;
      //判断状态是否发生了改变
      hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
    }
    //返回值处理后的state
    return hasChanged ? nextState : state;
  };
}

二. createStore

然后根据 rootReducer创建store===>createStore()
2.1 store

是redux的核心,存储APP的所有状态,只能有一个。改变状态的唯一方法是调用store.dispatch方法

2.2 createStore()作用
  • 两种创建方式:
  • createStore(rootReducer,initialState);
  • createStore(rootReducer,initialState,applyMiddleware(thunkMiddleware));

A thunk is a function that wraps an expression to delay its evaluation.简单来说一个 thunk 就是一个封装表达式的函数,封装的目的是延迟执行表达式

可以使用第三方库来增强store,通常会使用redux-thunk库来支持异步方法的dispatch。thunk最终起的作用,对dispatch调用的action进行检查,如果action在第一次调用之后返回的是function,则将(dispatch, getState)作为参数注入到action返回的方法中,执行异步逻辑(相当于开始一个新的action),若有返回对象则进行分发。

  • 返回值

{Store} A Redux store that lets you read the state, dispatch actions
and subscribe to changes.

  • (1) dispatch(action): 用于action的分发,改变store里面的state
  • (2) subscribe(listener): 注册listenerstore里面state发生改变后,执行该listener。返回unsubscrib()方法,用于注销当前listenerRedux采用了观察者模式,store内部维护listener数组,用于存储所有通过store.subscribe注册的listener
  • (3) getState(): 读取store里面的state
  • (4) replaceReducer(): 替换reducer,改变state修改的逻辑
2.3 源码分析

function createStore(reducer, preloadedState, enhancer) {
  ...

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.');
    }
    //返回增强store
    return enhancer(createStore)(reducer, preloadedState);
  }

  var currentState = preloadedState;
  var currentListeners = [];
  var nextListeners = currentListeners;
  var isDispatching = false;

  /**
   * 返回当前状态
   */
  function getState() {
    return currentState;
  }

  /**
   * 注册`listener`,维护一个listener的数组
   * `store`里面`state`发生改变后,执行该`listener`
   * 观察者模式实现的关键
   */
  function subscribe(listener) {
    ...
    nextListeners.push(listener);

    //返回一个注销listener的方法
    return function unsubscribe() {
      ...
      var index = nextListeners.indexOf(listener);
      nextListeners.splice(index, 1);
    };
  }

  /**
   * Dispatches an action. It is the only way to trigger a state change.
   */
   var currentReducer = reducer;
  function dispatch(action) {
    //类型校验...
    try {
      isDispatching = true;
      //执行rootReducer,得到新的state
      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }

    var listeners = currentListeners = nextListeners;
    //循环遍历,执行listener,通知数据改变了,listeners具体是什么?看容器组件
    for (var i = 0; i < listeners.length; i++) {
      listeners[i]();
    }

    return action;
  }

  /**
   * Replaces the reducer currently used by the store to calculate the state.
   */
  function replaceReducer(nextReducer) {
    currentReducer = nextReducer;
    dispatch({ type: ActionTypes.INIT });
  }

  //返回值
  return _ref2 = {
    dispatch: dispatch,
    subscribe: subscribe,
    getState: getState,
    replaceReducer: replaceReducer
  }, _ref2[_symbolObservable2['default']] = observable, _ref2;
}

三. Provider

store传递给应用中的View===> <Provider/>
3.1 <Provider/>是redux提供的组件
<Provider store={store}>
  <APP />
</Provider>
3.2 作用:将store传递给其子组件

将store设置到子组件的context中,这样应用的所有子组件就默认都拿到了store

3.3 源码
var Provider = function (_Component) {
  _inherits(Provider, _Component);

  //用于指定子组件可直接访问的上下文数据,所以子组件可以直接访问store了
  Provider.prototype.getChildContext = function getChildContext() {
    return { store: this.store, storeSubscription: null };
  };

  function Provider(props, context) {
    _classCallCheck(this, Provider);

    var _this = _possibleConstructorReturn(this, _Component.call(this, props, context));

    _this.store = props.store;
    return _this;
  }

  Provider.prototype.render = function render() {
    return _react.Children.only(this.props.children);
  };

  return Provider;
}(_react.Component);

exports.default = Provider;

四. connect

如何将react中的UI组件与redux的状态、事件关联起来====>connect()方法
4.0 UI组件和容器组件

React-Redux 将所有组件分成两大类:UI 组件(presentational component)和容器组件(container component)。

UI组件:

①只负责 UI 的呈现,不带有任何业务逻辑
②没有状态(即不使用this.state这个变量)
③所有数据都由参数(this.props)提供
④不使用任何 Redux 的 API

容器组件:

①负责管理数据和业务逻辑,不负责 UI 的呈现
②带有内部状态
③使用 Redux 的 API

4.1connect()生成容器组件
  • 通过<Provider/>传递store是给容器redux的容器组件
    用于从 UI 组件生成容器组件。connect的意思,就是将这两种组件连起来。
    connect方法接受两个参数:mapStateToProps和mapDispatchToProps。它们定义了 UI 组件的业务逻辑
let ConnectCounter = connect(
    mapStateToProps,
    mapDispatchToProps
)(Counter)
4.2 作用

connect是一个高阶函数,首先传入mapStateToPropsmapDispatchToProps,然后返回一个生产Component的函数wrapWithConnect(),然后再将真正的Component作为参数传入wrapWithConnect(MyComponent),这样就生产出一个经过包裹的Connect组件(也就是容器组件)。

容器组件具有如下特点:

  • (1)通过this.context获取祖先Component的store,也就是通过<Provider/>传递过来的store。
  • (2)props包括stateProps、dispatchProps、parentProps,合并在一起得到nextState,作为props传给真正的Component,这样在真正组件中就能通过this.props获取到各种数据和方法。
  • (3)componentDidMount调用store.subscribe(listener)注册监听方法,对store的变化进行订阅,当store变化的时候,更新渲染view。
  • (4)componentWillUnmount时注销订阅
4.3源码分析

注意订阅的实现

var Connect = function (_Component) {
            _inherits(Connect, _Component);

            /*
            * 构造函数中,构造一个订阅对象,属性有this.store,方法this.onStateChange.bind(this)
            */
            function Connect(props, context) {
                ...
                //获取store。
                //从父组件或context中获取store。这里使用的是从context中获取
                //storeKey = _ref$storeKey === undefined ? 'store' : _ref$storeKey,
                _this.store = props[storeKey] || context[storeKey];

                ...

                //初始化订阅逻辑
                _this.initSubscription();
                return _this;
            }

            //初始化订阅方法
            Connect.prototype.initSubscription = function initSubscription() {
                if (!shouldHandleStateChanges) return;

                var parentSub = (this.propsMode ? this.props : this.context)[subscriptionKey];

                //wym: 调用的是Subscription.js中方法,向store内部注册一个listener---this.onStateChange.bind(this)
                this.subscription = new _Subscription2.default(this.store, parentSub, this.onStateChange.bind(this));

            
                this.notifyNestedSubs = this.subscription.notifyNestedSubs.bind(this.subscription);
            };

            //当数据状态发生改变时
            Connect.prototype.onStateChange = function onStateChange() {
                this.selector.run(this.props);

                if (!this.selector.shouldComponentUpdate) {
                    this.notifyNestedSubs();
                } else {
                    this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate;
                    //设置state,view会自动重新渲染
                    this.setState(dummyState);
                }
            };

            Connect.prototype.getChildContext = function getChildContext() {
                ...
            };

            /*
            * 组件状态完成时,向store注册监听方法
            */
            Connect.prototype.componentDidMount = function componentDidMount() {
                if (!shouldHandleStateChanges) return;

                //实际调用this.store.subscribe(this.onStateChange);
                //向store注册监听方法
                this.subscription.trySubscribe();

                this.selector.run(this.props);
                if (this.selector.shouldComponentUpdate) this.forceUpdate();
            };



            Connect.prototype.componentWillUnmount = function componentWillUnmount() {
                //注销订阅
                if (this.subscription) this.subscription.tryUnsubscribe();
                this.subscription = null;
                ...
            };


            Connect.prototype.render = function render() {
                var selector = this.selector;
                selector.shouldComponentUpdate = false;

                if (selector.error) {
                    throw selector.error;
                } else {
                    return (0, _react.createElement)(WrappedComponent, this.addExtraProps(selector.props));
                }
            };

            return Connect;
        }(_react.Component);

五.mapStateToProps()

建立一个从(外部的)state对象到(UI 组件的)props对象的映射关系。mapStateToProps会订阅 Store,每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染

六. mapDispatchToProps

  • 用来建立 UI 组件的方法到store.dispatch方法的映射,它定义了哪些用户的操作应该当作 Action,传给 Store。
  • 它可以是一个函数,也可以是一个对象。(只是不同的写法,作用一样)

七. Action

Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。
分为:

  • 同步 action ,返回的是一个对象,要求是纯净的函数。

纯净:没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

  • 异步action,返回的是一个方法,这个函数会被Redux Thunk middleware执行。这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。

Redux中的观察者模式

redux之所以能够当state变化后,更新绑定的视图,是因为内部实现的观察者模式

观察者模式的实现

1. store提供了注册监听的方法===>subscribe(listener)
  • store内部维护listener数组,用于存储所有通过store.subscrib注册的listener,store里面state发生改变(即为调用store.dispatch())后,依次执行数组中的listener
  • store.subscrib返回unsubscrib方法,用于注销当前listener。

调用store.dispatch()的时候做了两件事:
(1)更新当前state: currentState = currentReducer(currentState, action);
(2)依次执行数组中的listener。

2. Connect组件中,向store中注册监听方法
  • ①构造方法中:初始化订阅逻辑,将listener:this.onStateChange.bind(this)传递给Subscription.js。
  • ②componentDidMount方法中,调用store.subscribe(listener)注册监听方法:onStateChange()

onStateChange方法中,当判断需要更新数据时,调用的是this.setState(state);
===》根据react机制,则会重新渲染view

  • ③在componentWillUnmount方法中,注销订阅

Redux原理图

根据以上的源码分析,redux的工作流可用下图进行概括。


redux创建过程概括

  • 将一个APP的状态分解成不同的reducer,最后创建store(有整个数据state,有分发action的方法,有注册listener的方法)
  • 将store通过<Provider/>组件传递给容器组件
  • 容器组件通过UI组件,mapStateToProps, mapDispatchToProps通过connect()转化而来
  • 将UI交互事件,外部输入事件写成action,用来触发reducer

redux更新数据过程概括

  • 在UI组件上有交互操作,或外部输入(网络请求)时,====>写成Action
  • store.dispatch(action),结果:
    (1)合成新的state,新的state通过mapStateToProps()传递给UI组件,
    (2)执行通过store.subscribe()注册的listener
  • listener具体逻辑:调用setState()设置信息数值,触发View的自动render
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,319评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,801评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,567评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,156评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,019评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,090评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,500评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,192评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,474评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,566评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,338评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,212评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,572评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,890评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,169评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,478评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,661评论 2 335

推荐阅读更多精彩内容