Open WangShuXian6 opened 6 years ago
例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
保持接口小,props 数量要少
根据数据边界来划分组件,利用组合(composition)
把 state 尽量往上层组件提取
避免 renderXXXX 函数
给回调函数类型的 props 加统一前缀
使用 propTypes 来定义组件的 props
无状态组件和类组件并不是对立的概念,一个类组件如果没有自己的state,一样是无状态组件,类组件和函数形式组件才是对立的概念。
- 尽量每个组件都有自己专属的源代码文件;
- 用解构赋值(destructuring assignment)的方法获取参数 props 的每个属性值;
- 利用属性初始化(property initializer)来定义 state 和成员函数。
from: https://zhuanlan.zhihu.com/p/20346379
React diff 作为 Virtual DOM 的加速器,其算法上的改进优化是 React 整个界面渲染的基础,以及性能提高的保障
React diff 会帮助我们计算出 Virtual DOM 中真正变化的部分,并只针对该部分进行实际 DOM 操作,而非重新渲染整个页面,从而保证了每次操作更新后页面的高效渲染
React 通过分层求异的策略,对 tree diff 进行算法优化;
React 通过相同类生成相似树形结构,不同类生成不同树形结构的策略,对 component diff 进行算法优化;
React 通过设置唯一 key的策略,对 element diff 进行算法优化;
计算一棵树形结构转换成另一棵树形结构的最少操作,是一个复杂且值得研究的问题。传统 diff 算法通过循环递归对节点进行依次对比,效率低下,算法复杂度达到 O(n^3),其中 n 是树中节点的总数。O(n^3) 到底有多可怕,这意味着如果要展示1000个节点,就要依次执行上十亿次的比较。这种指数型的性能消耗对于前端渲染场景来说代价太高了!现今的 CPU 每秒钟能执行大约30亿条指令,即便是最高效的实现,也不可能在一秒内计算出差异情况。
如果 React 只是单纯的引入 diff 算法而没有任何的优化改进,那么其效率是远远无法满足前端渲染所要求的性能。
React 通过制定大胆的策略,将 O(n^3) 复杂度的问题转换成 O(n) 复杂度的问题。
1-Web UI 中 DOM 节点跨层级的移动操作特别少,可以忽略不计。
2-拥有相同类的两个组件将会生成相似的树形结构,拥有不同类的两个组件将会生成不同的树形结构。
3-对于同一层级的一组子节点,它们可以通过唯一 id 进行区分。
基于以上三个前提策略,React 分别对 tree diff、component diff 以及 element diff 进行算法优化,事实也证明这三个前提策略是合理且准确的,它保证了整体界面构建的性能。
基于策略一,React 对树的算法进行了简洁明了的优化,即对树进行分层比较,两棵树只会对同一层次的节点进行比较。
既然 DOM 节点跨层级的移动操作少到可以忽略不计,针对这一现象,React 通过 updateDepth 对 Virtual DOM 树进行层级控制,只会对相同颜色方框内的 DOM 节点进行比较,即同一个父节点下的所有子节点。当发现节点已经不存在,则该节点及其子节点会被完全删除掉,不会用于进一步的比较。这样只需要对树进行一次遍历,便能完成整个 DOM 树的比较。
updateChildren: function(nextNestedChildrenElements, transaction, context) {
updateDepth++;
var errorThrown = true;
try {
this._updateChildren(nextNestedChildrenElements, transaction, context);
errorThrown = false;
} finally {
updateDepth--;
if (!updateDepth) {
if (errorThrown) {
clearQueue();
} else {
processQueue();
}
}
}
}
当出现节点跨层级移动时,并不会出现想象中的移动操作,而是以 A 为根节点的树被整个重新创建,这是一种影响 React 性能的操作,因此 React 官方建议不要进行 DOM 节点跨层级的操作。
在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。
React 是基于组件构建应用的,对于组件间的比较所采取的策略也是简洁高效。
1-如果是同一类型的组件,按照原策略继续比较 virtual DOM tree。
2-如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。
3-对于同一类型的组件,有可能其 Virtual DOM 没有任何变化,如果能够确切的知道这点那可以节省大量的 diff 运算时间,因此 React 允许用户通过 shouldComponentUpdate() 来判断该组件是否需要进行 diff。
当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。
1-INSERT_MARKUP,新的 component 类型不在老集合里, 即是全新的节点,需要对新节点执行插入操作。
2-MOVE_EXISTING,在老集合有新 component 类型,且 element 是可更新的类型,generateComponentChildren 已调用 receiveComponent,这种情况下 prevChild=nextChild,就需要做移动操作,可以复用以前的 DOM 节点。
3-REMOVE_NODE,老 component 类型,在新集合里也有,但对应的 element 不同则不能直接复用和更新,需要执行删除操作,或者老 component 不在新集合里的,也需要执行删除操作。
优化策略:允许开发者对同一层级的同组子节点,添加唯一 key 进行区分,虽然只是小小的改动,性能上却发生了翻天覆地的变化!
新老集合所包含的节点,如下图所示,新老集合进行 diff 差异化对比,通过 key 发现新老集合中的节点都是相同的节点,因此无需进行节点删除和创建,只需要将老集合中节点的位置进行移动,更新为新集合中节点的位置,此时 React 给出的 diff 结果为:B、D 不做任何操作,A、C 进行移动操作,即可。
首先对新集合的节点进行循环遍历,for (name in nextChildren),通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。
React 的主要思想是通过构建可复用组件来构建用户界面。 所谓组件其实就是 有限状态机,通过状态渲染对应的界面,且每个组件都有自己的生命周期,它规定了组件的状态和方法需要在哪个阶段进行改变和执行。
有限状态机(FSM),表示有限个状态以及在这些状态之间的转移和动作等行为的模型。 一般通过状态、事件、转换和动作来描述有限状态机,下面是描述组合锁状态机的模型图,包括5个状态、5个状态自转换、6个状态间转换和1个复位 RESET 转换到状态 S1。 状态机,能够记住目前所处的状态,根据当前的状态可以做出相应的决策,并且在进入不同的状态时,可以做不同的操作。 通过状态机将复杂的关系简单化,利用这种自然而直观的方式可以让代码更容易理解。
React 正是利用这一概念,通过管理状态来实现对组件的管理。 例如,某个组件有显示和隐藏两个状态,通常会设计两个方法 show() 和 hide() 来实现切换; 而 React 只需要设置状态 setState({ showed: true/false }) 即可实现。 同时,React 还引入了组件的生命周期概念。通过它就可以实现组件的状态机控制,从而达到 “生命周期-状态-组件” 的和谐画面。
当首次装载组件时,按顺序执行 getDefaultProps、getInitialState、componentWillMount、render 和 componentDidMount;
当卸载组件时,执行 componentWillUnmount;
当重新装载组件时,此时按顺序执行 getInitialState、componentWillMount、render 和 componentDidMount,但并不执行 getDefaultProps;
当再次渲染组件时,组件接受到更新状态,此时按顺序执行 componentWillReceiveProps、shouldComponentUpdate、componentWillUpdate、render 和 componentDidUpdate。
自定义组件(ReactCompositeComponent)的生命周期主要通过三种状态进行管理:MOUNTING、RECEIVE_PROPS、UNMOUNTING,它们负责通知组件当前所处的状态,应该执行生命周期中的哪个步骤,是否可以更新 state。
三个状态对应三种方法,分别为:mountComponent、updateComponent、unmountComponent,每个方法都提供了两种处理方法,
will 方法在进入状态之前调用,did 方法在进入状态之后调用,三种状态三种方法五种处理方法,此外还提供两种特殊状态的处理方法。
from : https://zhuanlan.zhihu.com/p/20312691
- React 界面完全由数据驱动;
- React 中一切都是组件;
- props 是 React 组件之间通讯的基本方式。
等号左边的 UI 代表最终画出来的界面;等号右边的 f 是一个函数,也就是我们写的 React 相关代码;data 就是数据,在 React 中,data 可以是 state 或者 props。
UI 就是把 data 作为参数传递给 f 运算出来的结果。这个公式的含义就是,如果要渲染界面,不要直接去操纵 DOM 元素,而是修改数据,由数据去驱动 React 来修改界面。
我们开发者要做的,就是设计出合理的数据模型,让我们的代码完全根据数据来描述界面应该画成什么样子,而不必纠结如何去操作浏览器中的 DOM 树结构。
这样一种程序结构,是声明式编程(Declarative Programming)的方式,代码结构会更加容易理解和维护。
- 用户界面就是组件;
- 组件可以嵌套包装组成复杂功能;
- 组件可以用来实现副作用。
在界面上看到的任何一个“块”,都需要代码来实现,而这部分代码最好是独立存在的,与其他代码之间的纠葛越少越好,所以要把这个“块”的相关代码封装在一个代码单元里。这样的代码单元,在 React 里就是一个“组件”。
在上面的图中,一个 Button 是一个界面元素,对应的就是一个 React 组件。在 React 中,一个组件可以是一个类,也可以是一个函数,这取决于这个组件是否有自己的状态。
现实中的应用是很复杂的,界面设计中包含很多元素,一个“块”套着另一个“块”,React 中的组件可以重复嵌套,就是为了支持现实中的用户界面需要。
并不是说组件必须要在界面画一些东西,一个组件可以什么都不画,或者把画界面的事情交给其他组件去做,自己做一些和界面无关的事情,比如获取数据。
下面是一个 Beacon 组件,它的 render 函数返回为空,所以它实际上并不渲染任何东西。
class Beacon extends React.Component { render() { return null; }
componentDidMount() { const beacon = new Image(); beacon.src = 'https://domain.name/beacon.gif'; } }
>不过,Beacon 的 componentDidMount 函数中创造了一个 Image 对象,访问了一个特定的图片资源,这样就可以对应服务器上留下日志记录,用于记录这一次网页访问。
>Beacon 组件的使用方式和普通组件别无二致,但是却能够轻松实现对网页访问的跟踪。
```tsx
<div>
<Beacon />
</div>
如果一个父组件有话要对子组件说,也就是,想要传递数据给子组件,则应该通过 props。
当然,你可以给子组件增加一个新的函数,然后让父组件去调用这个函数,但是,这种方法很拙劣。如果直接调用子组件的函数,执行过程也处于 React 生命周期之外,所以,不应该使用这种方法。
同样,如果子组件有话要同父组件说,那应该支持函数类型的 props。身为 JavaScript 里一等公民的函数可以作为参数传递,当然也可以作为 props 传递。让父组件传递一个函数类型的 props 进来,当子组件要传递数据给父组件时,调用这个函数类型 props,就把信息传递给了父组件。
如果两个完全没有关系的组件之间有话说,情况就复杂了一点 就没法直接通过 props 来传递信息。
一个比较土的方法,就是通过 props 之间的逐步传递,来把这两个组件关联起来。如果之间跨越两三层的关系,这种方法还凑合,但是,如果这两个组件隔了十几层,或者说所处位置多变,那让 props 跨越千山万水来相会,实在是得不偿失。
另一个简单的方式,就是建立一个全局的对象,两个组件把想要说的话都挂在这个全局对象上。这种方法当然简单可行,但是,我们都知道全局变量的危害罄竹难书,如果不想将来被难以维护的代码折磨,我们最好对这种方法敬而远之。
一般,业界对于这种场景,往往会采用第三方数据管理工具来解决
其实,不依赖于第三方工具,React 也提供了自己的跨组件通讯方式,这种方式叫 Context
React 的组件其实就就是软件设计中的模块,所以其设计原则也遵从通用的组件设计原则 要减少组件之间的耦合性(Coupling),让组件的界面简单,这样才能让整体系统易于理解、易于维护。
- 保持接口小,props 数量要少;
- 根据数据边界来划分组件,充分利用组合(composition);
- 把 state 往上层组件提取,让下层组件只需要实现为纯函数。
class StopWatch extends React.Component {
render() {
return (
<div>
<MajorClock>
<ControlButtons>
<SplitTimes>
</div>
);
}
}
const MajorClock = (props) => {
//TODO: 返回数字时钟的JSX
};
const ControlButtons = (props) => {
//TODO: 返回两个按钮的JSX
};
const SplitTimes = (props) => {
//TODO: 返回所有计次时间的JSX
}
尽量把数据状态往上层组件提取。在秒表这个应用中,上层组件就是 StopWatch,如果我们让 StopWatch 来存储时间状态,那一切就会简单很多。
StopWatch 中利用 setTimeout 或者 setInterval 来更新 state,每一次更新会引发一次重新渲染,在重新渲染的时候,直接把当前时间值传递给 MajorClock 就完事了。
ControlButtons 对状态的控制,让 StopWatch 传递函数类型 props 给 ControlButtons,当特定按钮时间点击的时候回调这些函数,StopWatch 就知道何时停止更新或者启动 setTimeout 或者 setInterval,因为这一切逻辑都封装在 StopWatch 中,非常直观自然。
SplitTimes,它需要一个数组记录所有计次时间,这些数据也很自然应该放在 StopWatch 中维护,然后通过 props 传递给 SplitTimes,这样 SplitTimes 只单纯做渲染就足够。
MajorClock,因为它依赖的数据只有当前时间,所以只需要一个 props。 传入的 props 是一个代表毫秒的数字,所以命名为 milliseconds props的命名一定力求简洁而且清晰
const MajorClock = ({milliseconds}) => { //TODO: 返回数字时钟的JSX };
MajorClock.propTypes = { milliseconds: PropTypes.number.isRequired };
>ControlButtons,这个组件需要根据当前是否是“启动”状态显示不同的按钮,所以需要一个 props 来表示是否“启动”,我们把它命名为 activated
>StopWatch 还需要传递回调函数给 ControlButtons,所以还需要支持函数类型的 props,分别代表 ControlButtons 可以做的几个动作:
>为了让开发者能够一眼认出回调函数类型的 props,这类 props 最好有一个统一的前缀,比如 on 或者 handle
```tsx
const ControlButtons = (props) => {
//TODO: 返回两个按钮的JSX
};
ControlButtons.propTypes = {
activated: PropTypes.bool,
onStart: PropTypes.func.isRquired,
onPause: PropTypes.func.isRquired,
onSplit: PropTypes.func.isRquired,
onReset: PropTypes.func.isRquired,
};
SplitTimes,它需要接受一个数组类型的 props
const SplitTimes = (props) => { //TODO: 返回所有计次时间的JSX }
SplitTimes.propTypes = { splits: PropTypes.arrayOf(PropTypes.number) };
>一个好的设计就是要在写代码之前就应用被证明最佳的原则,这样写代码的过程就会少走弯路。
##### 构建
##### ControlButtons
>从达到“代码整洁”的目的来说,应该每个组件都有一个独立的文件,然后这个文件用 export default 的方式导出单个组件
>在 src 目录下为 ControlButtons 创建一个 ControlButtons.js 文件
```tsx
import React from 'react';
const ControlButtons = () => {
//TODO: 实现ControlButtons
};
export default ControlButtons;
import ControlButtons from './ControlButtons';
因为 ControlButtons 是一个函数类型的组件,所以 props 以参数形式传递进来,props 中的属性包含 activated 这样的值,利用大括号,就可以完成对 props 的“解构”,把 props.activated 赋值给同名的变量 activated。
const ControlButtons = (props) => {
const {activated, onStart, onPause, onReset, onSplit} = props;
if (activated) {
return (
<div>
<button onClick={onSplit}>计次</button>
<button onClick={onPause}>停止</button>
</div>
);
} else {
return (
<div>
<button onClick={onReset}>复位</button>
<button onClick={onStart}>启动</button>
</div>
);
}
};
可以更进一步,把解构赋值提到参数中,这样连 props 的对象都看不见
const ControlButtons = ({activated, onStart, onPause, onReset, onSplit}) => {
}
>根据 activated 的值返回不同的 JSX,当 activated 为 true 时,返回的是“计次”和“停止”;当 activated 为 false 时,返回的是“复位”和“启动”,对应分别使用了传入的 on 开头的函数类型 props。
>ControlButtons 除了显示内容和分配 props,没有做什么实质的工作,实质工作会在 StopWatch 中
##### MajorClock
>如果使用 MajorClock 时没有传入 milliseconds 这个 props,那么 milliseconds 的值就是 0
```tsx
const MajorClock = ({milliseconds=0}) => {
return <h1>{ms2Time(milliseconds)}</h1>
};
因为把毫秒数转为 HH:mm:ss:mmm 这样的格式和 JSX 没什么关系,所以,我们不在组件中直接编写,而是放在 ms2Time 函数中
利用循环或者数组 map 而产生的动态数量的 JSX 元件,必须要有 key 属性 一般来说,key 不应该取数组的序号,因为 key 要唯一而且稳定,也即是每一次渲染过程中,key 都能唯一标识一个内容。对于 StopWatch 这个例子,倒是可以直接使用数组序号,因为计次时间的数组顺序不会改变,使用数组序号足够唯一标识内容。
import MajorClock from './MajorClock';
const SplitTimes = ({value=[]}) => { return value.map((v, k) => (
)); };
##### StopWatch 状态管理
>把这些子组件串起来,这就是 StopWatch。
>StopWatch 是一个有状态的组件,所以,不能只用一个函数实现,而是做成一个继承自 React.Component 的类
>对于一个 React 组件类,最少要有一个 render 函数实现
```tsx
class StopWatch extends React.Component {
render() {
return (
<Fragment>
<MajorClock />
<ControlButtons />
<SplitTimes />
</Fragment>
);
}
}
React 组件的 state 需要初始化
constructor() { super(...arguments);
this.state = {
isStarted: false,
startTime: null,
currentTime: null,
splits: [],
};
}
***
##### 属性初始化方法
>也可以完全避免编写 constructor 函数,而直接使用属性初始化(Property Initializer),也就是在 class 定义中直接初始化类的成员变量。
不用 constructor,可以这样初始化 state,效果是完全一样的:
```tsx
class StopWatch extends React.Component {
state = {
isStarted: false,
startTime: null,
currentTime: null,
splits: [],
}
}
不要在 JSX 中写内联函数(inline function) JSX 中应用的函数 props 应该尽量使用类成员函数,不要用内联函数。
render 这些生命周期函数,里面访问的 this 就是当前组件本身,完全是因为这些函数是 React 调用的,React 对它们进行了特殊处理,对于其他普通的成员函数,特殊处理就要靠我们自己了。
通常的处理方法,就是在构造函数中对函数进行绑定,然后把新产生的函数覆盖原有的函数,就像这样:
constructor() { super(...arguments);
this.onSplit = this.onSplit.bind(this);
}
>如果可以使用 bind operator,也可以这样写:
```tsx
this.onSplit = ::this.onSplit;
更好的方法依然是使用属性初始化,就和初始化 state 一样,利用等号直接初始化 onSplit,代码如下:
onSplit = () => { this.setState({ splits: [...this.state.splits, this.state.currentTime - this.state.startTime] }); }
不需要 constructor,函数体内的 this 绝对就是当前组件对象。
const clockStyle = {
'font-family': 'monospace'
};
const MajorClock = ({milliseconds=0}) => {
return <h1 style={clockStyle}>{ms2Time(milliseconds)}</h1>
}
导入一个同目录下的 ControlButtons.css 文件:
import "./ControlButtons.css";
npm install react-app-rewired styled-jsx
修改 scripts 部分 对应脚本中的 react-scripts 替换为 react-app-rewired,之后,当用 npm 执行这些指令的时候,就会使用 react-app-rewired。 package.json
"scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test --env=jsdom", "eject": "react-scripts eject" }
要让 react-scripts 支持 styled-jsx,对应只需要在项目根目录增加一个 config-overrides.js 文件 把 styled-jsx/babel 注入到 react-scripts 的基本配置中去,然后,我们的应用就支持 styled-jsx 了。
const { injectBabelPlugin } = require('react-app-rewired');
module.exports = function override(config, env) { config = injectBabelPlugin(['styled-jsx/babel'], config);
return config; };
***
##### 使用 styled-jsx 定制样式
>给 MajorClock 中的 h1 增加 CSS 规则
>style jsx 包裹的是一个字符串表达式,而这个字符串就是 CSS 规则。
>在 MajorClock 中用 style jsx 添加的 CSS 规则,只作用于 MajorClock 的 JSX 中出现的元素,不会影响其他的组件。
```tsx
const MajorClock = ({milliseconds=0}) => {
return (
<React.Fragment>
<style jsx>{`
h1 {
font-family: monospace;
}
`}</style>
<h1>
{ms2Time(milliseconds)}
</h1>
</React.Fragment>
);
};
可以动态修改 styled jsx 中的值,因为 styled jsx 的内容就是字符串,我们只要修改其中的字符串,就修改了样式效果。
让 MajorClock 在开始计时状态显示红色,否则显示黑色
const MajorClock = ({milliseconds=0, activated=false}) => { return ( <React.Fragment> <style jsx>{` h1 { color: ${activated? 'red' : 'black'}; font-family: monospace; } `}</style> <h1> {ms2Time(milliseconds)} </h1> </React.Fragment> ); };
这个模式的名称很多
- 容器组件和展示组件(Container and Presentational Components);
- 胖组件和瘦组件;
- 有状态组件和无状态组件。
软件设计中有一个原则,叫做“责任分离”(Separation of Responsibility),简单说就是让一个模块的责任尽量少,如果发现一个模块功能过多,就应该拆分为多个模块,让一个模块都专注于一个功能,这样更利于代码的维护。
使用 React 来做界面,无外乎就是获得驱动界面的数据,然后利用这些数据来渲染界面
最好把获取和管理数据这件事和界面渲染这件事分开
把获取和管理数据的逻辑放在父组件,也就是聪明组件;把渲染界面的逻辑放在子组件,也就是傻瓜组件。
这么做的好处,是可以灵活地修改数据状态管理方式,比如,最初你可能用 Redux 来管理数据,然后你想要修改为用 Mobx,如果按照这种模式分割组件,那么,你需要改的只有聪明组件,傻瓜组件可以保持原状。
功能可以分为两部分,第一部分是展示,也就是傻瓜组件 傻瓜组件 Joke 的功能很简单,显示一个笑脸,然后显示名为 value 的 props,也就是笑话的内容,如果没有 value 值,就显示一个“loading...”。
至于怎么获得笑话内容,不是 Joke 要操心的事,它只专注于显示笑话,所谓傻人有傻福,傻瓜组件虽然“傻”了一点,但是免去了数据管理的烦恼。
import SmileFace from './yaoming_simile.png';
const Joke = ({value}) => { return (
); }
***
##### 聪明组件
>聪明组件,这个组件不用管渲染的逻辑,只负责拿到数据,然后把数据传递给傻瓜组件,由傻瓜组件来完成渲染。
>RandomJoke
```tsx
export default class RandomJoke extends React.Component {
state = {
joke: null
}
render() {
return <Joke value={this.state.joke} />
}
componentDidMount() {
fetch('https://icanhazdadjoke.com/',
{headers: {'Accept': 'application/json'}}
).then(response => {
return response.json();
}).then(json => {
this.setState({joke: json.joke});
});
}
}
RandomJoke 的 render 函数只做一件事,就是渲染 Joke,并把 this.state 中的值作为 props 传进去。聪明组件的 render 函数一般都这样简单,因为渲染不是他们操心的业务,他们的主业是获取数据。
当 RandomJoke 被第一次渲染的时候,它的 state 中的 joke 值为 null,所以它传给 Joke 的 value 也是 null,这时候,Joke 会渲染一 “loading...”。
但是,在第一次渲染完毕的时候,componentDidMount 被调用,一个 API 请求发出去,拿到一个随机笑话,更新 state 中的 joke 值。因为对一个组件 state 的更新会引发一个新的渲染过程,所以 RandomJoke 的 render 再一次被调用,所以 Joke 也会再一次被渲染,这一次,传入的 value 值是一个真正的笑话,所以,笑话也就出现了。
应用了这种方法之后,如果你要优化界面,只需要去修改傻瓜组件 Joke, 如果你想改进数据管理和获取,只需要去修改聪明组件 RandomJoke。
因为傻瓜组件一般没有自己的状态,所以,可以像上面的 Joke 一样实现为函数形式, 其实,可以进一步改进,利用 PureComponent 来提高傻瓜组件的性能。
函数形式的 React 组件,好处是不需要管理 state,占用资源少,但是,函数形式的组件无法利用 shouldComponentUpdate。
当 RandomJoke 要渲染 Joke 时,即使传入的 props 是一模一样的,Joke 也要走一遍完整的渲染过程,这就显得浪费了。
好一点的方法,是把 Joke 实现为一个类,而且定义 shouldComponentUpdate 函数,每次渲染过程中,在 render 函数执行之前 shouldComponentUpdate 会被调用,如果返回 true,那就继续,如果返回 false,那么渲染过程立刻停止,因为这代表不需要重画了。
对于傻瓜组件,因为逻辑很简单,界面完全由 props 决定,所以 shouldComponentUpdate 的实现方式就是比较这次渲染的 props 是否和上一次 props 相同。 当然,让每一个组件都实现一遍这样简单的 shouldComponentUpdate 也很浪费,所以,React 提供了一个简单的实现工具 PureComponent,可以满足绝大部分需求。
改进后
class Joke extends React.PureComponent { render() { return ( <div> <img src={SmileFace} /> {this.props.value || 'loading...' } </div> ); } }
PureComponent 中 shouldComponentUpdate 对 props 做得只是浅层比较,不是深层比较,如果 props 是一个深层对象,就容易产生问题。 比如,两次渲染传入的某个 props 都是同一个对象,但是对象中某个属性的值不同,这在 PureComponent 眼里,props 没有变化,不会重新渲染
使用 React v16.6.0 之后的版本,可以使用一个新功能 React.memo 来完美实现 React 组件
React.memo 既利用了 shouldComponentUpdate,又不要求我们写一个 class,这也体现出 React 逐步向完全函数式编程前进。
const Joke = React.memo(() => ( <div> <img src={SmileFace} /> {this.props.value || 'loading...' } </div> ));
- 高阶组件不能去修改作为参数的组件,高阶组件必须是一个纯函数,不应该有任何副作用。
- 高阶组件返回的结果必须是一个新的 React 组件,这个新的组件的 JSX 部分肯定会包含作为参数的组件。
- 高阶组件一般需要把传给自己的 props 转手传递给作为参数的组件。
对于很多网站应用,有些模块都需要在用户已经登录的情况下才显示。比如,对于一个电商类网站,“退出登录”按钮、“购物车”这些模块,就只有用户登录之后才显示,对应这些模块的 React 组件如果连“只有在登录时才显示”的功能都重复实现,那就浪费了。
这时候,我们就可以利用“高阶组件(HoC)”这种模式来解决问题。
“高阶组件”名为“组件”,其实并不是一个组件,而是一个函数,只不过这个函数比较特殊 它接受至少一个 React 组件为参数,并且能够返回一个全新的 React 组件作为结果 这个新产生的 React 组件是对作为参数的组件的包装,所以,有机会赋予新组件一些增强的“神力”。
高阶组件的命名一般都带 with 前缀,命名中后面的部分代表这个高阶组件的功能。
const withDoNothing = (Component) => {
const NewComponent = (props) => {
return <Component {...props} />;
};
return NewComponent;
};
只有在登录时才显示
假设我们已经有一个函数 getUserId 能够从 cookies 中读取登录用户的 ID,如果用户未登录,这个 getUserId 就返回空,那么“退出登录按钮“就需要这么写:
const LogoutButton = () => { if (getUserId()) { return ...; // 显示”退出登录“的JSX } else { return null; } };
购物车
const ShoppintCart = () => { if (getUserId()) { return ...; // 显示”购物车“的JSX } else { return null; } };
两个组件明显有重复的代码,我们可以把重复代码抽取出来,形成 withLogin 这个高阶组件,
const withLogin = (Component) => {
const NewComponent = (props) => {
if (getUserId()) {
return <Component {...props} />;
} else {
return null;
}
}
return NewComponent;
};
只需要这样定义 LogoutButton 和 ShoppintCart
const LogoutButton = withLogin((props) => { return ...; // 显示”退出登录“的JSX });
const ShoppingCart = withLogin(() => { return ...; // 显示”购物车“的JSX });
>避免了重复代码,以后如果要修改对用户是否登录的判断逻辑,也只需要修改 withLogin,而不用修改每个 React 组件。
***
##### 高阶组件的高级用法
>高阶组件只需要返回一个 React 组件即可,没人规定高阶组件只能接受一个 React 组件作为参数,完全可以传入多个 React 组件给高阶组件。
>改进上面的 withLogin,让它接受两个 React 组件,根据用户是否登录选择渲染合适的组件。
```tsx
const withLoginAndLogout = (ComponentForLogin, ComponentForLogout) => {
const NewComponent = (props) => {
if (getUserId()) {
return <ComponentForLogin {...props} />;
} else {
return <ComponentForLogout{...props} />;
}
}
return NewComponent;
};
有了上面的 withLoginAndLogout,就可以产生根据用户登录状态显示不同的内容。
const TopButtons = withLoginAndLogout( LogoutButton, LoginButton );
高阶组件最巧妙的一点,是可以链式调用。
假设,你有三个高阶组件分别是 withOne、withTwo 和 withThree,那么,如果要赋予一个组件 X 某个高阶组件的超能力,那么,你要做的就是挨个使用高阶组件包装
const X1 = withOne(X); const X2 = withTwo(X1); const X3 = withThree(X2); const SuperX = X3; //最终的SuperX具备三个高阶组件的超能力
直接连续调用高阶组件
const SuperX = withThree(withTwo(withOne(X)));
高阶组件本身就是一个纯函数,纯函数是可以组合使用的, 所以,可以把多个高阶组件组合为一个高阶组件,然后用这一个高阶组件去包装X
const hoc = compose(withThree, withTwo, withOne); const SuperX = hoc(X);
compose,是函数式编程中很基础的一种方法,作用就是把多个函数组合为一个函数
export default function compose(...funcs) { if (funcs.length === 0) { return arg => arg }
if (funcs.length === 1) { return funcs[0] }
return funcs.reduce((a, b) => (...args) => a(b(...args))) }
>React 组件可以当做积木一样组合使用,现在有了 compose,我们就可以把高阶组件也当做积木一样组合,进一步重用代码。
>假如一个应用中多个组件都需要同样的多个高阶组件包装,那就可以用 compose 组合这些高阶组件为一个高阶组件,这样在使用多个高阶组件的地方实际上就只需要使用一个高阶组件了。
***
##### 不要滥用高阶组件
>高阶组件不得不处理 displayName,不然 debug 会很痛苦。
>当 React 渲染出错的时候,靠组件的 displayName 静态属性来判断出错的组件类,而高阶组件总是创造一个新的 React 组件类,所以,每个高阶组件都需要处理一下 displayName。
>如果要做一个最简单的什么增强功能都没有的高阶组件,也必须要写下面这样的代码:
```tsx
const withExample = (Component) => {
const NewComponent = (props) => {
return <Component {...props} />;
}
NewComponent.displayName = `withExample(${Component.displayName || Component.name || 'Component'})`;
return NewCompoennt;
};
对于 React 生命周期函数,高阶组件不用怎么特殊处理,但是,如果内层组件包含定制的静态函数,这些静态函数的调用在 React 生命周期之外,那么高阶组件就必须要在新产生的组件中增加这些静态函数的支持,这更加麻烦。
使用高阶组件,一定要非常小心,要避免重复产生 React 组件,比如,下面的代码是有问题的:
const Example = () => { const EnhancedFoo = withExample(Foo); return <EnhancedFoo /> }
每一次渲染 Example,都会用高阶组件产生一个新的组件,虽然都叫做 EnhancedFoo,但是对 React 来说是一个全新的东西,在重新渲染的时候不会重用之前的虚拟 DOM,会造成极大的浪费。
正确的写法是下面这样,自始至终只有一个 EnhancedFoo 组件类被创建:
const EnhancedFoo = withExample(Foo);
const Example = () => {
return
render props,指的是让 React 组件的 props 支持函数这种模式。因为作为 props 传入的函数往往被用来渲染一部分界面,所以这种模式被称为 render props。
一个最简单的 render props 组件 RenderAll 这个 RenderAll 预期子组件是一个函数,它所做的事情就是把子组件当做函数调用,调用参数就是传入的 props,然后把返回结果渲染出来,除此之外什么事情都没有做。
RenderAll 的内部元素即为 props.children ,调用 该内部元素并把 RenderAll 的所有 属性传给它
作为一个通用的render props,并不知道子组件需要用到哪些props,所以应该都传递过去,用不用在他,但是传不传在你。
const RenderAll = (props) => { return( <React.Fragment> {props.children(props)} </React.Fragment> ); };
使用 RenderAll RenderAll 的子组件,也就是夹在 RenderAll 标签之间的部分,其实是一个函数。这个函数渲染出 \<h1>hello world\<\/h1>,这就是上面使用 RenderAll 渲染出来的结果。
<RenderAll> {() => <h1>hello world</h1>} </RenderAll>
render props 可以做很多的定制功能,我们还是以根据是否登录状态来显示一些界面元素为例,来实现一个 render props。
实现 render props 的 Login 组件, render props 和高阶组件的第一个区别,就是 render props 是真正的 React 组件,而不是一个返回 React 组件的函数。
当用户处于登录状态,getUserName 返回当前用户名,否则返回空,然后我们根据这个结果决定是否渲染 props.children 返回的结果。
const Login = (props) => { const userName = getUserName();
if (userName) { const allProps = {userName, ...props}; return (
解决跨级的信息传递 避免 props 逐级传递,即是提供者的用途。
虽然这个模式叫做“提供者模式”,但是其实有两个角色,一个叫“提供者”(Provider),另一个叫“消费者”(Consumer),这两个角色都是 React 组件。 其中“提供者”在组件树上居于比较靠上的位置,“消费者”处于靠下的位置。
既然名为“提供者”,它可以提供一些信息,而且这些信息在它之下的所有组件,无论隔了多少层,都可以直接访问到,而不需要通过 props 层层传递。
实现提供者模式,需要 React 的 Context 功能,可以说,提供者模式只不过是让 Context 功能更好用一些而已。
所谓 Context 功能,就是能够创造一个“上下文”,在这个上下文笼罩之下的所有组件都可以访问同样的数据。
提供者模式的一个典型用例就是实现“样式主题”(Theme),由顶层的提供者确定一个主题,下面的样式就可以直接使用对应主题里的样式。 这样,当需要切换样式时,只需要修改提供者就行,其他组件不用修改。
在 React v16.3.0 之前,要实现提供者,就要实现一个 React 组件,不过这个组件要做两个特殊处理。
- 需要实现 getChildContext 方法,用于返回“上下文”的数据;
- 需要定义 childContextTypes 属性,声明“上下文”的结构。
class ThemeProvider extends React.Component {
getChildContext() {
return {
theme: this.props.value
};
}
render() {
return (
<React.Fragment>
{this.props.children}
</React.Fragment>
);
}
}
ThemeProvider.childContextTypes = {
theme: PropTypes.object
};
getChildContext 只是简单返回名为 value 的 props 值,但是,因为 getChildContext 是一个函数,它可以有更加复杂的操作,比如可以从 state 或者其他数据源获得数据。
对于 ThemeProvider,我们创造了一个上下文,这个上下文就是一个对象,结构是这样:
{ theme: { //一个对象 } }
做两个消费(也就是使用)这个“上下文”的组件,第一个是 Subject,代表标题;第二个是 Paragraph,代表章节。
class Subject extends React.Component {
render() {
const {mainColor} = this.context.theme;
return (
<h1 style={{color: mainColor}}>
{this.props.children}
</h1>
);
}
}
Subject.contextTypes = {
theme: PropTypes.object
}
在 Subject 的 render 函数中,可以通过 this.context 访问到“上下文”数据,因为 ThemeProvider 提供的“上下文”包含 theme 字段,所以可以直接访问 this.context.theme。
Subject 必须增加 contextTypes 属性,必须和 ThemeProvider 的 childContextTypes 属性一致,不然,this.context 就不会得到任何值。
为什么要求“提供者”用 childContextTypes 定义一次上下文结构,又要求“消费者”再用 contextTypes 再重复定义一次呢?
React 这么要求,是考虑到“上下文”可能会嵌套,就是一个“提供者”套着另一个“提供者”,这时候,底层的消费者组件到底消费哪一个“提供者”呢?通过这种显示的方式指定。
const Paragraph = (props, context) => {
const {textColor} = context.theme;
return (
<p style={{color: textColor}}>
{props.children}
</p>
);
};
Paragraph.contextTypes = {
theme: PropTypes.object
};
从上面的代码可以看到,因为 Paragraph 是一个函数形式,所以不可能访问 this.context,但是函数的第二个参数其实就是 context。
不要忘了设定 Paragraph 的 contextTypes,不然参数 context 也不会是上下文。
做一个组件来使用 Subject 和 Paragraph,这个组件不需要帮助传递任何 props
const Page = () => ( <div> <Subject>这是标题</Subject> <Paragraph> 这是正文 </Paragraph> </div> );
上面的组件 Page 使用了 Subject 和 Paragraph,现在我们想要定制样式主题,只需要在 Page 或者任何需要应用这个主题的组件外面包上 ThemeProvider,对应的 JSX 代码
<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} > <Page /> </ThemeProvider>
>当我们需要改变一个样式主题的时候,改变传给 ThemeProvider的 value 值就搞定了。
***
#### React v16.3.0 之后的提供者模式
>需要让“提供者”和“消费者”共同依赖于一个 Context 对象
>而且消费者也要使用 render props 模式。
##### 首先,要用新提供的 createContext 函数创造一个“上下文”对象。
>创造“提供者”极大简化了,不需要创造一个 React 组件类。
```tsx
const ThemeContext = React.createContext();
这个“上下文”对象 ThemeContext 有两个属性,分别就是Provider 和 Consumer。
const ThemeProvider = ThemeContext.Provider; const ThemeConsumer = ThemeContext.Consumer;
Subject
class Subject extends React.Component { render() { return ( <ThemeConsumer> { (theme) => ( <h1 style={{color: theme.mainColor}}> {this.props.children} </h1> ) } </ThemeConsumer> ); } }
上面的 ThemeConsumer 其实就是一个应用了 render props 模式的组件,它要求子组件是一个函数,会把“上下文”的数据作为参数传递给这个函数,而这个函数里就可以通过参数访问“上下文”对象。
在新的 API 里,不需要设定组件的 childContextTypes 或者 contextTypes 属性
Subject 没有自己的状态,没必要实现为类,我们用纯函数的形式实现 Paragraph
const Paragraph = (props, context) => { return ( <ThemeConsumer> { (theme) => ( <p style={{color: theme.textColor}}> {props.children} </p> ) } </ThemeConsumer> ); };
<ThemeProvider value={{mainColor: 'green', textColor: 'red'}} >
<Page />
</ThemeProvider>
在新版 Context API 中,需要一个“上下文”对象(上面的例子中就是 ThemeContext),使用“提供者”的代码和“消费者”的代码往往分布在不同的代码文件中,那么,这个 ThemeContext 对象放在哪个代码文件中呢? 最好是放在一个独立的文件中
为了避免依赖关系复杂,每个应用都不要滥用“上下文”,应该限制“上下文”的使用个数。
组合组件模式要解决的是这样一类问题:父组件想要传递一些信息给子组件,但是,如果用 props 传递又显得十分麻烦。
很多界面都有 Tab 这样的元件,我们需要一个 Tabs 组件和 TabItem 组件,Tabs 是容器,TabItem 是一个一个单独的 Tab,因为一个时刻只有一个 TabItem 被选中,很自然希望被选中的 TabItem 样式会和其他 TabItem 不同。
TabItem 有两个重要的 props: active 代表自己是否被激活,onClick 是自己被点击时应该调用的回调函数,这就足够了。 TabItem 所做的就是根据这两个 props 渲染出 props.children,没有任何复杂逻辑,是一个活脱脱的“傻瓜组件”,所以,用一个纯函数实现就可以了。
const TabItem = (props) => { const {active, onClick} = props; const tabStyle = { 'max-width': '150px', color: active ? 'red' : 'green', border: active ? '1px red solid' : '0px', }; return ( <h1 style={tabStyle} onClick={onClick}> {props.children} </h1> ); };
Tabs 如何把 active 和 onClick 传递给 TabItem 使用组合组件的 JSX 代码
<Tabs> <TabItem>One</TabItem> <TabItem>Two</TabItem> <TabItem>Three</TabItem> </Tabs>
Tabs 虽然可以访问到作为 props 的 children,但是到手的 children 已经是创造好的元素,而且是不可改变的,Tabs 是不可能把创造好的元素再强塞给 children 的。
如果 Tabs 并不去渲染 children,而是把 children 拷贝一份,就有机会去篡改这份拷贝,最后渲染这份拷贝就好了。
class Tabs extends React.Component {
state = {
activeIndex: 0
}
render() {
const newChildren = React.Children.map(this.props.children, (child, index) => {
if (child.type) {
return React.cloneElement(child, {
active: this.state.activeIndex === index,
onClick: () => this.setState({activeIndex: index})
});
} else {
return child;
}
});
return (
<Fragment>
{newChildren}
</Fragment>
);
}
}
使用 React.Children.map,可以遍历 children 中所有的元素,因为 children 可能是一个数组嘛。
使用 React.cloneElement 可以复制某个元素。这个函数第一个参数就是被复制的元素,第二个参数可以增加新产生元素的 props,我们就是利用这个机会,把 active 和 onClick 添加了进去。
这两个 API 双剑合璧,就能实现不通过表面的 props 传递,完成两个组件的“组合”。
而维护哪个 TabItem 是当前选中的状态,则是 Tabs 的责任。
对于组合组件这种实现方式,TabItem 非常简化;Tabs 稍微麻烦了一点,但是好处就是把复杂度都封装起来了,从使用者角度,连 props 都看不见。
应用组合组件的往往是共享组件库,把一些常用的功能封装在组件里,让应用层直接用就行。在 antd 和 bootstrap 这样的共享库中,都使用了组合组件这种模式。
如果你的某两个组件并不需要重用,那么就要谨慎使用组合组件模式,毕竟这让代码复杂了一些。
如果要开发需要关联的成对组件,可以采用这个方案。
对代码进行测试是最佳实践,可以保证代码质量
测试就是尽力发现软件中的缺陷(俗称 bug),当我们发现不了更多的 bug 时,说明这个软件质量可以接受了。
测试是尽力发现软件中的 bug。当我们发现 bug 数量和严重程度呈稳定的下降趋势,直到低于一个门槛(无须降低为 0,只需要降低到可接受的程度),没有更多更严重的 bug 出现,就说明这个软件的质量可以接受,可以上线了。
对”小块代码“的测试,也就是单元测试。
Mocha 之类老牌单元测试框架,把所有的单元测试都放在一个环境中执行,这就使所有单元测试访问的是同样一个全局变量空间,所以只要测试代码没写好,就会互相影响。而且,为了保证执行正常,所有的单元测试必须一个接一个地执行,这是体系架构决定的,没有办法。
Jest 不同,Jest 为每一个单元测试文件创造一个独立的运行环境,换句话说,Jest 会启动一个进程执行一个单元测试文件,运行结束之后,就把这个执行进程废弃了,这个单元测试文件即使写得比较差,把全局变量污染得一团糟,也不会影响其他单元测试文件,因为其他单元测试文件是用另一个进程来执行。
Jest 最重要的一个特性,就是支持并行执行
因为每个单元测试文件之间再无纠葛,Jest 可以启动多个进程同时运行不同的文件,这样就充分利用了电脑的多 CPU 多核
使用 create-react-app 产生的项目自带 Jest 作为测试框架,不奇怪,因为 Jest 和 React 一样都是出自 Facebook。
运行下面的命令,就可以进入交互式的”测试驱动开发“模式
npm test
虽然最好的 React 测试框架出自 Facebook 家,最受欢迎的 React 测试工具库却出自 Airbnb,这个工具库叫做 Enzyme。Enzyme 这个单词的含义是“酶”,至于命名原因已经无法考据,可能寓意着快速分解。
安装
npm i --save-dev enzyme enzyme-adapter-react-16
enzyme-adapter-react-16,这个库是用来作为适配器的。
- 因为不同 React 版本有各自特点,所用的适配器也会不同,
- 我们的项目中使用的是 16.4 之后的版本,所以用 enzyme-adapter-react-16;
- 如果用 16.3 版本,需要用 enzyme-adapter-react-16.3;
- 如果用 16.2 版本,需要用 enzyme-adapter-react-16.2;
- 如果用更老的版本 15.5,需要用 enzyme-adapter-react-15。
- 具体各个 React 版本对应什么样的 Adapter,请参考 enzyme官方文档。
- https://airbnb.io/enzyme/#installation
以之前秒表应用中的 ControlButtons 组件为例 创造一个 ControlButtons.test.js,来容纳对应的测试用例,因为所有后缀为 .test.js 的文件都会被 Jest 认作是测试用例文件。 在代码中,需要使用 Adapter
import {configure} from 'enzyme'; import Adapter from 'enzyme-adapter-react-16'; configure({adapter: new Adapter()}); `` 我们对 ControlButtons 组件的测试,就是要渲染它一次,看看渲染结果如何,enzyme 就能帮助我们做这件事。 比如,我们想要保证渲染出来的内容必须包含两个按钮,其中一个按钮的 class 名是 left-btn,另一个是 right-btn,那么我们就需要下面的单元测试用例: ```ts import {shallow} from 'enzyme';
it('renders without crashing', () => {
const wrapper = shallow(
>shallow 和 mount 的区别,就是 shallow 只会渲染被测试的 React 组件这一层,不会渲染子组件;
>而 mount 则是完整地渲染 React 组件包括其所有子组件,包括触发 componentDidMount 生命周期函数。
>原则上,能用 shallow 就尽量用 shallow,首先是为了测试性能考虑,其次是可以减少组件之间的影响
```ts
const Foo = () => ()
<div>
{/* other logic */
<Bar />
</div>
)
如果用 mount 去渲染 Foo,会连带 Bar 一起完全渲染,如果 Bar 出了什么毛病,那 Foo 的单元测试也过不了;如果用 shallow,只知道 Bar 曾经被用,即使 Bar 哪里出了问题,也不影响 Foo 的单元测试。
代码覆盖率
代码覆盖率必须达到 100%,也就是说,一个应用不光所有的单元测试都要通过,而且所有单元测试都必须覆盖到代码 100% 的角落。
在 create-react-app 创造的应用中,已经自带了代码覆盖率的支持,运行下面的命令,不光会运行所有单元测试,也会得到覆盖率汇报。
npm test -- --coverage
代码覆盖率包含四个方面:
- 语句覆盖率
- 逻辑分支覆盖率
- 函数覆盖率
- 代码行覆盖率
只有四个方面都是 100%,才算真的 100%。
UI = f(data) f 的参数 data,除了 props,就是 state。props 是组件外传递进来的数据,state 代表的就是 React 组件的内部状态。
对于 React 组件而言,数据分为两种:
- props
- state
props 是外部传给组件的数据,而 state 是组件自己维护的数据,对外部是不可见的。
判断某个数据以 props 方式存在,还是以 state 方式存在,并不难,只需要判断这个状态是否是组件内部状态。
数据存在 this.foo 中,而不是存在 this.state.foo 中,当这个组件渲染的时候,当然 this.foo 的值也就被渲染出来了,问题是,更新 this.foo 并不会引发组件的重新渲染
判断一个数据应该放在哪里,用下面的原则:
- 如果数据由外部传入,放在 props 中;
- 如果是组件内部状态,是否这个状态更改应该立刻引发一次组件重新渲染?如果是,放在 state 中;不是,放在成员变量中。
简单说来,调用 setState 之后的下一行代码,读取 this.state 并不是修改之后的结果。
React 非常巧妙地用任务队列解决了这个问题,可以理解为每次 setState 函数调用都会往 React 的任务队列里放一个任务,多次 setState 调用自然会往队列里放多个任务。React 会选择时机去批量处理队列里执行任务,当批量处理开始时,React 会合并多个 setState 的操作
setTimeout(() => {
this.setState({count: 2}); //这会立刻引发重新渲染
console.log(this.state.count); //这里读取的count就是2
}, 0);
当 React 调用某个组件的生命周期函数或者事件处理函数时,React 会想:“嗯,这一次函数可能调用多次 setState,我会先打开一个标记,只要这个标记是打开的,所有的 setState 调用都是往任务队列里放任务,当这一次函数调用结束的时候,我再去批量处理任务队列,然后把这个标记关闭。”
因为 setTimeout 是一个 JavaScript 函数,和 React 无关,对于 setTimeout 的第一个函数参数,这个函数参数的执行时机,已经不是 React 能够控制的了,换句话说,React 不知道什么时候这个函数参数会被执行,所以那个“标记”也没有打开。
当那个“标记”没有打开时,setState 就不会给任务列表里增加任务,而是强行立刻更新 state 和引发重新渲染。这种情况下,React 认为:“这个 setState 发生在自己控制能力之外,也许开发者就是想要强行同步更新呢,宁滥勿缺,那就同步更新了吧。”
React 选择不同步更新 state,是一种性能优化,如果你用上 setTimeout,就没机会让 React 优化了。
每当你觉得需要同步更新 state 的时候,往往说明你的代码设计存在问题,绝大部分情况下,你所需要的,并不是“state 立刻更新”,而是,“确定 state 更新之后我要做什么”
setState 的第二个参数可以是一个回调函数,当 state 真的被修改时,这个回调函数会被调用。 当 setState 的第二个参数被调用时,React 已经处理完了任务列表,所以 this.state 就是更新后的数据。
console.log(this.state.count); // 0 this.setState({count: 1}, () => { console.log(this.state.count); // 这里就是1了 }) console.log(this.state.count); // 依然为0
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
上面的代码表面上看会让 this.state.count 增加 3,实际上只增加了 1,因为 setState 没有同步更新 this.state 啊,所以给任务队列加的三个任务都是给 this.state.count 同一个值而已。
当 setState 的第一个参数为函数时,任务列表上增加的就是一个可执行的任务函数了,React 每处理完一个任务,都会更新 this.state,然后把新的 state 传递给这个任务函数。
setState 第一个参数的形式如下:
function increment(state, props) { return {count: state.count + 1}; }
这是一个纯函数,不光接受当前的 state,还接受组件的 props,在这个函数中可以根据 state 和 props 任意计算,返回的结果会用于修改 this.state。
如此一来,我们就可以这样连续调用 setState:
this.setState(increment); this.setState(increment); this.setState(increment);
用这种函数式方式连续调用 setState,就真的能够让 this.state.count 增加 3,而不只是增加 1。
创建一个独立于这两个组件的对象,在这个对象中存放共享的数据,没错,这个对象,相当于一个 Store。
如果只是一个简单对象,那么任何人都可以修改 Store,这不大合适。所以我们做出一些限制,让 Store 只接受某些『事件』,如果要修改 Store 上的数据,就往 Store 上发送这些『事件』,Store 对这些『事件』的响应,就是修改状态。
这里所说的『事件』,就是 action,而对应修改状态的函数,就是 reducer。
第一步,看这个状态是否会被多个 React 组件共享。
所谓共享,就是多个组件需要读取或者修改这个状态,如果是,那不用多想,应该放在 Store 上,因为 Store 上状态方便被多个组件共用,避免组件之间传递数据;如果不是,继续看第二步。
第二步,看这个组件被 unmount 之后重新被 mount,之前的状态是否需要保留。
举个简单例子,一个对话框组件。用户在对话框打开的时候输入了一些内容,不做提交直接关闭这个对话框,这时候对话框就被 unmount 了,然后重新打开这个对话框(也就是重新 mount),需求是否要求刚才输入的内容依然显示?如果是,那么应该把状态放在 Store 上,因为 React 组件在 unmount 之后其中的状态也随之消失了,要想在重新 mount 时重获之前的状态,只能把状态放在组件之外,Store 当然是一个好的选择;如果需求不要求重新 mount 时保持 unmount 之前的状态,继续看第三步。
第三步,到这一步,基本上可以确定,这个状态可以放在 React 组件中了。
更好的方法,是把源代码文件分类放在不同的目录中,根据分类方式,可以分为两种:
- 基于角色的分类(role based)
- 基于功能的分类(feature based)
基于角色的分类 把所有 reducer 放在一个目录(通常就叫做 reducers),把所有 action 放在另一个目录(通常叫 actions),最后,把所有的纯 React 组件放在另一个目录。
基于功能的分类方式,是把一个模块相关的所有源代码放在一个目录。 例如,对于博客系统,有 Post(博客文章)和 Comment(注释)两个基本模块,建立两个目录 Post 和 Comment,每个目录下都有各自的 action.js 和 reducer.js 文件,如下所示,每个目录都代表一个模块功能,这就是基于功能的分类方式。
Post -- action.js |_ reucer.js |_ view.js Comment -- action.js |_ reucer.js |_ view.js
基于功能的分类方式更优。因为每个目录是一个功能的封装,方便共享
安装
npm install redux react-redux
react-redux 就是『提供者模式』的实践。在组件树的一个比较靠近根节点的位置,我们通过 Provider 来引入一个 store
import {createStore} from 'redux'; import {Provider} from 'react-redux';
const store = createStore(...);
// JSX
>这个 Provider 当然也是利用了 React 的 Context 功能。在这个 Provider 之下的所有组件,如果使用 connect,那么『链接』的就是 Provider 的 state。
>connect 的用法,首先,我们需要一个『傻瓜组件』,可以由纯函数实现
```tsx
const CounterView = ({count, onIncrement}) => {
return (
<div>
<div>{count}</div>
<button onClick={onIncrement}>+</button>
</div>
);
};
把 CounterView 和 store 连接起来
import {connect} from 'react-redux';
const mapStateToProps = (state) => { return { count: state.count }; }
const mapDispatchToProps = (dispatch) => ({ onIncrement: () => dispatch({type: 'INCREMENT'}) });
const Counter = connect(mapStateToProps, mapDispatchToProps)(CounterView);
>这里的 connect 函数接受两个参数,一个 mapStateToProps 是把 Store 上的 state 映射为 props;另一个 mapDispatchToProps 则是把回调函数类型的 props 映射为派发 action 的动作,connect 函数调用会产生一个『高阶组件』。
>connect 产生的高阶组件产生了一个新的 React 组件 Counter,这个 Counter 其实就是一个『聪明组件』,它负责管理状态,而 CounterView 是一个『傻瓜组件』,只负责渲染。
>在 react-redux 中,应用了三个 React 模式:
> - 提供者模式
> - 高阶组件
> - 聪明组件和傻瓜组件的分离
***
##### Redux 和 React 结合的最佳实践
>1-Store 上的数据应该范式化。
>所谓范式化,就是尽量减少冗余信息,像设计 MySQL 这样的关系型数据库一样设计数据结构。
>2-使用 selector。
>对于 React 组件,需要的是『反范式化』的数据,当从 Store 上读取数据得到的是范式化的数据时,需要通过计算来得到反范式化的数据。你可能会因此担心出现问题,这种担心不是没有道理,毕竟,如果每次渲染都要重复计算,这种浪费积少成多可能真会产生性能影响,所以,我们需要使用 seletor。业界应用最广的 selector 就是 reslector 。
>reselector 的好处,是把反范式化分为两个步骤,第一个步骤是简单映射,第二个步骤是真正的重量级运算,如果第一个步骤发现产生的结果和上一次调用一样,那么第二个步骤也不用计算了,可以直接复用缓存的上次计算结果。
>3-只 connect 关键点的 React 组件
当 Store 上状态发生改变的时候,所有 connect 上这个 Store 的 React 组件会被通知:『状态改变了!』
>然后,这些组件会进行计算。connect 的实现方式包含 shouldComponentUpdate 的实现,可以阻挡住大部分不必要的重新渲染,但是,毕竟处理通知也需要消耗 CPU,所以,尽量让关键的 React 组件 connect 到 store 就行。
>一个实际的例子就是,一个列表种可能包含几百个项,让每一个项都去 connect 到 Store 上不是一个明智的设计,最好是只让列表去 connect,然后把数据通过 props 传递给各个项。
>使用 react-redux 的话,虽然 Provider 可以嵌套,但是,最里层的 Provider 提供的 store 才生效。
***
##### 如何实现异步操作
>最简单的 redux-thunk,代码量少,只有几行,用起来也很直观,但是开发者要写很多代码;
>而比较复杂的 redux-observable 相当强大,可以只用少量代码就实现复杂功能,但是前提是你要学会 RxJS,RxJS 本身学习曲线很陡,内容需要 一本书 的篇幅来介绍,这就是代价
要实现“单页应用”,一个最要紧的问题就是做好“路由”(Routing),也就是处理好下面两件事:
- 把 URL 映射到对应的页面来处理;
- 页面之间切换做到只需局部更新。;
react router v4 的动态路由
动态路由,指的是路由规则不是预先确定的,而是在渲染过程中确定的
安装 react-router-dom 依赖于 react-router ,所以 react-router 也会被自动安装上。
npm install react-router-dom
react-router 的工作方式,是在组件树顶层放一个 Router 组件,然后在组件树中散落着很多 Route 组件(注意比 Router 少一个“r”),顶层的 Router 组件负责分析监听 URL 的变化,在它保护伞之下的 Route 组件可以直接读取这些信息。
Router 和 Route 的配合,就是“提供者模式”,Router 是“提供者”,Route是“消费者”。
Router 其实也是一层抽象,让下面的 Route 无需各种不同 URL 设计的细节
第一种很自然,比如 / 对应 Home 页,/about 对应 About 页,但是这样的设计需要服务器端渲染,因为用户可能直接访问任何一个 URL,服务器端必须能对 /的访问返回 HTML,也要对 /about 的访问返回 HTML。
HashRouter
第二种看起来不自然,但是实现更简单。只有一个路径 /,通过 URL 后面的 # 部分来决定路由,/#/ 对应 Home 页,/#/about 对应 About 页。因为 URL 中#之后的部分是不会发送给服务器的,所以,无论哪个 URL,最后都是访问服务器的 / 路径,服务器也只需要返回同样一份 HTML 就可以,然后由浏览器端解析 # 后的部分,完成浏览器端渲染。
把 Router 用在 React 组件树的最顶层,这是最佳实践。
import {HashRouter} from 'react-router-dom';
ReactDOM.render(
依赖库打包 umd.zip
antd.DatePicker
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<meta charset="UTF-8">
<title>ant design in html</title>
<link rel="stylesheet" href="./umd/antd.css">
<script src="./umd/react.production.min.js"></script>
<script src="./umd/react-dom.production.min.js"></script>
<script src="./umd/browser5.8.24.js"></script>
<script src="./umd/moment-with-locales.js"></script>
<script src="./umd/antd-with-locales.js"></script>
<script></script>
</head>
<body>
<div id="date"></div>
<script>
moment.locale('zh-cn');
const dateDom = document.querySelector('#date')
const reactEle = React.createElement(antd.DatePicker, {
onChange: (e) => {
console.log(e)
console.log(e._d)
console.log(new Date(e._d).toLocaleTimeString())
}
})
ReactDOM.render(reactEle, dateDom)
</script>
</body>
</html>
antd.Table
<!DOCTYPE html> <html lang="zh-cn"> <head> <meta charset="UTF-8"> <title>ant design in html</title> <link rel="stylesheet" href="./umd/antd.css"> <script src="./umd/react.production.min.js"></script> <script src="./umd/react-dom.production.min.js"></script> <script src="./umd/browser5.8.24.js"></script> <script src="./umd/moment-with-locales.js"></script> <script src="./umd/antd-with-locales.js"></script> <script></script> </head> <body>
cd my-app npm start
npx create-react-app my-app
cd my-app npm start
You can now view react-app in the browser.
Local: http://localhost:3000/ On Your Network: http://192.168.1.129:3000/
Note that the development build is not optimized. To create a production build, use npm run build.
let comments = [ {'author': 'tom', 'comment': 'hello', 'text': '123'}, {'author': 'lili', 'comment': 'bucuo', 'text': '555'}, ]
class App extends Component { render() { return (
Welcome to React
To get started, edit
src/App.js
and save to reload.你好
} }
export default App;
class Comment extends Component { render() { let commentNodes = this.props.data.map((comment, index) => { return (
{comment.author}
{comment.comment}
} }
export default Comment
class Box extends Component { constructor(props) { super(props) this.state = {data: []} this.getComments() setInterval(() => { this.getComments() }, 5000) }
getComments() { fetch(this.props.url, { headers: { 'Content-Type': 'application/json', 'Accept': 'application/json' } }).then((response) => { return response.json() }).then((comments) => { this.setState({data: comments}) }) }
render() { return (
hello box
} }
export default Box
class Comment extends Component { handleSubmit(event) { event.preventDefault() let text = this.refs.text.value console.log(text) }
render() { let commentNodes = this.props.data.map((comment, index) => { return (
{comment.author}
{comment.comment}
} }
export default Comment
class Comment extends Component { handleSubmit(event) { event.preventDefault() let text = this.refs.text.value console.log(text) this.props.onCommentSubmit({text}) }
render() { let commentNodes = this.props.data.map((comment, index) => { return (
{comment.author}
{comment.comment}
} }
export default Comment
class Comment extends Component { handleSubmit(event) { event.preventDefault() let author = this.refs.author.value let comment = this.refs.comment.value console.log(author) this.props.onCommentSubmit({author, comment}) }
render() { let commentNodes = this.props.data.map((comment, index) => { return (
{comment.author}
{comment.comment}
} }
export default Comment