Redux其实很简单(原理篇)
在上一篇文章中,我们通过一个示例页面,了解到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 = initialState initialState = undefined } let state = initialState const 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 something dispatch(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 = false availableKeys.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的性能损耗可以忽略不计)。
原文作者:连城
本文来源: 掘金 如需转载请联系原作者

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
如何优雅地优化MySQL大表
单表优化除非单表数据未来会一直不断上涨,否则不要一开始就考虑拆分,拆分会带来逻辑、部署、运维的各种复杂度,一般以整型值为主的表在千万级以下,字符串为主的表在五百万以下是没有太大问题的。而事实上很多时候MySQL单表的性能依然有不少优化空间,甚至能正常支撑千万级以上的数据量。 字段尽量使用TINYINT、SMALLINT、MEDIUM_INT作为整数类型而非INT,如果非负则加上UNSIGNED VARCHAR的长度只分配真正需要的空间 使用枚举或整数代替字符串类型 尽量使用TIMESTAMP而非DATETIME, 单表不要有太多字段,建议在20以内 避免使用NULL字段,很难查询优化且占用额外索引空间 用整型来存IP 索引索引并不是越多越好,要根据查询有针对性的创建,考虑在WHERE和ORDER BY命令上涉及的列建立索引,可根据EXPLAIN来查看是否用了索引还是全表扫描 应尽量避免在WHERE子句中对字段进行NULL值判断,否则将导致引擎放弃使用索引而进行全表扫描 值分布很稀少的字段不适合建索引,例如"性别"这种只有两三个值的字段 字符字段只建前缀索引 字符字段最好不要做主键 不用...
- 下一篇
阿里安全资深总监张玉东解读安全技术9大新趋势
7月9日,2018第十三届 (ISC)² 亚太信息安全峰会在香港举行,经过多轮严苛遴选评审,阿里巴巴集团安全部资深总监张玉东所代表的阿里安全技术体系获颁亚太区信息安全领袖成就表彰,成为迄今中国内地首位来自互联网企业的获奖者。 阿里巴巴安全部资深总监张玉东在(ISC)²2018亚太峰会演讲 “荣获(ISC)²颁发的国际性认可奖项,在我看来更多是对阿里安全这些年持续在信息安全体系建设上的投入、努力担当以及成果的肯定,我只不过是作为代表来领了个奖,所有一切都是我的团队努力所得。”张玉东在香港接受表彰时表示。 网络安全形势严峻已成全球共识,业内人士认为,国内安全行业较之国外,面临的挑战正在不断增大。张玉东认为,首先是国内企业安全意识觉醒力度不够、安全投入不足;其次,很多企业的安全做法相对于瞬息万变的黑灰产形态,仍然比较传统;第三,安全行业参与各
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- SpringBoot2初体验,简单认识spring boot2并且搭建基础工程
- CentOS关闭SELinux安全模块
- Linux系统CentOS6、CentOS7手动修改IP地址
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7,8上快速安装Gitea,搭建Git服务器
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- Jdk安装(Linux,MacOS,Windows),包含三大操作系统的最全安装
- Red5直播服务器,属于Java语言的直播服务器
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Docker使用Oracle官方镜像安装(12C,18C,19C)