这不是源码解读哦!!!若是你但愿看到源码解析,那我想你随便 google 一下就有不少啦,固然 Redux 的源码自己也是简单易懂。推荐直接阅读~ 的源码自己也是简单易懂,欢迎直接查看源码。php
做者: 赵玮龙html
终于要更新了,第二篇文章就这样在清明节前给你们赶出来,但愿你也能有个能够充实本身的假期。此次分享下这个已经很老的前端技术栈(相对于前端发展速度来看),说他老并无说他的设计理念老,而是说它已经有本身的历史印记了。还记得第一次redux发版已是2015年6月的事情了,其实它也经历了不少过往才是如今咱们看到的样子哦。。。前端
在深挖历史以前先看看这个 lib 是干吗的,是时候仔细研读下这个 Motivation 了,若是你尚未真正使用过Redux,那我建议你能够看下文档前四章节: 动机,原理,三个原则,以及和其余理念的对比~react
若是你已经使用过能够直接跳过到正片了,没有用过的能够听我简单在这里啰嗦两句~ 按照做者的理念,由于咱们日渐复杂的前端逻辑和代码,而且愈来愈多的框架和库使用这个概念git
UI=f(data)
复制代码
包括前端spa的流行咱们日渐复杂的项目逻辑会有愈来愈难控制的 state, 也就是公式中的 data,而每每这些问题都来源于 data 自己可变的数据(mutation)和异步化(asynchronicity),做者把这两种问题的混合效应类比了这个实验曼妥思和可乐,我在我家厕所试过。。劝你千万不要尝试。。。结局每每是爆炸的局面!!!那么这个lib就是用来规约这个状态让他可控的。(每每有人说Redux是一个全局状态管理模块,我我的以为不尽然,它提供给咱们所谓的规约以致于让咱们的状态可控,它确实会维护一个惟一的状态state,而且全部的data都在这里,可是它是否是全局的却不是必须的!)github
从 release 最先的0.2.0到如今的4.0.0咱们能够看到做者 Dan gaearon(也是我我的比较欣赏的开源做者之一)的心路历程,最先的 Redux 可不是如今的样子哦(虽然我也是从3.1.0才开始使用~)sql
最先的版本(遥想那时候 React 正在倡导本身的 Flux 单向数据流, Github 也有各类本身基于这个理念实现的类型库) Redux 也是基于 Flux 理念去实现的,在1.0.0以前 Redux 自己还涵盖了现在 react-redux 库的内容,自行封装了相似于 Connector, Provider 等高阶组件去完成 Redux 和 React 的衔接。(做者的目的也显而易见,但愿你能无痛的在 React 中去使用 Redux)。那时候的 Flux 库大多高举 functional programming 的大旗,由于那时候这种向函数式编程借鉴的 Flux 概念自己也是这么想的,咱们前面提到过让一切数据流向包括逻辑可控,这偏偏是 functional programming 里 prue function 的概念,这也和 React 当年作jsx语法的初衷一致。可是一旦抛出这样的理论就须要你的受众群体去接受这个概念数据库
+--------+ +------------+ +-------+ +----+
| Action | +----------->+ Dispatcher +----------->+ Store +----------->+View|
+--------+ +-------+----+ +-------+ +-+--+
^ |
| |
| |
| |
+----------------------------------------+
复制代码
咱们能够发现数据流向是单向的,这就是 Flux 核心理念,而 Redux 确实是遵循了这个理念可是又有些不一样,不一样在哪呢?编程
+----------+
| | +-----------+ sliceState,action +------------+
| Action |dispacth(action)| +--------------------------------> |
| +----------------> store | | Reducers |
| Creators | | <--------------------------------+ |
| | +-----+-----+ (state, action) => state +------------+
+-----^----+ |
| |
| |subscribe
| |
| |
| |
| +--------+ |
+--------------+ View <---+
+--------+
复制代码
咱们能够从图中看到没有了 Dispatcher 反而多了一个 Reducers,这里不得不提一个点就是全部的 Flux 架构都围绕着所谓 Predictable(可预测的) 的概念来维护 state, 那么如何作到可预测的也就是咱们必须保证咱们的 state Immutable(数据不可变), Flux 里依靠 dispatcher 来分发保证 Entity 的不可变性,而 Redux 中是依靠 pure function 的概念来保证每次的 state 都是原始 state 的一个快照, 也是这个核心公式的实践 (state, action) => state 这样你的 Reducers 其实就是这样的任意多个 function,如何拆分这些 function 就是你须要考虑的事情了。而若是是 pure function 的话也利于咱们去作函数复用和单元测试。这就是 Redux 向函数式编程的概念借鉴的理念, 若是你熟悉 Elm 你必定知道 Model 的概念,要更新一个 Model 而且映射到 view 上你须要有 updater 去更新你的 Model,这里 Redux 借鉴了 updater 的概念去作 reducers 拆分和复用。若是你对 Elm 也感兴趣能够看这里。redux
其实函数式编程的理念也贯穿到了源码中,好比里面 compose 和 middleware 的实现,这些你均可以参考源码,有意思的是其实纵使连原做者在一些函数式编程概念上也会有没意识到的地方,在一些实现上也遵循了一些pr的意见,好比 compose 的实现:
从最先期的 reduceRight 改为 reduce 这点就能发现,迭代了三个大版本和多个小版本的做者依然没有意识到从右向左执行函数居然能够不用 reduceRight,感兴趣的同窗能够试验下,我当时看到这个pr也是惊讶这个提出者的 Lisp 或者 Haskell 功底啊,才能有这样的直觉!! (其实函数式编程确实是能够锻炼逻辑思惟模式和你的数学意识,可是真的仅此而已,并不会在所谓性能和可读性上带来什么明显提高) 为了功能的完整和解耦性,以前的版本严重耦合 React 也作出了调整,把上面提到的通讯高阶组件单独提到 react-redux 库单独维护了,这样 Redux 自己也更加纯净的作状态管理这件事。
在回顾了前世以后,咱们来看看现在的 Redux, 在基于多个版本的迭代和你们的实践事后,不管是从概念自己仍是从最佳实践的案例来看,包括 Github 上一些基于 Redux 作的封装都已经有了默认的最佳实践和使用规范,那咱们来看看今天的 Redux 自己使用的场景和方式。 从上面的理念咱们看出来如何拆分 reducer 和维护那个单一不可变 state 是咱们使用 redux 最应该关注的事情。 咱们下面主要说下在 React 中使用 Redux 的最佳实践方式: (现实应用场景中,咱们现在大多数人应该仍是使用 Redux+React 的开发方式, 若是你仍是对于 Redux 是个初学者那么你应该看这里)。 为了讨论的具备必定的官方性,咱们按照官方文档来看下(我会在我认为比较我的的想作出备注和阐述), 着重讨论如下三方面:
为何先说 reducer 呢? 由于其实咱们的 state 都是 reducer 组成的, 上面那张图能够看出 (state, action) => state 是计算出 state 的规约公式, createStore() 这个 api 也是接受你的 reducer 来生成 state 的。 咱们先来看看最外层咱们须要为 state 生成 Initinalizing state 方式:
// 官网说无非两种方式
// 最外层你有一个reducer:
function rootReducer(state = 0, action) { // 在你的createStore第二个参数没有的状况下,你是须要给state一个默认值
switch (action.type) {
case 'INCREMENT': return state + 1;
case 'DECREMENT': return state - 1;
default: return state;
}
}
// 经过官方提供的combineReducer去生成这个rootReducer, 其实你观看源码的话这个方法return的仍是一个 (state, action) => {}的函数
function a(state = 'zwl', action) { // 在你的createStore第二个参数没有的状况下,你是须要给state一个默认值
return state;
}
function b(state = 'zwt', action) { // 在你的createStore第二个参数没有的状况下,你是须要给state一个默认值
return state;
}
const rootReducer = combineReducers({ a, b })
复制代码
既然初始化咱们看到上面提到的规约公式能够初始化你的 state, 另外一个数据流向是反向的, reducer 会从你的 state 拿到须要处理的 sliceState,这里就须要翻开书看看官方文档是怎么提这个所谓 state 的范式处理状态的, 文档会从三个地方提到这个 state 自己的规约处理,分别是
固然我以为做者已经说的很清楚了,文章尾部也给了不少连接,可是这里仍是有必要总结下这个规约的 state 范式化大概应该有些什么最佳实践:
// 首先先看下这里的 state 基本结构,固然文档中也没有限制你,鼓励你根据本身的业务形态去定制,可是倒是有些比较好的实践方式
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
// 区分领域的数据, 而且可能会有两种非领域数据类型,一种是页面上一些ui状态好比一个 button 是否展现的 boolean 值,这时候你会发现所谓的 sliceState 可能就是一个 domainData 或者是它下面的一个更小的分支,这个是根据你的 reducer 拆分规则指定的, 可是你能够想象下若是你的 data 是单纬数据结构或者简单数据结构,它就会很是好作逻辑计算,好比你有[a, b, c]单纬数组就比[{},{},{}]要好删查改除!
{
domainData1: {},
domainData2: {},
appState1: {},
appState2: {},
ui: {
uiState1: {},
uiState2: {},
}
}
// 通过网络上一些经验包括笔者本身的经验,你的基本数据类型每每会遵循一个数据原则为了尽量维护最小的单元的数据,数据共享的部分会放在一块儿维护,至于如何范式化这个 state 后面也会提到
{
domainData1: {},
domainData1ID: [],
domainData2: {},
domainData2ID: [],
entites:{ //这里存放你须要共享数据的部分,可是仅仅是实例, 这里的实例的引用每每放在外面, 遵循的原则是实例和引用分开而且若是实例里有
//引用domainData1里的东西那么其实引用的也是id,你会存一个引用的id进去
commonData1: {},
commonData2: {},
},
commonData1ID: [],
commonData2ID: [],
ui: {
uiState1: {},
uiState2: {},
}
}
复制代码
下面我来讲下范式化 state 这个问题:
// 文档中列举了一个博客数据的例子(固然其实这个数据结构已经挺复杂的了)
const blogPosts = [
{
id: "post1",
author: {username: "user1", name: "User 1"},
body: "......",
comments: [
{
id: "comment1",
author: {username: "user2", name: "User 2"},
comment: ".....",
},
{
id: "comment2",
author: {username: "user3", name: "User 3"},
comment: ".....",
}
]
},
{
id: "post2",
author: {username : "user2", name : "User 2"},
body: "......",
comments: [
{
id: "comment3",
author: {username : "user3", name : "User 3"},
comment: ".....",
},
{
id: "comment4",
author: {username : "user1", name : "User 1"},
comment: ".....",
},
{
id: "comment5",
author: {username : "user3", name : "User 3"},
comment: ".....",
}
]
}
// 重复不少遍
]
// 其实这里咱们能够想象一下,若是咱们须要更新这个数据结构,假如说直接把这个数据挂在 state 上。 那就会出现这种状况的代码[...state, {...slice[comments], ...sliceUpdater}]或者嵌套更深的更新方式,首先咱们知道不管是扩展运算符和Object.assign都是浅拷贝,咱们每每须要对嵌套结构每个层级都去更新,若是操做数据结构就更加不方便了咱们须要根据每一个层级找到相应嵌套比较深的数据结构而后进行操做。这也就是为何我前面说咱们尽可能维持单维度的数据结构缘由
// 文档中建议咱们拍平数据后获得这样的数据结构
{
posts: {
byId: {
"post1": {
id: "post1",
author: "user1",
body: "......",
comments: ["comment1", "comment2"]
},
"post2": {
id: "post2",
author: "user2",
body: "......",
comments: ["comment3", "comment4", "comment5"]
}
}
allIds: ["post1", "post2"]
},
comments: {
byId: {
"comment1": {
id: "comment1",
author: "user2",
comment: ".....",
},
"comment2": {
id: "comment2",
author: "user3",
comment: ".....",
},
"comment3": {
id: "comment3",
author: "user3",
comment: ".....",
},
"comment4": {
id: "comment4",
author: "user1",
comment: ".....",
},
"comment5": {
id: "comment5",
author: "user3",
comment: ".....",
},
},
allIds: ["comment1", "comment2", "comment3", "commment4", "comment5"]
},
users: {
byId: {
"user1": {
username: "user1",
name: "User 1",
}
"user2": {
username: "user2",
name: "User 2",
}
"user3": {
username: "user3",
name: "User 3",
}
},
allIds: ["user1", "user2", "user3"] // 这里官方推荐把相应的id放在层级内。这个地方其实均可以也能够像我前面提到的放在users平级的地方,这个取决你的项目具体而定
}
}
// 能够发现不一样数据之间都被打成平级的关系,不须要去处理深层嵌套结构的问题,在给定的ID里去删查改除都比较方便!这里更新的话也是不会波及到别的 domainComponent 好比咱们只是更新 users 里的信息只须要去更新 users > byId > user 这部分去作浅复制,它不会像上面那种嵌套数据结构总体更新影响别的相应渲染组件也去更新,这里其实还有一个优化点咱们后面会说,就是咱们在选择这个 sliceState 的时候, 从选择的 selector 不作重复运算。
复制代码
这里拍平方式建议采用Normalizr本身写也不是不行,可是状况会比较多,这个第三方库仍是能比较好的解决这个问题。这里再提一句这个 Normalizer 有一个 denormalize 方法便于你把 normaliz 的数据结构给装回去。是否是感受有点像范式数据库里的 join 表的过程呢? 若是你熟悉范式化数据库设计,你可能以为这有一点点范式化数据库的概念,只不过这里确实是没有严格的定义必须遵循第几范式设计,它最重要的是你须要找到适合你的范式结构,这里做者也在文档中去给出一些连接(固然你不必先去学习数据库的概念)能够简单了解下这些概念,包括多对多数据库设计:
既然前面提到 sliceState 须要有个 selector,从 state 中选择相应的 slice 这个分片(这里顺便把前面提到的小优化不须要作重复运算的 selector 也提一下,这里会用到这个库):
// 首先你的sliceState须要去state选择相应的分片大多时候你都是
const usersSelector = state.users
const commonsSelector = state.commons
// 可是你会发现有些值是经过两个selector计算而来的,咱们就拿reselect官网的第一个例子来看下
import { createSelector } from 'reselect'
const shopItemsSelector = state => state.shop.items
const taxPercentSelector = state => state.shop.taxPercent
const subtotalSelector = createSelector(
shopItemsSelector,
items => items.reduce((acc, item) => acc + item.value, 0)
)
const taxSelector = createSelector(
subtotalSelector,
taxPercentSelector,
(subtotal, taxPercent) => subtotal * (taxPercent / 100)
)
export const totalSelector = createSelector(
subtotalSelector,
taxSelector,
(subtotal, tax) => ({ total: subtotal + tax })
)
let exampleState = {
shop: {
taxPercent: 8,
items: [
{ name: 'apple', value: 1.20 },
{ name: 'orange', value: 0.95 },
]
}
}
console.log(subtotalSelector(exampleState)) // 2.15
console.log(taxSelector(exampleState)) // 0.172
console.log(totalSelector(exampleState)) // { total: 2.322 }
// 这里使用reselect的做用是若是下次传入的shopItemsSelector,taxPercentSelector 并无改变那么这个selector不会从新计算,这个你们有兴趣能够看下源码,自己源码也很少很容易看完!
复制代码
上面概念里提到了 selector 和 state 也能多少看到 state 自己只是可读(read only)并不可修改, 下面我来讲下咱们的函数 reducer 如何拆分,它的规约又是如何的(官方说有如下几种 reducer):
具体定义你能够参考这里, 在我看来也不尽然非要分的这么细,函数主要的做用仍是帮咱们拆分逻辑以及能达到复用的效果,因此拆分 reducer 才是核心的概念。 具体的拆分逻辑能够参考这里,我这里就不班门弄斧了,文档的案例足够清楚了。 这应该是我看到最上心的文档了。不得不说做者是一个用心且勤奋的人!!!
咱们这里就说一些特殊场景的 reducer 如何处理,固然文档里仍是说了在这里如何处理须要跨分片数据的 reducer,通俗点讲就是咱们须要 sliceStateA 的 reducer 须要处理 sliceStateB 里的数据:
// 第一种方式
// (还记得咱们开头说的 Initinalizing state 的方式吗? 下面两种方式就是利用这点)
function combinedReducer(state, action) { // 在 root 层去拿最外面的 state 把相应须要的 sliceState 传给相应须要的 reducer
switch(action.type) {
case "A_TYPICAL_ACTION": {
return {
a: sliceReducerA(state.a, action),
b: sliceReducerB(state.b, action)
};
}
case "SOME_SPECIAL_ACTION": {
return {
// 明确地把 state.b 做为额外参数进行传递
a: sliceReducerA(state.a, action, state.b),
b: sliceReducerB(state.b, action)
}
}
case "ANOTHER_SPECIAL_ACTION": {
return {
a: sliceReducerA(state.a, action),
// 明确地把所有的 state 做为额外参数进行传递
b: sliceReducerB(state.b, action, state)
}
}
default: return state;
}
}
// 第二种方式
const combinedReducer = combineReducers({
a: sliceReducerA,
b: sliceReducerB
});
function crossSliceReducer(state, action) {
switch(action.type) {
case "SOME_SPECIAL_ACTION": {
return {
// state.b是额外的参数
a: handleSpecialCaseForA(state.a, action, state.b),
b: sliceReducerB(state.b, action)
}
}
default: return state;
}
}
function rootReducer(state, action) {
const intermediateState = combinedReducer(state, action);
const finalState = crossSliceReducer(intermediateState, action);
return finalState;
}
// 这都是官方推荐的方法, 可是你会发现万变不离其中,都须要从根部 root 去拿 state 达到共享数据的方式,而且不管是 combineReducers 仍是 function 的方式都是要 Initinalizing state 的
复制代码
最后再来简单讨论下异步化的问题,首先在早期 Redux 版本源码里是兼顾了异步方案的,就是咱们所熟悉的 redux-thunk 固然跟 react-redux 被整理出来单独做为项目同样的,它也被单独整理出来只是在文档中说起了一下。其实市面上的基于 Redux 异步解决方案也很是多,解决不一样场景的 redux-thunk 应该就够了,可是还有很复杂的请求场景可能就须要下面两个如今比较流行的库去解决:
针对两个方案没有好坏之分,首先他们都解决了一样的问题,可是两个理念彻底不同。
用 Generator 去解决异步问题而且本身定义了不少 api 便于你解决各类复杂场景的异步问题 例如: [call, put, cancel,...] 不少种方法,关于这个 redux-saga 文论是官方文档仍是网络上的各类教程已经不少我就不在这废话了。
采用了 rx.js 的方式去解决异步问题,而 rx.js 这个库主要是 reactive programming 一种实现,它属于 reactivex 其中一个分支利用流概念解决异步编程问题。这是一个很是大的话题,咱们有机会也会开专题来讨论下这个 rx.js。虽然学习它自己会比 redux-saga 有更多的 api,可能还有一堆以前没有接触过的概念须要理解。可是就面向将来可能性上学习 rx.js 自己的价值确定会比 redux-saga 要有用的多。
不过笔者也会根据业务和团队来决定这个问题比较合理,若是算上学习成本和开发成本可能自己 redux-saga 更加适合大型项目和多人维护团队。因此具体哪一种方式更加适合你,就由你来定啦!
最后来看下官方推荐的一些项目目录作法,你在这里也能看到比较全的作法! 我比较推荐第一种作法: 分别定义 actions, reducers(里面有相应的 selector), constants(actionTypes), components, containers 这样我以为比较清晰。 说了这么多如今成熟的最佳实践。是否是该畅想下将来呢?
其实我在上一篇文章中也提到了 React 自己的核心理念应该是会兼容单向数据流的方式(由于新的 context api 的存在!) 若是你不熟悉这个 context 能够参考React blog 这里我只是畅想下,仅表明我的观点,不能表明将来任何发展趋势。
// 上一篇文章咱们利用 context 去实现 react-redux 的时候咱们利用 context 传递了 redux 自己的 store,具体的 provider 和 connect 能够参考上一篇文章
// 咱们本身实现的 store应该是这样的。(所有凭自我意淫。。。能够看个思路)
export const makeStore = (store) => {
let subscriptions = []
const Context = createContext()
const getState = () => store.initialState ? store.initialState : {} // 拿到当前的 state
const subscribe = fn => {
subscriptions = [...subscriptions, fn]
}
// 这里把 Provider 和 Connect 拿进来,他们俩分别使保存这里 store 和把 mapStateProps 以及 actions 传递进去
class Privider... // 一个维护Context.Provider 负责传递 store,更改store class Connect... // 一个负责消费的Context.Cousumer 传递给你的组件相应的state,和actions // 这里我还没想好如何维护总体代码结构。。 } 复制代码
在我准备发文章的时候,已经有人完成了这类库,那我就只能安利一波了。但愿你们能看到一个方向而不是全盘否决 Redux。 由于毕竟如今咱们尚未真正作好代替它的准备,并且我相信你若是真的要代替的话,在现有的项目和新项目可能都会有很多坑,不过俗话说得好不踩坑怎么进步呢?(欢迎你们多多踩坑哈哈哈哈!!!!)
咱们经历一门技术也好,经历一个技术时代革新也罢。其实每每最重要的是过程,若是咱们忽略过程只在意结果那么一切好像都是没有调味的菜----索然无味了,再回归到 Redux 自己,它给咱们带来的最多的是一种规约(若是你跟着文章读下来你应该会体会到!),如何在现在多人团队的项目中尽可能增长可读性和提升维护成本,也是工程化从来探讨的主题。固然所谓的最佳实践也不过是咱们真正实践事后从不管是后端也好别的行业也好借鉴那些咱们真正有用的知识加以改造。所谓举一反三的重要性吧!最后指望读者还能继续关注个人我的更新以及团队更新!!!愿在技术的浪潮中咱们共勉前行。