Redux的全家桶与最佳实践

2019独角兽企业重金招聘Python工程师标准>>> hot3.png

04203537_ak6W.png
image.png


Redux 的第一次代码提交是在 2015 年 5 月底(也就是一年多前的样子),那个时候 React 的最佳实践还不是明晰,作为一个 View 层,有人会用 backbone 甚至是 angular 和它搭配,也有人觉得这层 View 功能已经足够强大,简单地搭配一些 utils 就直接上。后来便有了 FLUX 的演讲,React 社区开始注意到这种新的类似函数式编程的理念,Redux 也作为 FLUX 的一种变体开始受到关注,再后来顺理成章地得到 React 的『钦点』,作者也加入了 Facebook 从事 React 的开发。生态圈经过了这一年的成熟,现在很多第三方库已经非常完善,所以这里想介绍一下目前 Redux 的一些最佳实践。

1. 复习一下 Redux 的基本概念

首先我们复习一下 Redux 的基本概念, 如果你已经很熟悉了,就直接跳过这一章吧。

Redux 把界面视为一种状态机,界面里的所有状态、数据都可以由一个状态树来描述。所以对于界面的任何变更都简化成了状态机的变化:

(State, Input) => NewState

这其中切分成了三个阶段:

  1. action
  2. reducer
  3. store

所谓的 action,就是用一个对象描述发生了什么,Redux 中一般使用一个纯函数,即 actionCreator 来生成 action 对象。

// actionCreator => action
// 这是一个纯函数,只是简单地返回 action
function somethingHappened(data){return {type: 'foo',data: data}
}

随后这个 action 对象和当前的状态树 state 会被传入到 reducer 中,产生一个新的 state

//reducer(action, state) => newState
function reducer(action, state){switch(action.type){case 'foo':return { data: data };default:return state;}
}

store 的作用就是储存 state,并且监听其变化。
简单地说就是你可以这样产生一个 store :

import { createStore } from 'redux'
//这里的 reducer 就是刚才的 Reducer 函数
let store = createStore(reducer);

然后你可以通过 dispatch 一个 action 来让它改变状态:

store.getState();//{}
store.dispatch(somethingHappened('aaa'));
store.getState(); // { data: 'aaa'}

好了,这就是 Redux 的全部功能。对的,它就是如此简单,以至于它本体只有 3KB 左右的代码,因为它只是实现了一个简单的状态机而已,任何稍微有点编程能力的人都能很快写出这个东西。至于和 React 的结合,则需要 react-redux 这个库,这里我们就不讲怎么用了。

2. Redux的一些痛点

大体上,Redux 的数据流是这样的:

界面 => action => reducer => store => react => virtual dom => 界面

每一步都很纯净,看起来很美好对吧?对于一些小小的尝试性质的 DEMO 来说确实很美好。但其实当应用变得越来越大的时候,这其中存在诸多问题:

  1. 如何优雅地写异步代码?(从简单的数据请求到复杂的异步逻辑)
  2. 状态树的结构应该怎么设计?
  3. 如何避免重复冗余的 actionCreator?
  4. 状态树中的状态越来越多,结构越来越复杂的时候,和 react 的组件映射如何避免混乱?
  5. 每次状态的细微变化都会生成全新的 state 对象,其中大部分无变化的数据是不用重新克隆的,这里如何提高性能?

你以为我会在下面一一介绍这些问题是怎么解决的?还真不是,这里大部分问题的回答都可以在官方文档中看到: 技巧 | Redux 中文文档 ,文档里讲得已经足够详细(有些甚至详细得有些啰嗦了)。所以下面只挑 Redux 生态圈里几个比较成熟且流行的组件来讲讲。

3. Redux 异步控制

官方文档里介绍了一种很朴素的异步控制中间件 redux-thunk (如果你还不了解中间件的话请看 Middleware | Redux 中文文档 ,事实上 redux-thunk 的代码很简单,简单到只有几行代码:

function createThunkMiddleware(extraArgument) {return ({ dispatch, getState }) => next => action => {if (typeof action === 'function') {return action(dispatch, getState, extraArgument);}return next(action);};
}

它其实只干了一件事情,判断 actionCreator 返回的是不是一个函数,如果不是的话,就很普通地传给下一个中间件(或者 reducer);如果是的话,那么把 dispatchgetStateextraArgument 作为参数传入这个函数里,实现异步控制。

比如我们可以这样写:

//普通action
function foo(){return {type: 'foo',data: 123}
}//异步action
function fooAsync(){return dispatch => {setTimeout(_ => dispatch(123), 3000);}
}

但这种简单的异步解决方法在应用变得复杂的时候,并不能满足需求,反而会使 action 变得十分混乱。

举个比较简单的例子,我们现在要实现『图片上传』功能,用户点击开始上传之后,显示出加载效果,上传完毕之后,隐藏加载效果,并显示出预览图;如果发生错误,那么显示出错误信息,并且在2秒后消失。

用普通的 redux-thunk 是这样写的:

function upload(data){return dispatch => {// 显示出加载效果dispatch({ type: 'SHOW_WAITING_MODAL' });// 开始上传api.upload(data).then(res => {// 成功,隐藏加载效果,并显示出预览图dispatch({ type: 'PRELOAD_IMAGES', data: res.images });dispatch({ type: 'HIDE_WAITING_MODAL' });}).catch(err => {// 错误,隐藏加载效果,显示出错误信息,2秒后消失dispatch({ type: 'SHOW_ERROR', data: err });dispatch({ type: 'HIDE_WAITING_MODAL' });setTimeout(_ => dispatch({ type: 'HIDE_ERROR' }), 2000);})}
}

这里的问题在于,一个异步的 upload action 执行过程中会产生好几个新的 action,更可怕的是这些新的 action 也是包含逻辑的(比如要判断是否错误),这直接导致异步代码中到处都是 dispatch(action) ,是很不可控的情况。如果还要进一步考虑取消、超时、队列的情况,就更加混乱了。

所以我们需要更强大的异步流控制,这就是 GitHub - yelouafi/redux-saga: An alternative side effect model for Redux apps 。下面我们来看看如果换成 redux-saga 的话会怎么样:

import { take, put, call, delay } from 'redux-saga/effects'
// 上传的异步流
function *uploadFlow(action) {// 显示出加载效果yield put({ type: 'SHOW_WAITING_MODAL' });// 简单的 try-catchtry{const response = yield call(api.upload, action.data);yield put({ type: 'PRELOAD_IMAGES', data: response.images });yield put({ type: 'HIDE_WAITING_MODAL' });}catch(err){yield put({ type: 'SHOW_ERROR', data: err });yield put({ type: 'HIDE_WAITING_MODAL' });yield delay(2000);yield put({ type: 'HIDE_ERROR' });}     
}function* watchUpload() {yield* takeEvery('BEGIN_REQUEST', uploadFlow)
}

是不是规整很多呢?redux-saga 允许我们使用简单的 try-catch 来进行错误处理,更神奇的是竟然可以直接使用 delay 来替代 setTimeout 这种会造成回调和嵌套的不优雅的方法。

本质上讲,redux-sage 提供了一系列的『副作用(side-effects)方法』,比如以下几个:

  1. put (产生一个 action)
  2. call (阻塞地调用一个函数)
  3. fork (非阻塞地调用一个函数)
  4. take (监听且只监听一次 action)
  5. delay (延迟)
  6. race (只处理最先完成的任务)

并且通过 Generator 实现对于这些副作用的管理,让我们可以用同步的逻辑写一个逻辑复杂的异步流。

下面这个例子出自于 官方文档 ,实现了一个对于请求的队列,即让程序同一时刻只会进行一个请求,其它请求则排队等待,直到前一个请求结束:

import { buffers } from 'redux-saga';
import { take, actionChannel, call, ... } from 'redux-saga/effects';function* watchRequests() {// 1- 创建一个针对请求事件的 channelconst requestChan = yield actionChannel('REQUEST');while (true) {// 2- 从 channel 中拿出一个事件const {payload} = yield take(requestChan);// 3- 注意这里我们使用的是阻塞的函数调用yield call(handleRequest, payload);}
}function* handleRequest(payload) { ... }

更多关于 redux-saga 的内容,请参考 Read Me | redux-saga (中文文档: 自述 | Redux-saga 中文文档 )。

4. 提高 selector 的性能

把 react 与 redux 结合的时候,react-redux 提供了一个极其重要的方法: connect ,它的作用就是选取 redux store 中的需要的 state 与 dispatch , 交由 connect 去绑定到 react 组件的 props 中:

import { connect } from 'react-redux';
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'// 我们需要向 TodoList 中注入一个名为 todos 的 prop
// 它通过以下这个函数从 state 中提取出来:
const mapStateToProps = (state) => {// 下面这个函数就是所谓的selectortodos: state.todos.filter(i => i.completed)// 其它props...
}const mapDispatchToProps = (dispatch) => {onTodoClick: (id) => {dispatch(toggleTodo(id))}
}// 绑定到组件上
const VisibleTodoList = connect(mapStateToProps,mapDispatchToProps
)(TodoList)export default VisibleTodoList

在这里需要指定哪些 state 属性被注入到 component 的 props 中,这是通过一个叫 selector 的函数完成的。

上面这个例子存在一个明显的性能问题,每当组件有任何更新时都会调用一次 state.todos.filter 来计算 todos ,但我们实际上只需要在 state.todos 变化时重新计算即可,每次更新都重算一遍是非常不合适的做法。下面介绍的这个 reselect 就能帮你省去这些没必要的重新计算。

你可能会注意到, selector 实际上就是一个『 纯函数』

selector(state) => some props

而纯函数是具有可缓存性的,即对于同样的输入参数,永远会得到相同的输出值 (如果对这个不太熟悉的同学可以参考 JavaScript函数式编程 ,reselect 的原理就是如此,每次调用 selector 函数之前,它会判断参数与之前缓存的是否有差异,若无差异,则直接返回缓存的结果,反之则重新计算:

import { createSelector } from 'reselect';var state = {a: 100
}var naiveSelector = state => state.a;// mySelector 会缓存输入 a 对应的输出值
var mySelector = createSelector(naiveSelector, a => {console.log('做一次乘法!!!');return a * a;}
)console.log(mySelector(state));    // 第一次计算,需要做一次乘法
console.log(mySelector(state));    // 输入值未变化,直接返回缓存的结果
console.log(mySelector(state));    // 同上
state.a = 5;                            // 改变 a 的值
console.log(mySelector(state));    // 输入值改变,做一次乘法
console.log(mySelector(state));    // 输入值未变化,直接返回缓存的结果
console.log(mySelector(state));    // 同上

上面的输出值是:

做一次乘法!!!
10000
10000
10000
做一次乘法!!!
25
25
25

之前那个关于 todos 的范例可以这样改,就可以避免 todos 数组被重复计算的性能问题:

import { createSelector } from 'reselect';
import { connect } from 'react-redux';
import { toggleTodo } from '../actions'
import TodoList from '../components/TodoList'const todoSelector = createSelector(state => state.todos,todos => todos.filter(i => i.completed)
)const mapStateToProps = (state) => {todos: todoSelector// 其它props...
}const mapDispatchToProps = (dispatch) => {onTodoClick: (id) => {dispatch(toggleTodo(id))}
}// 绑定到组件上
const VisibleTodoList = connect(mapStateToProps,mapDispatchToProps
)(TodoList)export default VisibleTodoList

更多可以参考 GitHub - reactjs/reselect: Selector library for Redux

5. 减少冗余代码

redux 中的 action 一般都类似这样写:

function foo(data){return {type: 'FOO',data: data}
}//或者es6写法:
var foo = data => ({ type: 'FOO', data})

当应用越来越大之后,action 的数量也会大大增加,为每个 action 对象显式地写上 type 和 data 或者其它属性会造成大量的代码冗余,这一块是完全可以优化的。

比如我们可以写一个最简单的 actionCreator:

function actionCreator(type){return function(data){return {type: type,data: data}}
}var foo = actionCreator('FOO');
foo(123); // {type: 'FOO', data: 123}

redux-actions 就可以为我们做这样的事情,除了上面这种朴素的做法,它还有其它比较好用的功能,比如它提供的 createActions 方法可以接受不同类型的参数,以产生不同效果的 actionCreator,下面这个范例来自官方文档:

import { createActions } from 'redux-actions';const { actionOne, actionTwo, actionThree } = createActions({// 函数类型ACTION_ONE: (key, value) => ({ [key]: value }),// 数组类型ACTION_TWO: [(first) => first,               // payload(first, second) => ({ second }) // meta],// 最简单的字符串类型
}, 'ACTION_THREE');actionOne('key', 1));
//=>
//{
//  type: 'ACTION_ONE',
//  payload: { key: 1 }
//}actionTwo('Die! Die! Die!', 'It\'s highnoon~');
//=>
//{
//  type: 'ACTION_TWO',
//  payload: ['Die! Die! Die!'],
//  meta: { second: 'It\'s highnoon~' }
//}actionThree(76);
//=>
//{
//  type: 'ACTION_THREE',
//  payload: 76,
//}

更多可以参考 GitHub - acdlite/redux-actions: Flux Standard Action utilities for Redux.

转载于:https://my.oschina.net/cllgeek/blog/1584693

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/539926.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

php二分查找算法时间复杂度,一个运用二分查找算法的程序的时间复杂度是什么...

一个运用二分查找算法的程序的时间复杂度是“对数级别”。二分查找是一种效率较高的查找方法,算法复杂度即是while循环的次数,时间复杂度可以表示“O(h)O(log2n)”。本教程操作环境:windows7系统、Dell G3电脑。一个运用二分查找算法的程序的…

Android MediaPlayer使用方法简单介绍

1)如何获得MediaPlayer实例: 可以使用直接new的方式:MediaPlayer mp new MediaPlayer();也可以使用create的方式,如:MediaPlayer mp MediaPlayer.create(this, R.raw.test);//这时就不用调用setDataSource了* 需要在…

oracle基本的操作命令,oracle命令基本操作

--创建表空间create tablespace TBS_OTHERS datafile G:\APP\ORCL\ORADATA\ORCL\TBS_OTHERS01.dbf size 1000m;-- 创建用户create user C##JHGL identified by jhgl default tablespace TBS_OTHERScreate user C##YJYJHGL identified by jhgl default tablespace TBS_OTHERScre…

将不确定变为确定~头压缩是否有必要,MVC如何实现头压缩

网页的头部压缩在页面体积大的情况下非常有必要做,它会使页面体积有一个明显的减小,同时加到网页从服务端下载到客户端的速度,以下是我做的一个测试: 没有使用头压缩时: 使用了头压缩后: 我们可以看到&…

android .9.png ”点九” 图片制作方法

“点九”是andriod平台的应用软件开发里的一种特殊的图片形式,文件扩展名为:.9.png 智能手机中有自动横屏的功能,同一幅界面会在随着手机(或平板电脑)中的方向传感器的参数不同而改变显示的方向,在界面改变方向后,界面上的图形会因为长宽的变化而产生拉伸…

servlet3.0异步处理

Servlet3是Tomcat7出现的新特性,所以需要先安装tomcat7 微信企业号使用回调模式时: 假如企业无法保证在五秒内处理并回复,可以直接回复空串,企业号不会对此作任何处理,并且不会发起重试。这种情况下,可以…

使用svn diff的-r参数的来比较任意两个版本的差异

1 svn diff的用法1.1 对比当前本地的工作拷贝文件(working copy)和缓存在.svn下的版本库文件的区别[plain] view plaincopyprint? svn diff 1.2 对比当前本地的工作拷贝文件(working copy)和任意版本A的差异[plain] view plaincopyprint? svn diff -rA 比如,以下…

深入理解HTTP Session

session在web开发中是一个非常重要的概念,这个概念很抽象,很难定义,也是最让人迷惑的一个名词,也是最多被滥用的名字之一,在不同的场合,session一次的含义也很不相同。这里只探讨HTTP Session。为了说明问题…

Hibernate的懒加载session丢失解决方法

在web.xml加入spring提供的过滤器&#xff0c;延长session的生命周期 <!--Hibernate的懒加载session丢失解决方法 --><filter><filter-name>openSessionInView</filter-name><filter-class>org.springframework.orm.hibernate4.support.OpenSess…

Linux访问其他进程空间,Linux环境进程间通信系列(五):共享内存

共享内存可以说是最有用的进程间通信方式&#xff0c;也是最快的IPC形式。两个不同进程A、B共享内存的意思是&#xff0c;同一块物理内存被映射到进程A、B各自的进程地址空间。进程A可以即时看到进程B对共享内存中数据的更新&#xff0c;反之亦然。由于多个进程共享同一块内存区…

冲刺NO.8

Alpha冲刺第八天 站立式会议 项目进展 项目稳步进行&#xff0c;项目的基础部分如基本信息管理&#xff0c;信用信息管理等部分已相对比较完善。 问题困难 技术困难在短期内很难发生质的变化&#xff0c;而本项目由于选择了队员不太熟悉的程序框架&#xff0c;所以所以项目的交…

linux由众多微内核组成,什么是linux

大家对Linux这个词比较陌生吧&#xff0c;那么Linux是什么呢&#xff1f;Linux是什么Linux是一种自由和开放源码的类Unix操作系统。目前存在着许多不同的Linux&#xff0c;但它们都使用了Linux内核。Linux可安装在各种计算机硬件设备中&#xff0c;从手机、平板电脑、路由器和视…

浅析jQuery中常用的元素查找方法总结

$("#myELement") 选择id值等于myElement的元素&#xff0c;id值不能重复在文档中只能有一个id值是myElement所以得到的是唯一的元素 $("div") 选择所有的div标签元素&#xff0c;返回div元素数组 $(".myClass") 选择使用myClass类的css的所有…

右击菜单一键优化(增加新建office2003、新建reg和bat,删除新建公文包、新建wps、新建rar)...

右击菜单一键优化&#xff08;增加新建office2003、新建reg和bat&#xff0c;删除新建公文包、新建wps、新建rar&#xff09; Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROOT\.doc]"Word.Document.8""Content Type""application/msword&qu…

jquery获取select选择的显示值

转载自&#xff1a;http://blog.csdn.net/a5489888/article/details/8611703 本来以为jQuery("#select1").val();是取得选中的值&#xff0c; 那么jQuery("#select1").text();就是取得的文本。 这是不正确的&#xff0c;正确做法是&#xff1a; jQuery(&qu…

克隆整个linux系统环境的软件,开源的系统克隆工具 Clonezilla(再生龙)linux、UBUNTU备份不用愁...

Clonezilla是一个很好的系统克隆工具,它基于Partimage,吸取了Norton Ghost和Partition Image的优点。即不仅支持对整个系统进行克隆,而且也可以克隆单个的分区,这种灵活性可能更能适应备份者的需要。支持GNU/Linux的文件系统 ext2、ext3、reiserfs、xfs、jfs和Windows的FAT、FA…

SqlServer2008备份与还原(完整图示版)

一、备份 1、在需要备份的数据库上&#xff0c;右键——任务——备份&#xff0c;如下&#xff1a; 2、选择备份到哪个路径和备份名字&#xff1a; 点击“添加”&#xff0c;如下&#xff0c; 3、上面点击“确定”后&#xff0c;回到第一个页面&#xff0c;选中刚才添加的路径和…

Jquery mobile问题总汇

转载&#xff1a;http://www.wglong.com/main/artical!details?id4#q6 1页面缩放显示问题 问题描述&#xff1a; 页面似乎被缩小了&#xff0c;屏幕太宽了。 解决办法&#xff1a; 在head标签内加入&#xff1a; <meta name"viewport" content"widthdevice…

Linux通过文件大小查找,linux 根据文件大小查找文件

linux下的find命令用来查找文件&#xff0c;通过man find就知道它是无所不能的。所以按照文件大小来查找文件就不在话下。从man find搜索size&#xff0c;可以看到如下信息&#xff1a;-size n[cwbkMG]File uses n units of space. The following suffixes can be used:b for 5…

DBCP连接池介绍

DBCP连接池介绍 ----------------------------- 目前 DBCP 有两个版本分别是 1.3 和 1.4。 DBCP 1.3 版本需要运行于 JDK 1.4-1.5 &#xff0c;支持 JDBC 3。 DBCP 1.4 版本需要运行于 JDK 1.6 &#xff0c;支持 JDBC 4。 1.3和1.4基于同一套源代码&#xff0c;含有所有的bug修…