内容简介:相信大家在项目开发中,在页面较复杂的情况下,往往会遇到一个问题,就是在页面组件之间通信会非常困难。比如说一个商品列表和一个已添加商品列表:假如这两个列表是独立的两个组件,它们会共享一个数据 “
相信大家在项目开发中,在页面较复杂的情况下,往往会遇到一个问题,就是在页面组件之间通信会非常困难。
比如说一个商品列表和一个已添加商品列表:
假如这两个列表是独立的两个组件,它们会共享一个数据 “ 被选中的商品 ”,在 商品列表
选中一个商品,会影响 已添加商品列表
,在 已添加列表
中删除一个商品,同样会影响 商品列表
的选中状态。
它们两个是兄弟组件,在没有数据流框架的帮助下,在组件内数据有变化的时候,只能通过父组件传输数据,往往会有 onSelectedDataChange
这种函数出现,在这种情况下,还尚且能忍受,如果组件嵌套较深的话,那痛苦可以想象一下,所以才有解决数据流的各种框架的出现。
本质分析
我们知道 React 是 MVC
里的 V
,并且是数据驱动视图的,简单来说,就是 数据 => 视图
,视图是基于数据的渲染结果:
V = f(M) 复制代码
数据有更新的时候,在进入渲染之前,会先生成 Virtual DOM ,前后进行对比,有变化才进行真正的渲染。
V + ΔV = f(M + ΔM) 复制代码
数据驱动视图变化有两种方式,一种是 setState
,改变页面的 state
,一种是触发 props
的变化。
我们知道数据是不会自己改变,那么肯定是有“外力”去推动,往往是远程请求数据回来或者是 UI
上的交互行为,我们统称这些行为叫 action
:
ΔM = perform(action) 复制代码
每一个 action
都会去改变数据,那么视图得到的 数据(state)
就是所有 action
叠加起来的变更,
state = actions.reduce(reducer, initState) 复制代码
所以真实的场景会出现如下或更复杂的情况:
问题就出在,更新数据比较麻烦,混乱,每次要更新数据,都要一层层传递,在页面交互复杂的情况下,无法对数据进行管控。
有没有一种方式,有个集中的地方去管理数据,集中处理数据的 接收 , 修改 和 分发 ?答案显然是有的,数据流框架就是做这个事情,熟悉 Redux
的话,就知道其实上面讲的就是 Redux
的核心理念,它和 React
的数据驱动原理是相匹配的。
数据流框架
Redux
数据流框架目前占主要地位的还是 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
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
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
的原因。
xredux
但是我最终没有选择使用 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
去托管,甚至有人所有状态都有组件托管,也是存在的,因为页面太简单,根本就不需要用到数据流框架。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 大数据技术 DataPipeline在大数据平台的数据流实践
- DataPipeline在大数据平台的数据流实践
- 我对前后端数据模型和数据流的理解
- stream – 数据流处理
- 浅谈hdfs架构与数据流
- 我所认识的前端数据流
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
JavaScript修炼之道
波顿纽威 / 巩朋、张铁 / 人民邮电 / 2011-11 / 29.00元
《JavaScript修炼之道》是JavaScript的实战秘籍。作者将自己多年的编程经验融入其中,不仅可以作为学习之用,更是日常JavaScript开发中不可多得的参考手册,使读者少走很多弯路。《JavaScript修炼之道》的内容涵盖了当今流行的JavaScript库的运行机制,也提供了许多应用案例。《JavaScript修炼之道》针对各任务采取对页式编排,在对各任务的讲解中,左页解释了任务的......一起来看看 《JavaScript修炼之道》 这本书的介绍吧!
HTML 编码/解码
HTML 编码/解码
HSV CMYK 转换工具
HSV CMYK互换工具