《深入浅出 React和Redux》第一遍

前言

目的:第一遍摘抄书中大部分内容,方便过第二、三遍时可以随时随地通过手机查看。

1.React

1、我们在JSX中看到一个组件使用了onClick,但是并没有产生直接使用onclick的HTML,而是使用事件委托的方式处理点击事件,无论有多少个onClick出现,其实最后都只在DOM树上添加了一个事件处理函数,挂在最高层的DOM节点上。所有的点击事件都被这个事件处理函数捕获,然后根据具体组件分配给特定函数。
因为React控制了组件的生命周期,在unmount的时候自然能够清除相关的所有事件处理函数,内存泄漏也不再是一个问题。

2、试试

1
2
使用 ` create-react-app ` 命令创建项目
执行 ` npm run eject` 将隐藏在 ` react-scripts ` 中的一系列技术栈配置都显示出来 (不可逆)

3、纯函数,指的是没有任何副作用,输出完全依赖输入的函数,两次函数调用如果输入相同,得到的结果也绝对相同。

4、web前端开发关于性能优化有一个原则:尽量减少DOM操作。虽然DOM操作也只是一些简单的JavaScript语句,但是DOM操作会引起浏览器对网页进行重新布局,重新绘制,这就是一个比JavaScript语句执行慢很多的过程。

5、分而治之 把问题分解为多个小问题,拆分组件。

6、prop是组件的对外接口,state是组件的内部状态,对外用prop,对内用state。

7、

1
2
3
4
5
6
7
8
class Counter extends React.Component{
super(props);
this.onClickIncrementButton = this.onClickIncrementButton.bind(this);
this.onClickInDecrementButton = this.onClickDecrementButton.bind(this);
this.state = {
count: props.initValue || 0
}
}

(1) 如果一个组件需要定义自己的构造函数,一定要在构造函数的第一行通过super调用父类也就是React.Component的构造函数。如果在构造函数中没有调用super(props),那么组件实例被构造之后,类实例的所有成员函数就无法通过this.props访问到父组件传递过来的props值。
(2) 在Count的构造函数中还给两个成员函数绑定了当前this的执行环境,因为ES6方法创建的React组件类并不自动给我们绑定this到当前实例对象。
(3) 在构造函数中可以通过参数props获得传入prop值,在其他函数中比如render中,可以通过this.props访问传入prop的值。

8、在开发环境中定义 propTypes, 在开发过程中避免犯错,但是生产环境做propTypes检查没什么帮助,还要消耗CPU计算资源。在生产环境用插件babel-react-optimize,生产环境安装可以去掉propTypes。

9、组件的生命周期的三个阶段:

  • 装载过程(Mount),也就是把组件第一次在DOM树中渲染的过程;
  • 更新过程(Update),当组件被重新渲染的过程;
  • 卸载过程(Unmount),组件从DOM中删除的过程。

9.1、装载过程

  • constructor
  • getInitialState
  • getDefaultProps
  • componentWillMount
  • render
  • componentDidMount

【constructor】目的:

  • 初始化state

  • 绑定成员函数的this环境
    在ES6语法下,类的每个成员函数在执行时的this并不是和类实例自动绑定的。而在构造函数中,this就是当前组件实例,为了方便调用,往往在构造函数中将这个实例的特定函数绑定this为当前函数。

    1
    this.onClickIncrementButton = this.onClickIncrementButton.bind(this);

    表示通过bind方法让当前实例中onClickIncrementButton函数被调用时,this始终指向当前组件实例。

【getInitialState、 getDefaultProps】只有在React.createClass方法创造的组件类才有作用。

【类组件名.defaultProps】在ES6中可以指定props 的初始值

【render】
函数应该是一个纯函数,完全根据this.state和this.props来决定返回的结果,而且不要产生任何副作用。

【componentDidMount】
render函数被调用完之后,componentDidMount函数并不是会被立刻调用,componentDidMount被调用的时候,render函数返回的东西已经引发了渲染,组件已经被”装载“到了DOM树上。

举例:

1
2
3
4
5
<App>
<Counter1></Counter1>
<Counter2></Counter2>
<Counter3></Counter3>
</App>

父组件包含三个子组件,在这4个组件中各自生命周期函数中打印文字,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
constructor App
componentWillMount App
render App
constructor Counter1
componentWillMount Counter1
render Counter1
constructor Counter2
componentWillMount Counter2
render Counter2
constructor Counter3
componentWillMount Counter3
render Counter3
componentDidMount Counter1
componentDidMount Counter2
componentDidMount Counter3
componentDidMount App
可以看到,虽然componentWillMount都是紧贴自己组价的render函数之前被调用,componentDidMount可不是跟着render函数被调用,当所有三个组件的render都被调用之后,三个组件的componentDidMount才连在一起被调用。
之所以会出现上面的现象是因为render函数本身并不往DOM树上渲染或者装载内容,它只是返回一个JSX的对象,然后由React库来根据返回对象决定如何渲染。而React库肯定是要把所有组件返回的结果综合起来,才能知道该如何产生对应的DOM修改。所以,只有React库调用三个Counter组件的render函数之后,才能有可能完成装载,这时候才会依次调用各个组件的componentDidMount函数作为装载过程的收尾。
componentDidMount只在浏览器端执行,在componentDidMount被调用的时候,组件已经被装载到DOM树上了,可以放心获取渲染出来的任何DOM。

【componentDidUpdate】
当props或者state被修改的时候,就会引发组件的更新过程。

【shouldComponentUpdate】
我们知道render函数应该是一个纯函数,这个纯函数的逻辑输入就是组件的props和state。所以,shouldComponentUpdate的参数就是接下来的props和state值。

【componentWillUnMount】
当React组件要从DOM树上删除掉之前,对应的componentWillUnMount函数就会被调用

10、第二章小结:React利用props来定义组件的对外接口,用state来代表内部的状态,某个数据选择用props还是state表示,取决于这个数据是对外还是对内。

2. Redux

  • 唯一数据源
  • 保持状态只读
  • 数据改变只能通过纯函数完成

2.1.1唯一数据源

指的是应用的状态数据应该只存储在唯一的一个store上。
这个唯一Store的状态,是一个树形的对象,每个组件往往只是用树形对象上一部分的数据。

2.1.2保持状态只读

就是说不能直接修改状态,要修改Store的状态,必须要通过派发一个action对象完成。

2.1.3数据改变只能通过纯函数完成

2.2 容器组件和展示组件

拆分容器组件和展示组件,是设计React组件的一种模式,和Redux没有直接关系。

2.1 react-redux

2.1.1 connect

connect(mapStateToProps, mapDispatchToProps)(Counter)

这里有两次函数执行,第一次是connect函数,第二次是把connect函数返回的函数再次执行,最后产生的就是容器组件。

2.1.2 Provider

react-redux要求store必须包含三个函数:subscribe、dispatch、getState。

3. 模块化 React 和 Redux 应用

  • 代码文件的组织结构;
  • 确定模块的边界;
  • Store的状态树设计。

3.1 代码文件的组织方式

Redux应用适合于“按功能组织”,也就是把完成同一应用功能的代码放在一个目录下,一个应用功能包含多个角色的代码。
拿Todo应用为例,那个应用的两个基本功能就是TodoList和Filter,所以代码文件目录如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
todoList/
actions.js
actionTypes.js
index.js
reducer.js
views/
component.js
container.js
filter/
actions.js
actionTypes.js
index.js // 这个文件把所有的角色导入,统一导出
reducer.js
views/
component.js
container.js

每个基本功能对应的其实就是一个功能模块,每个功能模块对应一个目录。
在这种组织方式下,当你要修改某个功能模块代码的时候,只要关注对应的目录就行了。

3.2 确定模块的边界

在理想的情况下,我们应该通过增加代码就能增加系统的功能,而不是通过对现有代码的修改来增加功能。

不同功能模块之间的依赖关系应该简单而且清晰,也就是所谓的保持模块之间低耦合性;一个模块应该把自己的功能封装的很好,让外界不要太依赖与自己内部的结构,这样不会因为内部的变化而影响外部模块的功能,就是所谓高内聚性。

React组件本身应该具有低耦合性和高内聚性的特点,不过,在Redux的游乐场中,React组件扮演的就是一个视图的角色,还有reducer、actions这些角色参与这个游戏。对于整个Redux应用而言,整体由模块构成,但是模块不再是React组件,而是由React组件加上相关reducer和actions构成的一个小整体。

可以预期每个模块之间会有依赖关系,比如filter模块想要todoList的action构造函数和视图,那么我们希望对方如何导入?一种写法是像下面的代码这样:

1
2
import * as actions from '../todoList/actions';
import container as TodoList from '../todoList/views/container';

todoList和filter中的文件名几乎一样,但是这毕竟是模块内部的事情,不应该假设所有模块都按照这样的文件夹名命名。

现在我们既然把一个目录看做一个模块,那我们要做的就是明确这个模块对外的接口,而这个接口应该实现把内部封装起来。todoList和filter模块目录下的index.js就是我们的模块接口。

比如,在todoList/index.js中,代码如下:

1
2
3
4
import * as actions from '../todoList/actions';
import reducer from './reducer';
import views as TodoList from '../todoList/views/container';
export {actions, reducer, views};

如果filter中的组件想要使用todoList中的功能,应该导入todoList这个目录,如下:

1
import {actions, reducer, views} from '../todoList';

3.3 状态树的设计

  • 一个模块控制一个状态节点
  • 避免冗余数据
  • 树形结构扁平

3.4 辅助开发工具

  • React Devtools:可以检视React组件的树形结构。
  • Redux Devtools:可以检视Redux数据流,可以将Store状态跳跃到任何一个历史状态,也就是所谓的“时间旅行”功能。
  • React Perf:可以发现React组件的渲染问题。

redux-immutable-state-invariant辅助包:每个reducer函数都必须是一个纯函数,不能修改传入的参数state和action,这个包可以在开发环境中使用,当不小心在reducer中修改了参数state,会给以错误警告。

4. 组件的性能优化

4.1 单个组件的性能优化