前言:在当下的前端界,react 和 redux 发展得如火如荼,react 在 github 的 star 数达 42000 +,超过了 jquery 的 39000+,也即将超过前几年比较火的angular 1 的 49000+;redux 的 star 数也要接近 20000,可见你们对其的热情程度,到底是什么魔力让你们为之疯狂呢?让咱们上车,亲自体验一波试试~~本文章偏向于讲解redux流程。css
宅印前端基于 react + redux 的模式开发,咱们指定了一套分工明确的并行开发流程。下面经过一个 “苹果篮子” 实例,来看看整个应用开发流程。html
很是感谢 @ckinmind 为本教程提供完整源码demo
完整源码: https://github.com/ckinmind/a...
实例体验 https://ckinmind.github.io/ap...
(摘苹果的过程模拟请求网络,体现一个异步动做,因此响应看起来有些延迟)
更优雅的方式:若是你想更优雅地使用 react + redux ,欢迎查看个人最新系列文章 pastate.js 响应式 react 框架。前端
首先,咱们来看看这个实例的原型:react
看到这个水果篮子的样子,你们应该能够明白它的功能:你能够作两件事 -- 摘苹果和吃苹果。当你摘苹果时,应用会向后台发送ajax请求索取苹果,每一个苹果有两个属性:编号和重量。当你吃苹果掉时,不用告诉后台,在前端偷偷吃掉就好啦~ 同时苹果篮子会显示当前的苹果量和已经吃掉的苹果量。好!那下面咱们来开工!jquery
下面先来整体了解一下 redux 应用的基本原理,一图胜千言:webpack
可见整个redux流程的逻辑很是清晰,数据流是单向循环的,就像一个生产的流水线:git
store(存放状态) -> container(显示状态) -> reducer (处理动做)-> store
这一点对精细化分工协做颇有好处。es6
咱们来看看这三个概念:github
NOTE:从对象的包含关系上讲,reducer 是store的一部分,但在逻辑上咱们把它分出来,这样会比较容易理解整个redux流程。web
咱们能够作个形象的比喻,把 js 比喻成巴士,把 store, container, reducer 比喻为三个车站,再把 state 和 action 比喻成两种乘客。这是一趟环路巴士:
js巴士
从 store车站
出发,载上 state乘客
,state乘客
到达某个 container车站
下车并把本身展现出来action乘客
上车了,js巴士
把 action乘客
送到 reducer车站
,在这里 action乘客
和 state乘客
生了一个孩子 new state
,js巴士把 new state
送回了 store车站
(好像是人生轮回→_→)redux 只是定义了应用的数据流程,只解决了 “数据层”(model layer) 的问题,通常还会使用 react, angular 等做为“显示层” (UI layer) 来一块儿使用,咱们项目采用 react 做为显示框架。
在开始以前,这里先提供一些介绍react和redux的参考资料,若是在下文遇到哪些点不理解,能够来这里翻看参考资料:
下文的展现的js代码,会用到少许简单的 es6 语法,能够在遇到时参考这里,或本身查找资料:
咱们会使用 babel 编译器把es6语法编译成es5, 因此你们没必要担忧浏览器兼容性问题,能够大胆使用es6。
应用的基础配置工做由前端开发主管负责,你们没必要详细理解。
按照开发的内容,咱们把前端团队分为两个小组: “布局组” 和 “逻辑组”,每一个小组有2种任务,一共4种任务。
布局组 - 负责 contianer、component 部分
逻辑组 - 负责 action、reducer 部分
布局组 要求对 HTML + CSS 布局比较熟悉,只须要会简单的 js 便可, 不须要完整地理解redux流程。
逻辑组 要求对 js 比较熟悉,最好能够比较完整地理解redux流程, 但基本不须要涉及HTML + CSS布局工做。
接下来,先给出咱们教程相关的 src 目录。这里你们能够先一扫而过,大概了解便可
- src 源码文件夹:包含项目源码,咱们基本都在这个文件夹下作开发 - containers 容器文件夹:存放容器组件,好比 “苹果篮子” - components 组件文件夹:存放普通显示组件,好比 “苹果” - actions actions文件夹:存放能够发出的action - reducers reducers文件夹:存放action的处理器reducers - services 服务文件夹:存放通过封装的服务,如 ajax服务, 模拟数据服务 - styles 样式文件夹:存放应用的样式,如css, scss - images 图片文件夹:存放图片资源 - apis 开发接口文件夹:存放开发接口文档
下面正式开始啦,先从布局组开始。
redux 的组件分为两类,一类是容器组件 container ,一类是普通显示组件 component。容器负责接收store中的state和并发送action, 大多数时候须要和store直接链接,容器通常不须要屡次使用,好比咱们这个应用的苹果篮子。普通组件放在容器里面,由容器肯定显示的时机、数量和内容,普通组件通常会屡次使用。
苹果篮子组件的原型以下:
对于容器,咱们使用 React 组件类 的方式书写,这里主要是静态的jsx代码,至关于html。
下面是 AppleBasket.jsx
import React from 'react'; import { connect } from 'react-redux'; class AppleBusket extends React.Component { render(){ return ( <div className="appleBusket"> <div className="title">苹果篮子</div> <div className="stats"> <div className="section"> <div className="head">当前</div> <div className="content">0个苹果,0克</div> </div> <div className="section"> <div className="head">已吃掉</div> <div className="content">2个苹果,480克</div> </div> </div> <div className="appleList"> <div className="empty-tip">苹果篮子空空如也</div> </div> <div className="btn-div"> <button>摘苹果</button> </div> </div> ); } } export default connect()(AppleBusket);
同时静态布局开发人员还要负责书写样式,宅印的样式使用sass 书写, 下面是的例子是appleBasket.scss , 你们能够作参考:
.appleBusket{ width: 400px; margin: 20px; border-radius: 4px; border: 1px solid #ddd; >.title{ padding: 6px 0px; text-align: center; color: #069; font-size: 20px; //font-weight: 600; } >.stats{ width: 100%; $border: 1px dashed #ddd; border-top: $border; border-bottom: $border; padding: 10px 0px; .section{ display: inline-block; width: 50%; padding-left: 8px; .head{ padding: 6px 0px; font-size: 16px; color: #069; } .content{ font-size: 20px; font-weight: 200; } &:first-of-type{ border-right: 1px solid #f0f0f0; } } } >.appleList{ padding: 10px 0px; .empty-tip{ text-align: center; font-size: 16px; color: #ccc; padding: 20px 0px; } } >.btn-div{ text-align: center; button{ color: #fff; background-color: #096; border: none; font-size: 14px; padding: 6px 40px; border-radius: 6px; margin: 20px auto; } } }
苹果组件的样子以下:
普通组件的定义方法和容器相似,只是其不须要使用redux链接器来封装,以下:
AppleItem.jsx
import React from 'react'; class AppleItem extends React.Component { render() { return ( <div className="appleItem"> <div className="apple"><img src="../images/apple.png" alt=""/></div> <div className="info"> <div className="name">红苹果 - 1号</div> <div className="weight">265克</div> </div> <div className="btn-div"><button>吃掉</button></div> </div> ); } } export default AppleItem;
样式文件 appleItem.scss 在此省略。
哪些显示元素要做为容器,哪些要做为普通组件,没有百分之百肯定划分规则,你们根据本身的理解把它划分到某一类便可。
这些就是任务一的内容,书写静态布局,基本都是html+css工做,不须要涉及js代码。
写完了静态布局后,接下来要进行动态渲染啦~
动态渲染听起来很高大上,其实意思就是根据一个stete数据对象来显示内容而已。首先须要肯定其state的结构。容器的state 是 store 中state的一部分,前端管理员会事先约定好其数据结构,并在对应的reducer中给出,只要去那里复制一份出来便可。普通组件的state只要本身定义便可,并在文件中说明清楚。
下面看看“苹果篮子”的动态布局,咱们去 appleBasketReducer.js 能够获得水果篮子的 state 的结构以下:
{ isPicking : false, newAppleId: 1, apples: [ /*{ id: 0, weight: 235, isEaten: false }*/ ] }
咱们这个数据结构把它 “实例化”,以下这样放在咱们改为写的 AppleBasket.jsx 中,而后咱们开始书写咱们的动态渲染代码啦,以下:
import React from 'react'; import { connect } from 'react-redux'; import AppleItem from '../components/AppleItem'; class AppleBusket extends React.Component { render() { let { state } = this.props; //这部分从对应的 appleBasketReducer.js 中拷贝 let mockState = { isPicking : false, newAppleId: 3, apples: [ { id: 1, weight: 235, isEaten: true }, { id: 2, weight: 256, isEaten: false } ] }; //是否开启模拟数据的开关,注释这行代码关闭模拟数据 state = mockState; //对 state 作显示级别的转化 let stats = { appleNow: { quantity: 0, weight: 0 }, appleEaten: { quantity: 0, weight: 0 } }; state.apples.map(apple => { let selector = apple.isEaten ? 'appleEaten':'appleNow'; stats[selector].quantity ++; stats[selector].weight += apple.weight; }) return ( <div className="appleBusket"> <div className="title">苹果篮子</div> <div className="stats"> <div className="section"> <div className="head">当前</div> <div className="content"> {stats.appleNow.quantity}个苹果, {stats.appleNow.weight}克 </div> </div> <div className="section"> <div className="head">已吃掉</div> <div className="content"> {stats.appleEaten.quantity}个苹果, {stats.appleEaten.weight}克 </div> </div> </div> <div className="appleList"> { state.apples.map(apple => <AppleItem state ={apple} />) } </div> <div className="btn-div"> <button>摘苹果</button> </div> </div> ); } } function select(state) { return { state: state.appleBusket } } export default connect(select)(AppleBusket);
可见,动态布局的工做要求只是在 HTML + CSS 布局的基础上,再加上 JSX 语法能力便可。
普通显示组件的动态渲染和容器相似,只是这里的state能够本身规定,并给出示例的mockState(模拟state),使用组件的人按照示例传入数据便可使用。
AppleItem.jsx 的更新以下:
import React from 'react'; class AppleItem extends React.Component { shouldComponentUpdate(nextProps) { return nextProps.state != this.props.state; } render() { let { state, actions } = this.props; /** * 这个区域是 mock 数据区,也做为组件文档,请书写清楚 * //在组件发布时,请注释掉,提升性能 */ let mockState = { id: 1, weight: 256, isEaten: false }; let mockActions = { eatApple : id => console.log('eatApple',id) }; /** * 开关这行代码,用于切换装入的数据来源。(为了开关的方便,请把两句代码合成一行) * 在开发阶段打开,使用内部 state 和 action, 开发完成后请注释关闭 */ state = mockState; actions = mockActions; if (state.isEaten) return null; return ( <div className="appleItem"> <div className="apple"><img src="../images/apple.png" alt=""/></div> <div className="info"> <div className="name">红苹果 - {state.id}号</div> <div className="weight">{state.weight}克</div> </div> <div className="btn-div"><button onClick={() => actions.eatApple(state.id) }>吃掉</button></div> </div> ); } } export default AppleItem;
容器和普通显示组件state的对比:
容器的state咱们是从store中的总state直接得到的,注意 AppleBusket.jsx 靠后面这段代码:
function select(state) { return { state: state.appleBusket } }
select是一个state筛选器, 功能是选择总state中的 appleBusket 做为本容器的state。而这个state的格式咱们会在其对应的reducer中规定(由于咱们须要在reducer中提供对应state的默认值)
普通显示组件的state格式由组件开发人员本身约定便可,并在mockState 区域给出例子。当别人要使用你的显示组件时,必须根据你规定的格式传入state数据。在组件里面,咱们通常会实现以下这样一个自动切换器,当组件的使用者在使用该组件时没有传入state, 就会显示内部的模拟state,为使用者带来预览效果。
if(state === undefined) state = mockState;
以上就是布局组的开发工做: 静态布局 + 动态布局。前者只须要会html+css布局便可,后者还要会一些jsx的语法和基本的js语法。
任务内容:开发 redux 流程的 action,并把action部署到对应容器和组件中。
技能要求:须要对js比较熟悉,并要求要会使用jq的ajax功能。
首先,咱们先来看看 action 的定义。
1. 狭义的 action
狭义的action是指一个简单的对象,对象的结构以下,只要在对象内包含type属性指明action的名称便可,同时,在对象的其余属性里,能够以任何形式自由保存其余相关数据。
let action = { type: 'ACTION_NAME', ... }
通常 type 的内容使用 大写字母+下划线 的格式。
在这个定义的基础上,业界提出以一种标准 action, 叫作 Flux Standard Action , 该标准下的action除了type属性以外,只容许多加(不是必须包含)这三个属性:payload,error,meta。
let FSA = { type: 'ACTION_NAME', payload: <bool | number | string | object>, //action的负载,能够是数据或 error 对象 error: <bool>, // 指明该action是不是一个以 error 为负载的action meta: <string> // action元数据, 包含解释该action含义的信息 }
咱们宅印约定都要使用 Flux Standard Action,下面是吃苹果 action:
let FSA = { type: 'EAT_APPLE', payload: 3, // 负载是3, 说明吃掉3号苹果, 这里也能够写成 { id : 3 } meta: 'This action will eat an apple!' // (不是必须的) }
一个action只是一个对象,他须要根据时机被 store 的 dispatch 函数调用才会开始进行处理:store.dispatch(action_1)
。
2. 广义的 action
广义的 action 是指在中间件的支持下,dispatch 函数能够调用的数据类型,除了普通action以外,常见的有 thunk, promise 等。咱们用经常使用的 thunk来举个例子。
thunk 其实就是一个代码片断,能够简单理解为一种特定的函数,咱们能够dispatch 这个thunk。 thunk函数具备以下的签名
(dispatch, getState) => { //在函数体内可使用 dispatch 方法来发射其余 action //在函数体内可使用 getState 方法来获取当前的state }
下面是一个咱们摘苹果动做的例子:
let pickAppleAction = (dispatch, getState) => { ajax({ url: '/pickApple', method: 'GET', }).done(data => { //发射普通 action dispatch({ type: 'DONE_PICK_APPLE', payload: data.weight // 或者 payload: {weight: data.weight} }); }).fail(xhr => { //发射普通 action, 其负载是一个error dispatch({ type: 'FAIL_PICK_APPLE', payload: new Error(xhr.responseText) , error: true }); }) }
定义好以后,咱们能够直接这样调用这个thunk:
dispatch( pickAppleAction )
接下来,咱们来作进一步优化,把action统一为actionCreator , 咱们不难察觉,每次都要书写{ type: 'ACTION_NAME' ... } 是很麻烦也很容易出错的,actionCreator 就是为解决这个问题而生的,actionCreator 就是一个产生特定action(这里指广义的action)的函数,下面来看简单的actionCreator 例子:
//传统写法 var eatApple = function(id) { return { type: 'EAT_APPLE', payload: id } } // es6 写法 let eatApple = id => ({ type: 'EAT_APPLE', payload: id })
这样一来,一方面是使用起来比较简单方便,另外一方面是具备文档做用。
只须要这样发射action就能够啦:
dispatch(eatApple(3))
普通action的actionCreator封装工做, 可使用 redux-actions 自动完成, 查看其文档就能够快速上手,能够根据状况使用。
在项目中,咱们会为每一个板块写一个的action文件,并统一使用actionCreator, 因此最终 appleAction.js 以下:
import ajax from '../services/ajax'; //通过封装的增强型 ajax 函数 //这是名空间,对普通action作划分 const prefix = 'apple/'; let actions = { //注意这里须要 () => ... , 否则 pickAppleAction 不是一个actionCreator, 而是一个thunk pickApple: () => (dispatch, getState) => { //若是正在摘苹果,则结束这个thunk, 不执行摘苹果 if(getState().isPicking) return; //通知开始摘苹果 dispatch(actions.beginPickApple()); //发送摘苹果请求 ajax({ url: '/appleBasket/pickApple', method: 'GET' }).done(data => { dispatch(actions.donePickApple(data.weight)) }) .fail(xhr => { dispatch(actions.failPickApple(xhr.responseText)); }) }, beginPickApple: () => ({ type: 'apple/BEGIN_PICK_APPLE' }), donePickApple: appleWeight => ({ type: 'apple/DONE_PICK_APPLE', payload: appleWeight }), failPickApple: errMsg => ({ type: 'apple/FAIL_PICK_APPLE', payload: new Error(errMsg), error: true }), eatApple: appleId => ({ type: 'apple/EAT_APPLE', payload: appleId }) }; export default actions;
这样一来,只要引入 appleAction.js ,就能够快速使用定义好的action,结合某些编辑器的智能提示功能,很是方便,下面是 vsCode 编辑器的效果:
写好了action以后,只要在 container 的对应位置装上action就行了, 下面是appleBasket.jsx 整体代码:
import React from 'react'; import { connect } from 'react-redux'; import AppleItem from '../components/AppleItem'; import actions from '../actions/appleActions'; class AppleBusket extends React.Component { render() { let { state, dispatch } = this.props; ... return ( ... <div className="appleList"> { state.apples.map(apple => <AppleItem state ={apple} actions={{eatApple: id => dispatch(actions.eatApple(id))}} key={apple.id} /> ) } </div> <div className="btn-div"> <button onClick={() => dispatch(actions.pickApple())}>摘苹果</button> </div> ... ) } } function selectState(state) { return { state: state.appleBusket } } export default connect(selectState)(AppleBusket);
注意这两行。就是装入action的地方
actions={{eatApple: id => dispatch(actions.eatApple(id))}} <button onClick={() => dispatch(actions.pickApple())}>摘苹果</button>
上面代码中引入的actions实际上是actionCreators。
此外,actionCreator还有很简洁的用法:对actionCreator作dispatch级别的封装,这个过程咱们可使用 redux 提供的 bindActionCreators 函数自动完成。而后就能够直接调用action,而不须要使用dispatch方法去调用actionCreator。看下面更新后的代码:
import React from 'react'; import { bindActionCreators } from 'redux'; import { connect } from 'react-redux'; import AppleItem from '../components/AppleItem'; import actions from '../actions/appleActions'; class AppleBusket extends React.Component { render() { let { state, actions} = this.props; ... return ( ... <div className="appleList"> { state.apples.map(apple => <AppleItem state ={apple} actions={{eatApple: actions.eatApple}} key={apple.id} /> ) } </div> <div className="btn-div"> <button onClick={actions.pickApple}>摘苹果</button> </div> ... ) } } function selectState(state) { return { state: state.appleBusket } } function buildActionDispatcher(dispatch) { return { actions: bindActionCreators(actions, dispatch) } } export default connect(selectState, buildActionDispatcher)(AppleBusket);
注意这三个变更:
let { state, actions } = this.props; actions={{eatApple: actions.eatApple }} <button onClick={actions.pickApple}>摘苹果</button>
咱们对比一下以前的写法:
let { state, dispatch } = this.props actions={{eatApple: id => dispatch(actions.eatApple(id))}} <button onClick={() => dispatch(actions.pickApple())}>摘苹果</button>
能够发现使用新的方式使代码简洁了不少!
可是,这对于有对象属性提示功能编辑器来讲,这种方式会使编辑器没法分析对象属性:
这时,须要一边看actions文件对该actions对象的定义,一边在目标位置填入action,不过这也不是很麻烦。并且对于使用没有对象属性提示的编辑器的开发者来讲,这个 drawback 根本就不存在。咱们相对推荐使用这种通过dispatch封装的action, 但不要求,你们根据本身的状况使用便可。
对于普通显示组件
对于普通显示组件的actions传递方式,咱们统一使用actions属性传递,以下:
AppleBasket.jsx
... <AppleItem state ={apple} actions={{eatApple: actions.eatApple }} key={apple.id} /> ...
AppleItem.jsx
... <button onClick={() => actions.eatApple(state.id)}>吃掉</button> ...
普通显示组件使用统一actions属性接受父级的action,能够在组件内部创建mockActions, 这个mockActions 既有文档功能,也有测试功能,很是实用:
let mockActions = { eatApple : id => console.log('eatApple',id), //指定了函数的签名 foo: (arg1,arg2) => console.log('foo',arg1,arg2) //也响应了调用测试 }; /** * 开关这行代码,用于切换装入的state和actions来源。(为了开关的方便,请把两句代码合成一行) * 在开发阶段打开,使用内部 state 和 action, 开发完成后请注释关闭 */ state = mockState; actions = mockActions;
点击 “摘苹果” 和 “吃掉” 按钮,咱们看看控制台,已经能够发出咱们想要的action啦:
好啦,actions 开发的内容就介绍到这里。咱们来总结一下咱们对action所作的封装:
action -> actionCreator -> actionDispatcher
开发内容: reducer的其实就是action的处理器。其开发的内容很明确清晰,就是开发一类函数,接受action 和 当前的state,返回新的state。
技术要求:要求对js比较熟悉,须要会使用 immutable.js 这个数据静态化库。
下面是reducer功能的图解:
咱们先看看咱们苹果板块的state的数据结构,很是简单,这里是某个时刻的状态:
{ isPicking : false, newAppleId: 1, apples: [ { id: 0, weight: 235, isEaten: false } ] }
有三个一级属性:
咱们上面说起actions分为广义的action和狭义的普通action。其实,非普通action, 如thunk,通常会以发起普通action结束。咱们reducer只须要处理狭义上的普通action,。在咱们摘苹果应用里,总共有这4个普通action:
//通知store应用开始摘苹果 beginPickApple: () => ({ type: 'apple/BEGIN_PICK_APPLE' }), //摘苹果成功 donePickApple: appleWeight => ({ type: 'apple/DONE_PICK_APPLE', payload: appleWeight }), //摘苹果失败 failPickApple: error => ({ type: 'apple/FAIL_PICK_APPLE', payload: error, error: true }), //吃苹果 eatApple: appleId => ({ type: 'apple/EAT_APPLE', payload: appleId })
下面是根据action,写出的 reducer 的基本结构:
(state = defaultState, action) => { switch (action.type) { case 'apple/BEGIN_PICK_APPLE': //TODO return state; case 'apple/DONE_PICK_APPLE': //TODO return state; case 'apple/FAIL_PICK_APPLE': //TODO return state; case 'apple/EAT_APPLE': //TODO return state; default: return state; } };
咱们能够看到,reducer是一个函数,接受state和action两个参数,在函数内部,根据 action.type 来肯定要作哪些操做,而且每种操做都要返回state(或者是新的,或者是原来的)。
咱们以 apple/EAT_APPLE
动做为例,讲解如何书写reducer。EAT_APPLE 动做的含义是吃苹果,咱们能够很是简单地处理这个动做:直接把对应苹果对象的 isEaten 属性设为true便可。
按照通常的思惟,咱们会这样处理:
... case 'apple/EAT_APPLE': state.apples[action.payload].isEaten = true; return state; ...
可是,这种方法在 redux 应用里看不到做用,由于这种写法不会使store触发react进行从新渲染,为何呢?由于 newState == oldState
! 下面咱们来作一些解释:
首先,要先从js对象的相等判断运算提及,咱们看下面的代码
let a = { foo: 'bar'}; let b = { foo: 'bar'}; console.log( a == b ); //结果是 false
a 和 b 看起来同样,但为何是false呢?由于对象和数组的赋值是引用赋值, a 和 b 只是一个引用符号,其所指向的对象实体不一样(好比 a -> object#001, b -> object#002),js的对象(数组)相等判断是根据是否指向同一个对象实体来的肯定的 (object#001 ?= object#002 // false),详见 MDN。
再看看下面的例子:
let a = {foo: 'bar'}; let b = a; b.foo = 'good'; console.log( a == b ); //结果是 true
如今应该能够理解刚才为何newState == oldState
了吧~
redux 是根据返回的state是否改变来决定是否通知 react 更新的。根据这种状况所,可能有人会这样改进刚才的reducer:
state.apples[action.payload].isEaten = true; newState = Object.assign({},state); return newState;
这样一来,点击 “吃掉”按钮,看到了有效果了,苹果不见了!可是,这种写法只是迎合了redux更新视觉组件的触发条件,还具备很大的局限性,不是咱们redux规定的reducer。下面咱们来看看正确的reducer:
首先,reducer有这样的重要约束:在reducer里,不能够修改原来的state,须要保持使每一个版本的state不变。
这种保持数据不变(Persistent data structure)的方式在函数式编程(Functional programming)很是常见。在咱们的redux应用里,其意义在于:
1. 调试意义:保持每一个版本的state的不变性,使得咱们能够跟踪每一个时刻的state, 跟踪应用的“发展史”,这个特性为调试带来了很大的方便。
2. 性能意义:保持state不变这个约束引导咱们使用局部更新对象的方法,这样会能够很是有效地提升react或其余显示框架的渲染效率。咱们先来看看为了保持数据不变性,要怎么对state作更新,以咱们的苹果篮子state为例:
例子:通知开始摘苹果:apple/BEGIN_PICK_APPLE
为了保证每一个版本的state不变性,咱们有两种实现方式:“深复制”,“浅复制”。咱们来看两种模式的内部原理:
深复制方式:有人会这样想:“保持state的不变性很容易,只须要深复制一个state, 让后在新的state要怎么修改就怎么修改,不ok了吗?”,以下就是深复制
这种方式是一种很低级保持不变性的方式:
它只是简单迎合保持数据不变性的约束,虽然有必定调试意义,可是,不但没有提升程序的性能,反而下降了程序的整体性能!没有实践意义。
浅复制方式:浅复制模式只对内部数据发生变化的引用作更新,以下
“state” 对象的内部数据发生变化,因此建立新的state引用;而apples array 内部数据不发生变化,因此就不对该引用作更新!在这个操做中,这种浅复制的方法运行效率比较高,并且其简单地实现了数据不变性,为调试带来方便,同时,也是更重要的,这种浅复制的方式极大地提升了视觉组件渲染阶段的运行效率!咱们来对比一下:当用户点击摘苹果时,若是使用“深复制”,渲染程序须要从新遍历整个state对象树来作视觉更新,而使用浅复制来实现数据不变性时,渲染程序只须要遍历state对象的一级子节点便可,而不须要对apples array 作遍历,性能大大地提升。尤为是当苹果不少的时候,两种方式的性能差距是很是明显的。
备注:在react组件里面,要开启条件更新这个生命周期函数 shouldComponentUpdate, 才会对把这个性能提升点释放出来,相似这样:
... shouldComponentUpdate(nextProps) { return nextProps.state != this.props.state; } ...
下面咱们再给出 “吃苹果” reducer 进行浅复制的例子:
如今你们应该理解了用浅复制实现数据不变性的原理和意义了,下面咱们来看具体的代码实现。
咱们的代码用 es6 编写,这里要用到 es6 两个很是方便的特性:
你们能够稍微看一下文档,或者看我下面的例子就知道其用法了:
// apple basket reducer export default (state = { isPicking: false, newAppleId: 1, apples: [ { id: 0, weight: 235, isEaten: false } ] }, action) => { let newState ; switch (action.type) { case 'apple/BEGIN_PICK_APPLE': newState = Object.assign({}, state, { isPicking: true }); return newState; case 'apple/DONE_PICK_APPLE': newState = Object.assign({}, state, { apples: [ ...state.apples, { id: state.newAppleId, weight: action.payload, isEaten: false } ], newAppleId: state.newAppleId + 1, isPicking: false }) return newState; case 'apple/FAIL_PICK_APPLE': //这里只是简单处理 newState = Object.assign({}, state, { isPicking: false }); return newState; case 'apple/EAT_APPLE': newState = Object.assign({}, state, { apples: [ ...state.apples.slice(0, action.payload), Object.assign({}, state.apples[action.payload], { isEaten: true }), ...state.apples.slice(action.payload + 1) ] }) return newState; default: return state; } };
你们会发现这种浅复制操做在开发的过程会复杂一点,因而就有了 immutable.js 这个专门处理不变性数据的库(也是facebook出品),它可使用相似赋值的方式生成浅复制的不变性数据,下面来看看它怎么简化咱们的开发:
咱们用 apple/EAT_APPLE
这个reducer来讲明。
这是原来的 reducer:
... case 'apple/EAT_APPLE': newState = Object.assign({}, state, { apples: [ ...state.apples.slice(0, action.payload), Object.assign({}, state.apples[action.payload], { isEaten: true }), ...state.apples.slice(action.payload + 1) ] }) return newState; ...
这是使用 immutable.js 库的reducer :
import { fromJS } from 'immutable'; ... case 'apple/EAT_APPLE': return fromJS(state).setIn(['apples',action.payload,'isEaten'], true).toJS(); ...
用了immutable.js后,轻松一行代码搞定!若是团队约定 state 都用 immutable 内部的数据类型,就能够连 fromJS 和 toJS 的转化都省了,超级方便!
到这里, reducer 任务的介绍就结束啦~
至此,咱们四个任务都介绍完了,你们应该对redux流程有必定概念了,咱们来回顾一下咱们的4个任务:
这样子,咱们经过流程化把 react + redux 的主要流程都定义好了,这种模式的可构建性很高,能够构建很是复杂的单页面应用,不会由于应用的业务复杂性增长而增长开发复杂性。
而且在这种分工里面,布局组对专一于写样式布局,大可能是基本的HTML+CSS 工做;逻辑组专一于开发应用逻辑,基本都是JS工做,分工获得很是明确的规划,人们能够更好地作精本身负责的工做,各个部件的耦合性很低,人员安排灵活性比较大。
这就是咱们用苹果篮子实例讲解的react+redux开发流程,你们造成redux流程基本的概念就好,具体的理解仍是要在实践中慢慢参透。
一些依赖的JS库没有在这里给你们介绍,你们能够在后面的相关js库中查看。
很是感谢 @ckinmind 为本教程提供完整源码demo
完整源码: https://github.com/ckinmind/a...
实例体验 https://ckinmind.github.io/ap...
(摘苹果的过程模拟请求网络,体现一个异步动做,因此响应看起来有些延迟)
项目相关js库列表:
感谢您的阅读,但愿这篇文章对你们有帮助,欢迎回复和讨论。