上篇文章 结合源码学习 Redux 介绍了 redux 的基本知识,时隔三个月,实战之后重新来看 react-redux 源码,整理下开发过程中遇到的问题和一些看源码学到的冷知识。

react-redux

mapActionToProps 支持 Object 类型

一开始看文档,很多例子使用 mapDispatchToProps 都是👇这种形式。

1
2
3
4
5
6
7
8
9
10
11
12
import * as actions from './actions'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
return { todos: state.todos }
}

function mapDispatchToProps(dispatch) {
return bindActionCreators(actions, dispatch)
}

export default connect(mapStateToProps, mapDispatchToProps)(TodoApp)

这样的写法没有问题,然而对新手不够友好🐶。

刚学 react 看到代码,内心YY,bindActionCreators 是什么鬼???为什么 redux 这么难???为什么我要用 react??? 为什么我要学前端???

这种会引发灵魂拷问的代码,还是应该尽量避免。特别是紧急项目,哪有时间给你去看代码,先用了再说。看源码能发现,react-redux 内部会判断如果 mapDispatchToProps 是 Object 的情况下会自行调用执行 bindActionCreators

1
2
3
4
5
export function whenMapDispatchToPropsIsObject(mapDispatchToProps) {
return (mapDispatchToProps && typeof mapDispatchToProps === 'object')
? wrapMapToPropsConstant(dispatch => bindActionCreators(mapDispatchToProps, dispatch))
: undefined
}

mapDispatchToProps.js

如果不需要起别名的话,可以改写成以下这种形式。隐藏掉 bindActionCreators ,有时间再看~

1
2
3
4
5
6
7
8
import * as actions from './actions'
import { bindActionCreators } from 'redux'

function mapStateToProps(state) {
return { todos: state.todos }
}

export default connect(mapStateToProps, actions)(TodoApp)

慎用 subscribe

按照 redux 的设计,每次 dispatch 都会触发 subscribe 的执行,容易导致不相关的代码多次执行。

1
2
3
4
5
6
7
8
9
import i18next from 'i18next'

store.subscribe(() => {
const resouces = store.getState().i18n
i18next.init({
lng: 'en',
resources
}
})

上面的代码,本意是在每次 store 的 i18n 更新时,都重新初始化 i18next。代码能够正常工作,然而任意一次 dispatch 都会重新初始化 i18next。比如用户点了添加购物车按钮。。。

shouldComponentUpdate 与性能优化

在写 React 组件的时候,需要 extends React.Component 来继承基础组件。React 15.3 的时候又搞出 React.PureComponent 。你可能也知道,用 React.PureComponent 性能会好一些。为什么呢?

这里面的奥妙在于 React Component 的生命周期方法,shouldComponentUpdate,React Component 在每次渲染之前都会来询问它。

查水表,请问有人在家吗?

如果是 PureComponent, shouldUpdate 的判断逻辑会使用 shallowEqual。

1
2
3
4
if (type.prototype && type.prototype.isPureReactComponent) {
shouldUpdate =
!shallowEqual(oldProps, props) || !shallowEqual(oldState, state);
}

所以秘密都藏在 shallowEqual 里面。具体代码在这: shallowEqual.js

在做业务开发的时候,判断两个 Object 是否相等,我们通常会使用 JSON.stringify 或者深度遍历所有属性来校验。前者有严格顺序要求,后者需要深度遍历,anyways,两种方法的效率都不高。

shallowEqual 这个方法的作用就是字面意思,浅层比较。拿到两个 Object,按照 shalloEqual 的逻辑只要满足下面两种判断的一种,即认为相等。

  • 严格相等 ===
  • 拥有相同数量 && 一样的 key && value 严格相等。

可以在单元测试里面看到预期的运行结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
expect(
shallowEqual(
{a: 1, b: 2, c: 1},
{a: 1, b: 2, c: 1}
)
).toBe(true);

expect(
shallowEqual(
{a: 1, b: 2, c: {}},
{a: 1, b: 2, c: {}}
)
).toBe(false);

现在知道了,PureComponent 做的优化点主要是使用 shallowCompare 来判断 props,state 是否有更新。回到主题, react-redux 也用了同样的判断。connect 方法第四个参数支持传入 equal 比对方法。默认判断 props 是否更新用的判断方法都是 shallowEqual。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
return function connect(
mapStateToProps,
mapDispatchToProps,
mergeProps,
{
pure = true,
areStatesEqual = strictEqual,
areOwnPropsEqual = shallowEqual,
areStatePropsEqual = shallowEqual,
areMergedPropsEqual = shallowEqual,
...extraOptions
} = {}
) {
// ...
}

同时,selectorFactory.js 会记录上一次的 state 和 props,根据 areStatesEqual,arePropsEqual 来判断 是否有更新。当 equal 结果 为 true,直接返回上一次的 mergeProps,当 equal 为 false,则重新计算 mergeProps。

所以,即便每次 dispatch,reducer 返回的都是全新的 Object,组件并不会每次都重新 render !因为只要 shallowEqual 返回为 true,拿到的是 react-redux 上一次缓存的 Object。

说到这,还能顺带提一下 Immutable ,判断对象是否相等更高效的方式,不再这里展开。
immutable

HOC

高阶组件对于 Vue 开发者来说是个陌生的概念,React 文档

从用法上看,高阶组件就是一个方法,传入一个组件,返回另一个组件。比如 react-redux 里面的 connect 方法,通过传入 组件和 map*ToProps 方法,让组件和 store 连接。组件内部就可以直接通过 props 获得 connect 之后的值。

使用高阶组件的形式,connect 方法可以让 UI 组件和数据源解耦,也就是说对于 UI 组件,并不需要关心数据从哪里来。

Middleware 之 redux-thunk

redux-thunk 是个 redux 中间件,可以让 redux action 支持方法类型。代码及其简单,简单到只有几行(github ✨数有九千)

1
2
3
4
5
6
({ dispatch, getState }) => next => action => {
if (typeof action === 'function') {
return action(dispatch, getState);
}
return next(action);
}

利用 redux 中间件的特性,调用 next 方法继续执行其他中间件,调用 store.dispatch 则又是一个全新的 dispatch。引入 redux-thunk 之后,就可以 dispatch 来执行异步方法了。

1
2
3
4
5
6
7
8
9
10
function incrementAsync() {
return dispatch => {
setTimeout(() => {
// Yay! Can invoke sync or async actions with `dispatch`
dispatch(increment());
}, 1000);
};
}

dispatch(incrementAsync())

关联 store

react-redux 的 Provider 组件非常简单,只是利用 react context api 将 props.store 放到 context 上,然后所有子孙组件就可以通过 context.store 拿到 store 的引用。

1
2
3
4
componentWillMount() {
this.unsubscribe = store.subscribe(() => {
})
}

这种方式监听 store 的变更除了执行频繁之外,还容易出现父子组件执行顺序导致不必要的重复渲染。而这些 react-redux 帮我们处理好了。同样是 Context Api,react-redux 还会将 Subscription 放入 Context。

Subscription 是个订阅模式的实现,子组件都来监听父组件变更(实际上并不是严格的父子关系,通过 context api 来跨级往上找),第一个父组件监听 store.subscribe。这样来保证顺序。

通过 context.subscriptionKey 获取 parentHub,如果没有 parentHub 就监听 store.subscribe(),如果存在 parentHub 则监听 parentHub 的 变更。

Subscription

然后变更回调里面就是执行 selector 来判断是否需要重新渲染,如果自身不需要更新则通知子组件去执行 onStateChange,如果自身需要重新渲染,则在 didUpdate 之后去通知子组件。

1
2
3
4
5
6
7
8
9
10
onStateChange() {
this.selector.run(this.props)

if (!this.selector.shouldComponentUpdate) {
this.notifyNestedSubs()
} else {
this.componentDidUpdate = this.notifyNestedSubsOnComponentDidUpdate
this.setState(dummyState)
}
}

connectAdvanced

总结

以前没有看源码的习惯,自从学 react 之后,开始养成有问题看源码的习惯。redux,react-redux 都是代码量很小的库,推荐阅读。

实战过之后再来回顾,总结学习,上面写的内容都是我在学习过程中了解到的,不一定对。

相关阅读

如需转载,请注明出处: http://w3ctrain.com / 2018/06/10/learning-react-redux/

helkyle

我叫周晓楷

我现在是一名前端开发工程师,在编程的路上我还是个菜鸟,w3ctrain 是我用来记录学习和成长的地方。