在上一篇文章中,我们通过一个示例页面,了解到Redux的使用方法以及各个功能模块的作用。如果还不清楚Redux如何使用,可以先看看Redux其实很简单(示例篇),然后再来看本文,理解起来会更加轻松。

那么在这一篇文章中,笔者将带大家编写一个完整的Redux,深度剖析Redux的方方面面,读完本篇文章后,大家对Redux会有一个深刻的认识。

核心API

这套代码是笔者阅读完Redux源码,理解其设计思路后,自行总结编写的一套代码,API的设计遵循与原始一致的原则,省略掉了一些不必要的API。

createStore

这个方法是Redux核心中的核心,它将所有其他的功能连接在一起,暴露操作的API供开发者调用。

const INIT = '@@redux/INIT_' + Math.random().toString(36).substring(7)export default function createStore (reducer, initialState, enhancer) {if (typeof initialState === 'function') {enhancer = initialStateinitialState = undefined}let state = initialStateconst listeners = []const store = {getState () {return state},dispatch (action) {if (action && action.type) {state = reducer(state, action)listeners.forEach(listener => listener())}},subscribe (listener) {if (typeof listener === 'function') {listeners.push(listener)}}}if (typeof initialState === 'undefined') {store.dispatch({ type: INIT })}if (typeof enhancer === 'function') {return enhancer(store)}return store
}

在初始化时,createStore会主动触发一次dispach,它的action.type是系统内置的INIT,所以在reducer中不会匹配到任何开发者自定义的action.type,它走的是switch中default的逻辑,目的是为了得到初始化的状态。

当然我们也可以手动指定initialState,笔者在这里做了一层判断,当initialState没有定义时,我们才会dispatch,而在源码中是都会执行一次dispatch,笔者认为没有必要,这是一次多余的操作。因为这个时候,监听流中没有注册函数,走了一遍reducer中的default逻辑,得到新的state和initialState是一样的。

第三个参数enhancer只有在使用中间件时才会用到,通常情况下我们搭配applyMiddleware来使用,它可以增强dispatch的功能,如常用的logger和thunk,都是增强了dispatch的功能。

同时createStore会返回一些操作API,包括:

  • getState:获取当前的state值
  • dispatch:触发reducer并执行listeners中的每一个方法
  • subscribe:将方法注册到listeners中,通过dispatch来触发

applyMiddleware

这个方法通过中间件来增强dispatch的功能。

在写代码前,我们先来了解一下函数的合成,这对后续理解applyMiddleware的原理大有裨益。

函数的合成

如果一个值要经过多个函数,才能变成另外一个值,就可以把所有中间步骤合并成一个函数,这叫做函数的合成(compose)

举个例子

function add (a) {return function (b) {return a + b}
}// 得到合成后的方法
let add6 = compose(add(1), add(2), add(3))add6(10) // 16

下面我们通过一个非常巧妙的方法来写一个函数的合成(compose)。

export 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)))
}

上述代码巧妙的地方在于:通过数组的reduce方法,将两个方法合成一个方法,然后用这个合成的方法再去和下一个方法合成,直到结束,这样我们就得到了一个所有方法的合成函数。

有了这个基础,applyMiddleware就会变得非常简单。

import { compose } from './utils'export default function applyMiddleware (...middlewares) {return store => {const chains = middlewares.map(middleware => middleware(store))store.dispatch = compose(...chains)(store.dispatch)return store}
}

光看这段代码可能有点难懂,我们配合中间件的代码结构来帮助理解

function middleware (store) {return function f1 (dispatch) {return function f2 (action) {// do somethingdispatch(action)// do something}}
}

可以看出,chains是函数f1的数组,通过compose将所欲f1合并成一个函数,暂且称之为F1,然后我们将原始dispatch传入F1,经过f2函数一层一层地改造后,我们得到了一个新的dispatch方法,这个过程和Koa的中间件模型(洋葱模型)原理是一样的。

为了方便大家理解,我们再来举个例子,有以下两个中间件

function middleware1 (store) {return function f1 (dispatch) {return function f2 (action) {console.log(1)dispatch(action)console.log(1)}}
}function middleware2 (store) {return function f1 (dispatch) {return function f2 (action) {console.log(2)dispatch(action)console.log(2)}}
}// applyMiddleware(middleware1, middleware2)

大家猜一猜以上的log输出顺序是怎样的?

好了,答案揭晓:1, 2, (原始dispatch), 2, 1。

为什么会这样呢?因为middleware2接收的dispatch是最原始的,而middleware1接收的dispatch是经过middleware1改造后的,我把它们写成如下的样子,大家应该就清楚了。

console.log(1)/* middleware1返回给middleware2的dispatch */
console.log(2)
dispatch(action)
console.log(2)
/* end */console.log(1)

三个或三个以上的中间件,其原理也是如此。

至此,最复杂最难理解的中间件已经讲解完毕。

combineReducers

由于Redux是单一状态流管理的模式,因此如果有多个reducer,我们需要合并一下,这块的逻辑比较简单,直接上代码。

export default function combineReducers (reducers) {const availableKeys = []const availableReducers = {}Object.keys(reducers).forEach(key => {if (typeof reducers[key] === 'function') {availableKeys.push(key)availableReducers[key] = reducers[key]}})return (state = {}, action) => {const nextState = {}let hasChanged = falseavailableKeys.forEach(key => {nextState[key] = availableReducers[key](state[key], action)if (!hasChanged) {hasChanged = state[key] !== nextState[key]}})return hasChanged ? nextState : state}
}

combineReucers将单个reducer塞到一个对象中,每个reducer对应一个唯一键值,单个reducer状态改变时,对应键值的值也会改变,然后返回整个state。

bindActionCreators

这个方法就是将我们的action和dispatch连接起来。

function bindActionCreator (actionCreator, dispatch) {return function () {dispatch(actionCreator.apply(this, arguments))}
}export default function bindActionCreators (actionCreators, dispatch) {if (typeof actionCreators === 'function') {return bindActionCreator(actionCreators, dispatch)}const boundActionCreators = {}Object.keys(actionCreators).forEach(key => {let actionCreator = actionCreators[key]if (typeof actionCreator === 'function') {boundActionCreators[key] = bindActionCreator(actionCreator, dispatch)}})return boundActionCreators
}

它返回一个方法集合,直接调用来触发dispatch。

中间件

在自己动手编写中间件时,你一定会惊奇的发现,原来这么好用的中间件代码竟然只有寥寥数行,却可以实现这么强大的功能。

logger

function getFormatTime () {const date = new Date()return date.getHours() + ':' + date.getMinutes() + ':' + date.getSeconds() + ' ' + date.getMilliseconds()
}export default function logger ({ getState }) {return next => action => {/* eslint-disable no-console */console.group(`%caction %c${action.type} %c${getFormatTime()}`, 'color: gray; font-weight: lighter;', 'inherit', 'color: gray; font-weight: lighter;')// console.time('time')console.log(`%cprev state`, 'color: #9E9E9E; font-weight: bold;', getState())console.log(`%caction    `, 'color: #03A9F4; font-weight: bold;', action)next(action)console.log(`%cnext state`, 'color: #4CAF50; font-weight: bold;', getState())// console.timeEnd('time')console.groupEnd()}
}

thunk

export default function thunk ({ getState }) {return next => action => {if (typeof action === 'function') {action(next, getState)} else {next(action)}}
}

这里要注意的一点是,中间件是有执行顺序的。像在这里,第一个参数是thunk,然后才是logger,因为假如logger在前,那么这个时候action可能是一个包含异步操作的方法,不能正常输出action的信息。

心得体会

到了这里,关于Redux的方方面面都已经讲完了,希望大家看完能够有所收获。

但是笔者其实还有一个担忧:每一次dispatch都会重新渲染整个视图,虽然React是在虚拟DOM上进行diff,然后定向渲染需要更新的真实DOM,但是我们知道,一般使用Redux的场景都是中大型应用,管理庞大的状态数据,这个时候整个虚拟DOM进行diff可能会产生比较明显的性能损耗(diff过程实际上是对象和对象的各个字段挨个比较,如果数据达到一定量级,虽然没有操作真实DOM,也可能产生可观的性能损耗,在小型应用中,由于数据较少,因此diff的性能损耗可以忽略不计)。

原文发布时间为:2018年06月28日
原文作者:连城
本文来源: 掘金 如需转载请联系原作者

Redux其实很简单(原理篇)相关推荐

  1. v58.03 鸿蒙内核源码分析(环境脚本) | 编译鸿蒙原来很简单 | 百篇博客分析HarmonyOS源码

    颜渊问仁.子曰:"克己复礼为仁.一日克己复礼,天下归仁焉.为仁由己,而由人乎哉?"颜渊曰:"请问其目."子曰:"非礼勿视,非礼勿听,非礼勿言,非礼勿动 ...

  2. 写你的shell,其实很简单[架构篇]

    引语:我本人以前并没有写过shell脚本,也许是因为懒,也许是没有被逼到要去写shell的地步.但是,前段时间,工作需求,要求重新跑几个月的脚本,这些脚本是每天定时进行跑的,而且每天是好几个脚本一起关 ...

  3. 使用篇丨链路追踪(Tracing)很简单:链路拓扑

    前文回顾: 基础篇|链路追踪(Tracing)其实很简单 使用篇|链路追踪(Tracing)其实很简单:请求轨迹回溯与多维链路筛选 使用篇丨链路追踪(Tracing)很简单:链路实时分析.监控与告警 ...

  4. 使用篇丨链路追踪(Tracing)很简单:链路实时分析、监控与告警

    前文回顾: 基础篇|链路追踪(Tracing)其实很简单 使用篇|链路追踪(Tracing)其实很简单:请求轨迹回溯与多维链路筛选 在前面文章里面,我们介绍了单链路的筛选与轨迹回溯,是从单次请求的视角 ...

  5. Android 热修复原理篇及几大方案比较

    热修复说白了就是"即时无感打补丁",比如你们公司上线一个app,用户反应有重大bug,需要紧急修复.2015年以来,Android开发领域里对热修复技术的讨论和分享越来越多,同时也 ...

  6. (python)小最的电脑上安装了一个机器翻译软件,他经常用这个软件来翻译英语文章这个翻译软件的原理很简单,它只是从头到尾,依次将每个英文单词用对应的中文含义来替换。对于每个英文单词,软件会先在内存中

    问题描述 小最的电脑上安装了一个机器翻译软件,他经常用这个软件来翻译英语文章 这个翻译软件的原理很简单,它只是从头到尾,依次将每个英文单词用对应的中文含义 来替换.对于每个英文单词,软件会先在内存中查 ...

  7. 刨析 SpringBoot 自动装配原理,其实很简单

    J3 SpringBoot # 源码 # 自动装配 一日我在愉快得遨游时,看到有鱼友在问:SpringBoot 中引入了 Nacos 依赖为啥就可以直接使用 Nacos 中的相关功能呀! 认真思考了一 ...

  8. 英特尔核显自定义分辨率_核显也能60帧玩3A大作?原理很简单!8寸电脑掌机游戏测评...

    现在科技发展迅速,电脑硬件每年更新换代,性能也是越来越强大,处理器自带的核芯显卡性能也早已超过了普通人的理解范围,相信有很多玩家也早就领教过了英特尔HD系列核显的强大性能,轻轻松松玩个英雄联盟之类的网 ...

  9. java断点续传原理_很简单的Java断点续传实现原理

    原理解析 在开发当中,"断点续传"这种功能很实用和常见,听上去也是比较有"逼格"的感觉.所以通常我们都有兴趣去研究研究这种功能是如何实现的? 以Java来说,网 ...

最新文章

  1. java文件锁定_如何使用java锁定文件(如果可能的话)
  2. 比特币现金与比特币呈竞争关系 分析表示加密货币之间的竞争不是坏事
  3. VSTO Office二次开发对PPT自定义任务窗格测试
  4. jooq和jdbc_将jOOQ与JDBC比较
  5. 获得网页中元素的位置
  6. 无向图中两点之间的距离_九上数学:二次函数图像,一动点到两定点距离和最小...
  7. 整理优秀的网盘搜索合集
  8. Linux下安装hbase
  9. pppoe按需连接服务器无响应,pppoe服务器无响应怎么解决_pppoe拨号失败怎么办
  10. BZOJ_3689_异或之_可持久化Trie+堆
  11. css面试精讲之防止高度坍塌的4种方式
  12. 单片机芯片解密OTP存储器加密
  13. 【Baidu Apollo】5 预测
  14. Item Categories
  15. 办公自动化系统OA学习要点
  16. 机器视觉引导定位系统,工业视觉定位检测
  17. 我们都需要时间,成为更好的人(转载)
  18. ACRush 楼天成回忆录
  19. 学习笔记2:指针经典代码阅读练习
  20. 第三方支付平台的状态表

热门文章

  1. ue4怎么导出fbx文件_【教程】Houdini Engine在UE4中的基本使用(一)
  2. facebook对话链接_并非里程碑! Facebook的100种语言互译模型夸大宣传遭质疑
  3. android 原生分享界面_索尼PlayStation App获得语音聊天功能和新的UI界面
  4. 打印机如何信任计算机,小技巧,骗取打印机的信任
  5. jsforeach异步的问题_js中forEach回调同异步题目
  6. ie php脚本引擎,使用php重新实现PHP脚本引擎内置函数
  7. LeetCode:155. 最小栈
  8. 《系统集成项目管理工程师》必背100个知识点-25变更请求
  9. 笔记-项目沟通管理-规划沟通管理
  10. Vue+Openlayers+Draw实现画笔切换功能,切换画笔为点、线、面