相信你们在项目开发中,在页面较复杂的状况下,每每会遇到一个问题,就是在页面组件之间通讯会很是困难。ios
好比说一个商品列表和一个已添加商品列表:git
假如这两个列表是独立的两个组件,它们会共享一个数据 “被选中的商品”,在商品列表
选中一个商品,会影响已添加商品列表
,在已添加列表
中删除一个商品,一样会影响商品列表
的选中状态。github
它们两个是兄弟组件,在没有数据流框架的帮助下,在组件内数据有变化的时候,只能经过父组件传输数据,每每会有 onSelectedDataChange
这种函数出现,在这种状况下,还尚且能忍受,若是组件嵌套较深的话,那痛苦能够想象一下,因此才有解决数据流的各类框架的出现。redux
咱们知道 React 是 MVC
里的 V
,而且是数据驱动视图的,简单来讲,就是数据 => 视图
,视图是基于数据的渲染结果:axios
V = f(M)
复制代码
数据有更新的时候,在进入渲染以前,会先生成 Virtual DOM,先后进行对比,有变化才进行真正的渲染。api
V + ΔV = f(M + ΔM)
复制代码
数据驱动视图变化有两种方式,一种是 setState
,改变页面的 state
,一种是触发 props
的变化。bash
咱们知道数据是不会本身改变,那么确定是有“外力”去推进,每每是远程请求数据回来或者是 UI
上的交互行为,咱们统称这些行为叫 action
:markdown
ΔM = perform(action)
复制代码
每个 action
都会去改变数据,那么视图获得的数据(state)
就是全部 action
叠加起来的变动,app
state = actions.reduce(reducer, initState)
复制代码
因此真实的场景会出现以下或更复杂的状况:框架
问题就出在,更新数据比较麻烦,混乱,每次要更新数据,都要一层层传递,在页面交互复杂的状况下,没法对数据进行管控。
有没有一种方式,有个集中的地方去管理数据,集中处理数据的接收,修改和分发?答案显然是有的,数据流框架就是作这个事情,熟悉 Redux
的话,就知道其实上面讲的就是 Redux
的核心理念,它和 React
的数据驱动原理是相匹配的。
数据流框架目前占主要地位的仍是 Redux,它提供一个全局 Store
处理应用数据的接收,修改和分发。
它的原理比较简单,View
里面有任何交互行为须要改变数据,首先要发一个 action
,这个 action
被 Store
接收并交给对应的 reducer
处理,处理完后把更新后的数据传递给 View
。Redux
不依赖于任何框架,它只是定义一种方式控制数据的流转,能够应用于任何场景。
虽然定义了一套数据流转的方式,但真正使用上会有很多问题,我我的总结主要是两个问题:
咱们来看看写一个数据请求的例子,这是很是典型的案例:
actions.js
export const FETCH_DATA_START = 'FETCH_DATA_START'; export const FETCH_DATA_SUCCESS = 'FETCH_DATA_SUCCESS'; export const FETCH_DATA_ERROR = 'FETCH_DATA_ERROR'; export function fetchData() { return dispatch => { dispatch(fetchDataStart()); axios.get('xxx').then((data) => { dispatch(fetchDataSuccess(data)); }).catch((error) => { dispatch(fetchDataError(error)); }); }; } export function fetchDataStart() { return { type: FETCH_DATA_START, } } ...FETCH_DATA_SUCCESS ...FETCH_DATA_ERROR 复制代码
reducer.js
import { FETCH_DATA_START, FETCH_DATA_SUCCESS, FETCH_DATA_ERROR } from 'actions.js'; export default (state = { data: null }, action) => { switch (action.type) { case FETCH_DATA_START: ... case FETCH_DATA_SUCCESS: ... case FETCH_DATA_ERROR: ... default: return state } } 复制代码
view.js
import { createStore, applyMiddleware } from 'redux'; import thunk from 'redux-thunk'; import reducer from 'reducer.js'; import { fetchData } from 'actions.js'; const store = createStore(reducer, applyMiddleware(thunk)); store.dispatch(fetchData()); 复制代码
第一个问题,发一个请求,由于须要托管请求的全部状态,因此须要定义不少的 action
,这时很容易会绕晕,就算有人尝试把这些状态再封装抽象,也会充斥着一堆模板代码。有人会挑战说,虽然一开始是比较麻烦,繁琐,但对项目可维护性,扩展性都比较友好,我不太认同这样的说法,目前还算简单,真正业务逻辑复杂的状况下,会显得更恶心,效率低且阅读体验差,相信你们也写过或看过这样的代码,后面本身看回来,须要在 actions
文件搜索一下 action
的名称,reducer
文件查询一下,绕一圈才慢慢看懂。
第二个问题,按照官方推荐使用 redux-thunk 实现异步 action
的方法,只要在 action
里返回一个函数便可,这对有强迫症的人来讲,简直受不了,actions
文件显得它很不纯,原本它只是来定义 action
,却居然要夹杂着数据请求,甚至 UI
上的交互!
我以为 Redux
设计上没有问题,思路很是简洁,是我很是喜欢的一个库,它提供的数据的流动方式,目前也是获得社区的普遍承认。然而在使用上有它的缺陷,虽然是能够克服,可是它自己难道没有能够优化的地方?
dva 的出来就是为了解决 redux
的开发体验问题,它首次提出了 model
的概念,很好地把 action
、reducers
、state
结合到一个 model
里面。
model.js
export default { namespace: 'products', state: [], reducers: { 'delete'(state, { payload: id }) { return state.filter(item => item.id !== id); }, }, }; 复制代码
它的核心思想就是一个 action
对应一个 reducer
,经过约定,省略了对 action
的定义,默认 reducers
里面的函数名称即为 action
的名称。
在异步 action
的处理上,定义了 effects(反作用)
的概念,与同步 action
区分起来,内部借助了 redux-saga 来实现。
model.js
export default { namespace: 'counter', state: [], reducers: { }, effects: { *add(action, { call, put }) { yield call(delay, 1000); yield put({ type: 'minus' }); }, }, }; 复制代码
经过这样子的封装,基本保持 Redux
的用法,咱们能够沉浸式地在 model
编写咱们的数据逻辑,我以为已经很好地解决问题了。
不过我我的喜爱问题,不太喜欢使用 redux-saga
这个库来解决异步流,虽然它的设计很巧妙,利用了 generator
的特性,不侵入 action
,而是经过中间件的方式进行拦截,很好地将异步处理隔离出独立的一层,而且以此声称对实现单元测试是最友好的。是的,我以为设计上真的很是棒,那时候还特地阅读了它的源码,赞叹做者真的牛,这样的方案都能想出来,可是后来我看到还有更好的解决方案(后面会介绍),就放弃使用它了。
mirrorx 和 dva
差很少,只是它使用了单例的方式,全部的 action
都保存了 actions
对象中,访问 action
有了另外一种方式。还有就是处理异步 action
的时候可使用 async/await
的方式。
import mirror, { actions } from 'mirrorx' mirror.model({ name: 'app', initialState: 0, reducers: { increment(state) { return state + 1 }, decrement(state) { return state - 1 } }, effects: { async incrementAsync() { await new Promise((resolve, reject) => { setTimeout(() => { resolve() }, 1000) }) actions.app.increment() } } }); 复制代码
它内部处理异步流的问题,相似 redux-thunk
的处理方式,经过注入一个中间件,这个中间件里判断 当前 action
是否是异步 action
(只要判断是否是 effects
里定义的 action
便可),若是是的话,就直接中断了中间件的链式调用,能够看看这段代码。
这样的话,咱们 effects
里的函数就可使用 async/await
的方式调用异步请求了,其实不是必定要使用 async/await
,函数里的实现没有限制,由于中间件只是调用函数执行而已。
我是比较喜欢使用 async/await
这种方式处理异步流,这是我不用 redux-saga
的缘由。
可是我最终没有选择使用 mirrorx
或 dva
,由于用它们就捆绑一堆东西,我以为不该该作成这样子,为啥好好的解决 Redux
问题,最后变成都作一个脚手架出来?这不是强制消费吗?让人用起来就会有限制。了解它们的原理后,我本身参照写了个 xredux 出来,只是单纯解决 Reudx
的问题,不依赖于任何框架,能够看做只是 Redux
的升级版。
使用上和 mirrorx
差很少,但它和 Redux
是同样的,不绑定任何框架,能够独立使用。
import xredux from "xredux"; const store = xredux.createStore(); const actions = xredux.actions; // This is a model, a pure object with namespace, initialState, reducers, effects. xredux.model({ namespace: "counter", initialState: 0, reducers: { add(state, action) { return state + 1; }, plus(state, action) { return state - 1; }, }, effects: { async addAsync(action, dispatch, getState) { await new Promise(resolve => { setTimeout(() => { resolve(); }, 1000); }); actions.counter.add(); } } }); // Dispatch action with xredux.actions actions.counter.add(); 复制代码
在异步处理上,其实也存在问题,可能你们也遇到过,就是数据请求有三种状态的问题,咱们来看看,写一个数据请求的 effects
:
import xredux from 'xredux'; import { fetchUserInfo } from 'services/api'; const { actions } = xredux; xredux.model({ namespace: 'user', initialState: { getUserInfoStart: false, getUserInfoError: null, userInfo: null, }, reducers: { // fetch start getUserInfoStart (state, action) { return { ...state, getUserInfoStart: true, }; }, // fetch error getUserInfoError (state, action) { return { ...state, getUserInfoStart: false, getUserInfoError: action.payload, }; }, // fetch success setUserInfo (state, action) { return { ...state, userInfo: action.payload, getUserInfoStart: false, }; } }, effects: { async getUserInfo (action, dispatch, getState) { let userInfo = null; actions.user.getUserInfoStart(); try { userInfo = await fetchUserInfo(); actions.user.setUserInfo(userInfo); } catch (e) { actions.user.setUserInfoError(e); } } }, }); 复制代码
能够看到,仍是存在不少感受没用的代码,一个请求须要3个 reducer
和1个 effect
,当时想着怎么优化,但没有很好的办法,后来我想到这3个 reducer
有个共同点,就是只是赋值,没有任何操做,那我内置一个 setState
的 reducer
,专门去处理这种只是赋值的 action
就行了。
最后变成这样:
import xredux from 'xredux'; import { fetchUserInfo } from 'services/api'; const { actions } = xredux; xredux.model({ namespace: 'user', initialState: { getUserInfoStart: false, getUserInfoError: null, userInfo: null, }, reducers: { }, effects: { async getUserInfo (action, dispatch, getState) { let userInfo = null; // fetch start actions.user.setState({ getUserInfoStart: true, }); try { userInfo = await fetchUserInfo(); // fetch success actions.user.setState({ getUserInfoStart: false, userInfo, }); } catch (e) { // fetch error actions.user.setState({ getUserInfoError: e, }); } } }, }); 复制代码
这个目前是本身比较满意的方案,在项目中也有实践过,写起来确实比较简洁易懂,不知你们有没有更好的办法。
使用了 Redux
,按道理应用中的状态数据应该都放到 Store
中,那组件是否能有本身的状态呢?目前就会有两种见解:
Store
中托管,全部组件都是纯展现组件。Store
托管。这两种就是分别对应贫血组件和充血组件,区别就是组件是否有本身的逻辑,仍是说只是纯展现。我以为这个问题不用去争论,没有对错。
理论上固然是说贫血组件好,由于这样保证数据是在一个地方管理的,可是付出的代价多是沉重的,使用了这种方式,每每到后面会有想死的感受,一种想回头又不想放弃的感受,其实不必这么执着。
相信你们几乎都是充血组件,有一些状态只与组件相关的,由组件去托管,有些状态须要共享的,交给 Store
去托管,甚至有人全部状态都有组件托管,也是存在的,由于页面太简单,根本就不须要用到数据流框架。
在 React
开发中不可避免会遇到数据流的问题,如何优雅地处理目前也没有最完美的方案,社区也存在各类各样的方法,能够多思考为何是这样作,了解底层原理比盲目使用别人的方案更重要。
若是想详细了解 xredux 如何在 React
中运用,可使用 RIS 初始化一个 Standard 应用看看,以前的文章《RIS,建立 React 应用的新选择》 有简单提过,欢迎你们体验。