Redux源码剖析及应用

使用redux+react已有一段时间,刚开始使用并未深入了解其源码,最近静下心细读源码,感触颇深~

本文主要包含Redux设计思想、源码解析、Redux应用实例应用三个方面。

背景:

React 组件 componentDidMount 的时候初始化 Model,并监听 Model 的 change 事件,当 Model 发生改变时调用 React 组件的 setState 方法重新 render 整个组件,最后在组件 componentWillUnmount 的时候取消监听并销毁 Model。

最开始实现一个简单实例:例如add加法操作,只需要通过React中 setState 去控制变量增加的状态,非常简单方便。

但是当我们需要在项目中增加乘法/除法/幂等等复杂操作时,就需要设计多个state来控制views的改变,当项目变大,里面包含状态过多时,代码就变得难以维护并且state的变化不可预测。可能需要增加一个小功能时,就会引起多处改变,导致开发效率降低,代码可读性不高

例如以往使用较多backbone形式:

image

如上图所示,可以看到 Model 和 View 之间关系复杂,后期代码难以维护。

为了解决上述问题,在 React 中引入了 Redux。Redux 是 JavaScript 状态容器,提供可预测化的状态管理方案。下面详细介绍~~

目的:

1、深入理解Redux的设计思想

2、剖析Redux源码,并结合实际应用对源码有更深层次的理解

3、实际工程应用中所遇到的问题总结,避免再次踩坑

一、Redux设计思想

背景:

传统 View 和 Model :一个 view 可能和多个 model 相关,一个 model 也可能和多个 view 相关,项目复杂后代码耦合度太高,难以维护。

redux 应运而生,redux 中核心概念reducer,将所有复杂的 state 集中管理,view 层用户的操作不能直接改变 state从而将view 和 data 解耦。redux 把传统MVC中的 controller 拆分为action和reducer

设计思想:

(1)Web 应用是一个状态机,视图与状态是一一对应的。

(2)所有的状态,保存在一个对象里面。

Redux 让应用的状态变化变得可预测。如果想改变应用的状态,就必须 dispatch 对应的 action。而不能直接改变应用的状态,因为保存这些状态的地方(称为 store)只有 get方法(getState) 而没有 set方法

只要Redux 订阅(subscribe)相应框架(例如React)内部方法,就可以使用该应用框架保证数据流动的一致性。

Action Creator:

只能通过dispatch action来改变state,这是唯一的方法

action通常的形式是: action = { type: ' ... ', data: data } action一定是有一个type属性的对象

在dispatch任何一个 action 时将所有订阅的监听器都执行,通知它们有state的更新

image

Store:

Redux中只有一个store,store中保存应用的所有状态;判断需要更改的状态分配给reducer去处理。

可以有多个reducer,每个reducer去负责一小部分功能,最终将多个reducer合并为一个根reducer

作用:

  • 维持state树;

  • 提供 getState() 方法获取 state;

  • 提供 dispatch(action) 方法更新 state;

  • 通过 subscribe(listener) 注册监听器。

Reducer:

store想要知道一个action触发后如何改变状态,会执行reducer。reducer是纯函数,根reducer拆分为多个小reducer ,每个reducer去处理与自身相关的state更新

注:不直接修改整个应用的状态树,而是将状态树的每一部分进行拷贝并修改拷贝后的变量,然后将这些部分重新组合成一颗新的状态树。应用了数据不可变性(immutable),易于追踪数据改变。此外,还可以增加例如撤销操作等功能。

Views:

容器型组件 Container component 和展示型组件 Presentational component)

建议是只在最顶层组件(如路由操作)里使用 Redux。其余内部组件仅仅是展示性的,所有数据都通过 props 传入。

容器组件 展示组件
Location 最顶层,路由处理 中间和子组件
Aware of Redux
读取数据 从 Redux 获取 state 从 props 获取数据
修改数据 向 Redux 派发 actions 从 props 调用回调函数

Middleware:

中间件是在action被发起之后,到达reducer之前对store.dispatch方法进行扩展,增强其功能。

例如常用的异步action => redux-thunk、redux-promise、redux-logger等

Redux中store、action、views、reducers、middleware等数据流程图如下:

image

简化数据流程图:

image

Redux核心:

  • 单一数据源,即:整个Web应用,只有一个Store,存储着所有的数据【数据结构嵌套太深,数据访问变得繁琐】,保证整个数据流程是Predictable。

  • 将一个个reducer自上而下一级一级地合并起,最终得到一个rootReducer。 => Redux通过一个个reducer完成了对整个数据源(object tree)的拆解访问和修改。 => Redux通过一个个reducer实现了不可变数据(immutability)。

  • 所有数据都是只读的,不能修改。想要修改只能通过dispatch(action)来改变state。

二、Redux源码解析

前记--- redux的源码比较直观简洁~

Redux概念和API,请直接查看官方英文API和官方中文API

Redux目录结构:

|---src
   |---applyMiddleware.js
   |---bindActionCreators.js
   |---combineReducers.js
   |---compose.js
   |---createStore.js 定义createStore
   |---index.js redux主文件,对外暴露了几个核心API

以下分别是各个文件源码解析(带中文批注):

1) combineReducers.js

  • 实质:组合多个分支reducer并返回一个新的reducer,参数也是state和action,进行state的更新处理

  • 初始化:store.getState()的初始值为reducer(initialState, { type: ActionTypes.INIT })

  • Reference:http://cn.redux.js.org//docs/api/combineReducers.html

combineReducers() 所做的只是生成一个函数,这个函数来调用一系列reducer,每个reducer根据它们的key来筛选出state中的一部分数据并处理,然后这个生成的函数再将所有reducer的结果合并成一个最终的state对象。

在实际应用中,reducer中对于state的处理是新生成一个state对象(深拷贝):

image

因此在combineReducers中每个小reducers的 nextStateForKey !== previousStateForKey 一定为 true => hasChange也一定为true

那么问题来了,为什么要每次都拷贝一个新的state,返回一个新的state呢?
解释:
  1. Reducer 只是一些纯函数,它接收之前的 state 和 action,并返回新的 state。刚开始可能只有一个 reducer,随着应用变大,把它拆成多个小的 reducers,分别独立地操作 state tree 的不同部分,因为 reducer 只是函数,可以控制它们被调用的顺序,传入附加数据,甚至编写可复用的 reducer 来处理一些通用任务,如分页器等。因为Reducer是纯函数,因此在reducer内部直接修改state是副作用,而返回新值是纯函数,可靠性增强,便于追踪bug。

  2. 此外由于不可变数据结构总是修改引用,指向同一个数据结构树,而不是直接修改数据,可以保留任意一个历史状态,这样就可以做到react diff从而局部刷新dom,也就是react非常快速的原因。

  3. 因为严格限定函数纯度,所以每个action做了什么和会做什么总是固定的,甚至可以把action存到一个栈里,然后逆推出以前的所有state,即react dev tools的工作原理。再提及react,一般来说操作dom只能通过副作用,然而react的组件都是纯函数,它们总是被动地直接展现store中得内容,也就是说,好的组件,不受外部环境干扰,永远是可靠的,出了bug只能在外面的逻辑层。这样写好纯的virtual dom组件,交给react处理副作用,很好地分离了关注点。

2) applyMiddleware.js

  • 实质:利用中间件来包装store的dispatch方法,如果有多个middleware则需要使用compose函数来组合,从右到左依次执行middleware

  • Reference:applymiddleware方法、middleware介绍

Reducer有很多很有意思的中间件,可以参考中间件

3) createStore.js

  • 实质:
  1. 若不需要使用中间件,则创建一个包含dispatch、getState、replaceReducer、subscribe四种方法的对象

  2. 若使用中间件,则利用中间件来包装store对象中的dispatch函数来实现更多的功能

  • createStore.js中代码简单易读,很容易理解~

(警告)注:

  1. redux.createStore(reducer, preloadedState, enhancer)

    如果传入了enhancer函数,则返回 enhancer(createStore)(reducer, preloadedState)

    如果未传入enhancer函数,则返回一个store对象,如下:

image
  1. store对象对外暴露了dispatch、getState、subscribe、replaceReducer方法

  2. store对象通过getState() 获取内部最新state

  3. preloadedState为 store 的初始状态,如果不传则为undefined

  4. store对象通过reducer来修改内部state值

  5. store对象创建的时候,内部会主动调用dispatch({ type: ActionTypes.INIT })来对内部状态进行初始化。通过断点或者日志打印就可以看到,store对象创建的同时,reducer就会被调用进行初始化。

Reference:http://cn.redux.js.org/docs/api/Store.html

image

考虑实际应用中通常使用的中间件thunk和logger:

  • thunk源码:
image
  • logger源码:

整个store包装流程:

4) bindActionCreators.js

5) compose.js

[图片上传中...(image-ab545c-1525849044819-7)]

6) index.js

  • 实质:抛出Redux中几个重要的API函数

三、实例应用Redux

Redux的核心思想:Action、Store、Reducer、UI View配合来实现JS中复杂的状态管理,详细讲解请查看:Redux基础

React+Redux结合应用的工程目录结构如下:

|—actions
    addAction.js
    reduceAction.js
|—components
    |—dialog
    |—pagination
|—constant
|—containers
    |---add
        addContainer.js
        add.less
    |—reduce
        reduceContainer.js
        reduce.less
|—reducers
    addReducer.js
    reduceReducer.js
|—setting
    setting.js
|—store
    configureStore.js
|—entry
    index.js
|—template
    index.html

优势:明确代码依赖,减少耦合,降低复杂度~~

下面是实际工程应用中使用react+redux框架进行重构时,总结使用redux时所涉及部分问题&&需要注意的点:

1. Store

在创建新的store即createStore时,需要传入由根Reducer、初始化的state树及应用中间件。

1)根Reducer

重构的工程应用代码很多,不可能让全部state的变更都通过一个reducer来处理。需要拆分为多个小reducer,最后通过combineReducers来将多个小reducer合并为一个根reducer。拆分reducer时,每个reducer负责state中一部分数据,最终将处理后的数据合并成为整个state。注意每个reducer只负责管理全局state中它负责的一部分。每个 reducer的state参数都不同,分别对应它管理的那部分state数据。

实际工程代码重构中以功能来拆分reducer:

[图片上传中...(image-ca8b5e-1525849044819-6)]

是es6中对象的写法,每个reducer所负责的state可以更改属性名。

2)initialState => State树

设计state结构:在Redux应用中,所有state都被保存在一个单一对象中,其中包括工程全局state,因此对于整个重构工程而言,提前设计state结构显得十分重要。

尽可能把state范式化:大部分程序处理的数据都是嵌套或互相关联的,开发复杂应用时,尽可能将state范式化,不存在嵌套。可参考State范式化

2、Action

唯一触发更改state的入口,通常是dispatch不同的action。

API请求尽量都放在Action中,但发送请求成功中返回数据不同情况尽量在Reducer中进行处理。

  • action.js:
  • reducer.js

注:

1、如若在请求发送后,需要根据返回数据来判断是否需要发送其他请求或者执行一些非纯函数,那么可以将返回数据不同情况的处理在Action中进行。

2、假设遇到请求错误,需要给用户展示错误原因,如上述reducer代码中errorReason。 需要考虑到是否可能会在提示中增加DOM元素或者一些交互操作,因此最好是将errorReason在action中赋值,最后在reducer中进行数据处理【reducer是纯函数】。

  • action.js

3、Reducer

reducer是一个接收旧state和action,返回新state的函数。 (prevState, action) => newState

切记要保持reducer纯净,只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。永远不要在reducer中做这些操作:

a、修改传入参数
b、执行有副作用的操作,如API请求和路由跳转等
c、调用非纯函数,例如Date.now() 或 Math.random()

永远不要修改旧state!比如,reducer 里不要使用 Object.assign(state, newData),应该使用Object.assign({}, state, newData)。这样才不会覆盖旧的 state。

  • reducer.js:

4、View(Container)

渲染界面

a、mapStateToProps

利用mapStateToProps可以拿到全局state,但是当前页面只需要该页面的所负责部分state数据,因此在给mapStateToProps传参数时,只需要传当前页面所涉及的state。因此在对应的reducer中,接收的旧state也是当前页面所涉及的state值。

b、mapDispatchToProps

在mapDispatchToProps中利用bindActionCreators让store中dispatch页面所有的Action,以props的形式调用对应的action函数。

所有的 dispatch action 均由 container 注入 props 方式实现。

c、connect ( react-redux )

react-redux 提供的 connect() 方法将组件连接到 Redux,将应用中的任何一个组件connect()到Redux Store中。被connect()包装好的组件都可以得到一些方法作为组件的props,并且可以得到全局state中的任何内容。

connect中封装了shouldComponentUpdate方法

如果state保持不变那么并不会造成重复渲染的问题,内部组件还是使用mapStateToProps方法选择该组件所需要的state。需要注意的是:单独的功能模块不能使用其他模块的state.

d、bind

在constructor中bind所有event handlers => bind方法会在每次render时都重新返回一个指向指定作用域的新函数

  • container.js

四、总结

整篇文章主要是源码理解和具体项目应用中整个Redux处理state的流程,我对Redux有了更深层次的理解。

Redux+React已广泛应用,期待在未来的使用过程中,有更多更深刻的理解~

如有错误,欢迎指正 ( ̄▽ ̄)

参考链接:

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

推荐阅读更多精彩内容

  • 出句作者:陈竹松 试对:刘琴琴 雨洗桃花面=风吹杨柳腰 22112=11121 西风渐瘦池边柳=微雨滴红彼...
    刘琴琴的简书阅读 360评论 0 0
  • 今天遇到了一件令人不愉快的事,不能做只快乐的兔子了。 受了委屈,我也只能边骂边笑脸相迎。讲真,就算我再怎么没心没肺...
    哆啦有只大兔子阅读 908评论 2 7
  • 1 晚上,在我和弟的强烈要求下,爸和妈终于答应一起去看烟花。他们两人开摩托车去,我和弟开小车去。 找地方停好车,一...
    CherryHuang阅读 219评论 2 1