Skip to content

Commit

Permalink
finish 56
Browse files Browse the repository at this point in the history
  • Loading branch information
ascoders committed May 14, 2018
1 parent b8b88a8 commit f142ec1
Showing 1 changed file with 242 additions and 0 deletions.
242 changes: 242 additions & 0 deletions 56.精读《重新思考 Redux》.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
本周精读内容是 [《重新思考 Redux》](https://hackernoon.com/redesigning-redux-b2baee8b8a38)

## 1 引言

《重新思考 Redux》是 [rematch](https://github.com/rematch/rematch) 作者 [Shawn McKay](https://github.com/ShMcK) 写的一篇干货软文。

[dva](https://github.com/dvajs/dva) 之后,有许多基于 redux 的状态管理框架,但大部分都很局限,甚至是倒退。但直到看到了 rematch,总算觉得 redux 社区又进了一步。

这篇文章的宝贵之处在于,抛开 Mobx、RXjs 概念,仅针对 redux 做深入的重新思考,对大部分还在使用 redux 的工程场景非常有帮助。

## 2 概述

比较新颖的是,作者给出一个公式,评价一个框架或工具的质量:

`工具质量 = 工具节省的时间/使用工具消耗的时间`

如果这样评估原生的 redux,我们会发现,使用 redux 需要额外花费的时间可能超过了其节省下来的时间,从这个角度看,redux 是会降低工作效率的。

但 redux 的数据管理思想是正确的,复杂的前端项目也确实需要这种理念,为了更有效率的使用 redux,我们需要使用基于 redux 的框架。作者从 6 个角度阐述了基于 redux 的框架需要解决什么问题。

### 简化初始化

redux 初始化代码涉及的概念比较多,比如 `compose` `thunk` 等等,同时将 `reducer``initialState``middlewares` 这三个重要概念拆分成了函数方式调用,而不是更容易接受的配置方式:

```typescript
const store = preloadedState => {
return createStore(
rootReducer,
preloadedState,
compose(applyMiddleware(thunk, api), DevTools.instrument())
);
};
```

如果换成配置方式,理解成本会降低不少:

```typescript
const store = new Redux.Store({
instialState: {},
reducers: { count },
middlewares: [api, devTools]
});
```

> 笔者注:redux 的初始化方式非常函数式,而下面的配置方式就更面向对象一些。相比之下,还是面向对象的方式更好理解,毕竟 store 是一个对象。`instialState` 也存在同样问题,相比显示申明,将 `preloadedState` 作为函数入参就比较抽象了,同时 redux 对初始 state 的赋值也比较隐蔽,`createStore` 时统一赋值比较别扭,因为 reducers 是分散的,如果在 reducers 中赋值,要利用 es 的默认参数特性,看起来更像业务思考,而不是 redux 提供的能力。
### 简化 Reducers

redux 的 reducer 粒度太大,不但导致函数内手动匹配 `type`,还带来了 `type``payload` 等理解成本:

```typescript
const countReducer = (state, action) => {
switch (action.type) {
case INCREMENT:
return state + action.payload;
case DECREMENT:
return state - action.payload;
default:
return state;
}
};
```

如果用配置的方式设置 reducers,就像定义一个对象一样,会更清晰:

```typescript
const countReducer = {
INCREMENT: (state, action) => state + action.payload,
DECREMENT: (state, action) => state - action.payload
};
```

### 支持 async/await

redux 支持动态数据还是挺费劲的,需要理解高阶函数,理解中间件的使用方式,否则你不会知道为什么这样写是对的:

```typescript
const incrementAsync = count => async dispatch => {
await delay();
dispatch(increment(count));
};
```

为什么不抹掉理解成本,直接允许 `async` 类型的 action 呢?

```typescript
const incrementAsync = async count => {
await delay();
dispatch(increment(count));
};
```

> 笔者注:我们发现 rematch 的方式,dispatch 是 import 进来的(全局变量),而 redux 的 dispatch 是注入进来的,乍一看似乎 redux 更合理,但其实我更推崇 rematch 的方案。经过长期实践,组件最好不要使用数据流,项目的数据流只用一个实例完全够用了,全局 dispatch 的设计其实更合理,而注入 dispatch 的设计看似追求技术极致,但忽略了业务使用场景,导致画蛇添足,增加了不必要的麻烦。
### 将 action + reducer 改为两种 action

redux 抽象的 action 与 reducer 的指责很清晰,action 负责改 store 以外所有事,而 reducer 负责改 store,偶尔用来做数据处理。这种概念其实比较模糊,因为往往不清楚数据处理放在 action 还是 reducer 里,同时过于简单的 reducer 又要写 action 与之匹配,感觉过于形式化,而且繁琐。

重新考虑这个问题,我们只有两类 action:`reducer action``effect action`

* reducer action:改变 store。
* effect action:处理异步场景,能调用其他 action,不能修改 store。

同步的场景,一个 reducer 函数就能处理,只有异步场景需要 `effect action` 处理掉异步部分,同步部分依然交给 reducer 函数,这两种 action 职责更清晰。

### 不再显示申明 action type

不要在用一个文件存储 Action 类型了,`const ACTION_ONE = 'ACTION_ONE'` 其实重复写了一遍字符串,直接用对象的 key 表示 action 的值,再加上 store 的 name 为前缀保证唯一性即可。

同时 redux 建议使用 `payload` key 来传值,那为什么不强制使用 `payload` 作为入参,而要通过 `action.payload` 取值呢?直接使用 `payload` 不但视觉上减少代码数量,容易理解,同时也强制约束了代码风格,让建议真正落地。

### Reducer 直接作为 ActionCreator

redux 调用 action 比较繁琐,使用 `dispatch` 或者将 reducer 经过 `ActionCreator` 函数包装。为什么不直接给 reducer 自动包装 `ActionCreator` 呢?减少样板代码,让每一行代码都有业务含义。

最后作者给出了一个 `rematch` 完整的例子:

```typescript
import { init, dispatch } from "@rematch/core";
import delay from "./makeMeWait";

const count = {
state: 0,
reducers: {
increment: (state, payload) => state + payload,
decrement: (state, payload) => state - payload
},
effects: {
async incrementAsync(payload) {
await delay();
this.increment(payload);
}
}
};

const store = init({
models: { count }
});

dispatch.count.incrementAsync(1);
```

## 3 精读

我觉得本文基本上把 redux 存在的工程问题分析透彻了,同时还给出了一套非常好的实现。

### 细节的极致优化

首先是直接使用 `payload` 而不是整个 `action` 作为入参,加强了约束同时简化代码复杂度:

```typescript
increment: (state, payload) => state + payload;
```

其次使用 `async` 在 effects 函数中,使用 `this.increment` 函数调用方式,取代 `put({type: "increment"})`(dva),在 typescript 中拥有了类型支持,不但可以用自动跳转代替字符串搜索,还能校验参数类型,在 redux 框架中非常难得。

最后在 `dispatch` 函数,也提供了两种调用方式:

```typescript
dispatch({ type: "count/increment", payload: 1 });
dispatch.count.increment(1);
```

如果为了更好的类型支持,或者屏蔽 `payload` 概念,可以使用第二种方案,再一次简化 redux 概念。

### 内置了比较多的插件

rematch 将常用的 reselect、persist、immer 等都集成为了插件,相对比较强化插件生态的概念。数据流对数据缓存,性能优化,开发体验优化都有进一步施展的空间,拥抱插件生态是一个良好的发展方向。

比如 [rematch-immer](https://github.com/rematch/rematch/blob/master/plugins/immer/README.md) 插件,可以用 mutable 的方式修改 store:

```typescript
const count = {
state: 0,
reducers: {
add(state) {
state += 1;
return state;
}
}
};
```

但是当 state 为非对象时,immer 将不起作用,所以最好能养成 `return state` 的习惯。

最后说一点瑕疵的地方,`reducers` 申明与调用参数不一致。

### Reducers 申明与调用参数不一致

比如下面的 reducers:

```typescript
const count = {
state: 0,
reducers: {
increment: (state, payload) => state + payload,
decrement: (state, payload) => state - payload
},
effects: {
async incrementAsync(payload) {
await delay();
this.increment(payload);
}
}
};
```

定义时 `increment` 是两个参数,而 `incrementAsync` 调用它时,只有一个参数,这样可能造成一些误导,笔者建议保持参数对应关系,将 `state` 放在 `this` 中:

```typescript
const count = {
state: 0,
reducers: {
increment: payload => this.state + payload,
decrement: payload => this.state - payload
},
effects: {
async incrementAsync(payload) {
await delay();
this.increment(payload);
}
}
};
```

当然 rematch 的方式保持了函数的无副作性质,可以看出是做了一些取舍。

## 4 总结

重复一下作者提出工具质量的公式:

`工具质量 = 工具节省的时间/使用工具消耗的时间`

如果一个工具能节省开发时间,但本身带来了很大使用成本,在想清楚如何减少使用成本之前,不要急着用在项目中,这是我得到的最大启发。

最后感谢 rematch 作者精益求精的精神,给 redux 带来进一步的极致优化。

## 5 更多讨论

> 讨论地址是:[精读《重新思考 Redux》 · Issue #83 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/83)
**如果你想参与讨论,请[点击这里](https://github.com/dt-fe/weekly),每周都有新的主题,周末或周一发布。**

0 comments on commit f142ec1

Please sign in to comment.