核心概念
高级指引
1️⃣ 无障碍
JSX 支持所有 aria-* HTML 属性。虽然大多数 React 的 DOM 变量和属性命名都使用驼峰命名(camelCased),但 aria-* 应该像其在 HTML 中一样使用带连字符的命名法
<input
type="text"
aria-label={labelText}
aria-required="true"
onChange={onchangeHandler}
value={inputValue}
name="name"
/>
2️⃣ 代码分割
打包
使用webpack、Rollup等构建工具,把多文件互相引入合并到一个单文件bundle,整个应用可以一次性加载
import
最佳引入代码的方式
React.lazy
在组件首次渲染时,再导入相关的包
import React, { Suspense, lazy } from 'react';
const OtherComponent = lazy(() => import('./OtherComponent'));
然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级
fallback 属性接受任何在组件加载过程中你想展示的 React 元素
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
在外层包裹一层异常捕获边界,可增强用户体验
3️⃣ Context
组件间跨层级的数据传递方式
何时使用?
共享数据,避免中间元素传递
使用context的顾虑
context会使组件的可复用性变差,有时候比context更好的操作:
- 组件组合
- 把组件作为参数传递
API
React.createContext
const MyContext = React.createContext(defaultValue);
Context.Provider
<MyContext.Provider value={/* 某个值 */}>
- 可以嵌套,但是消费者只能取到距离自身最近的value
- Provider 及其内部 consumer 组件都不受制于
shouldComponentUpdate函数 - ❗️不要在value中使用对象,最好存放于state中,提高性能
Class.contextType
// 也可写成以下形式
static contextType = MyContext;
- 订阅最近的value
Context.Consumer
<MyContext.Consumer>
{context => {/* 基于 context 值进行渲染*/}}
</MyContext.Consumer>
- 可嵌套使用
Context.displayName
DevTools 中将显示为 MyDisplayName
const MyContext = React.createContext(/* some value */);
MyContext.displayName = 'MyDisplayName';
4️⃣ 错误边界
(React 16 引入)错误边界是一种 React 组件,这种组件可以捕获并打印发生在其子组件树任何位置的 JavaScript 错误,并且,它会渲染出备用 UI,而不是渲染那些崩溃了的子组件树。错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
❗️无法捕获:
- 事件:可使用JavaScript中的try-catch
- 异步代码
- 服务器渲染
- 自身组件抛出的错误(仅捕获子组件错误)
如果一个 class 组件中定义了 static getDerivedStateFromError()或 componentDidCatch()这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。
当抛出错误后,请使用 static getDerivedStateFromError() 渲染备用 UI ,使用 componentDidCatch() 打印错误信息。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染能够显示降级后的 UI
return { hasError: true };
}
componentDidCatch(error, errorInfo) {
// 你同样可以将错误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {
if (this.state.hasError) {
// 你可以自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
5️⃣ Refs转发
对于可重用的组件库、高阶组件非常有用。
Ref 转发是一个可选特性,其允许某些组件接收 ref,并将其向下传递(换句话说,“转发”它)给子组件。
// 使用 React.forwardRef 来获取传递给它的 ref,然后转发到它渲染的 DOM button
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
// 创建一个React ref并向下传递
const ref = React.createRef();
<FancyButton ref={ref}>Click me!</FancyButton>;
// button挂载完成后,ref.current 将指向 <button> DOM 节点
这样,使用 FancyButton 的组件可以获取底层 DOM 节点 button 的 ref ,并在必要时访问,就像其直接使用 DOM button 一样
在HOC组件中使用
❗️如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。我们可以使用 React.forwardRef API 明确地将 refs 转发到内部的 FancyButton 组件
function logProps(Component) {
class LogProps extends React.Component {
componentDidUpdate(prevProps) {
console.log('old props:', prevProps);
console.log('new props:', this.props);
}
render() {
const {forwardedRef, ...rest} = this.props;
// 将自定义的 prop 属性 “forwardedRef” 定义为 ref
return <Component ref={forwardedRef} {...rest} />;
}
}
// 注意 React.forwardRef 回调的第二个参数 “ref”。
// 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
// 然后它就可以被挂载到被 LogProps 包裹的子组件上。
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
6️⃣ Fragment
将子列表分组,无需添加额外DOM元素。
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
短语法
<>
<td>Hello</td>
<td>World</td>
</>
7️⃣ 高阶组件HOC
——参数为组件,返回值为新组件的函数
- 组件:将 props 转换为 UI
- 高阶组件:将组件转换为另一个组件
使用HOC解决横切关注点问题
我们需要一个抽象,允许我们在一个地方定义这个逻辑,并在许多组件之间共享它。我们可以编写一个创建组件函数。该函数将接受一个子组件作为它的其中一个参数,该子组件将订阅数据作为 prop。
const CommentListWithSubscription = withSubscription(
CommentList,
(DataSource) => DataSource.getComments()
);
const BlogPostWithSubscription = withSubscription(
BlogPost,
(DataSource, props) => DataSource.getBlogPost(props.id)
);
‼️ HOC 不会修改传入的组件,也不会使用继承来复制其行为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作用,使用组合
约定:将不相关的props传递给被包裹的组件
render() {
// 过滤掉非此 HOC 额外的 props,且不要进行透传
const { extraProp, ...passThroughProps } = this.props;
// 将 props 注入到被包装的组件中。
// 通常为 state 的值或者实例方法。
const injectedProp = someStateOrInstanceMethod;
// 将 props 传递给被包装组件
return (
<WrappedComponent
injectedProp={injectedProp}
{...passThroughProps}
/>
);
}
约定:最大化可组合性
HOC 通常可以接收多个参数
const ConnectedComment = connect(commentSelector, commentActions)(CommentList);
// 如果你把它分开,就会更容易看出发生了什么
// connect 是一个函数,它的返回值为另外一个函数。
const enhance = connect(commentListSelector, commentListActions);
// 返回值为 HOC,它会返回已经连接 Redux store 的组件
const ConnectedComment = enhance(CommentList);
约定:包装显示名称以便轻松调试
‼️ 注意
不要在render中使用HOC
diff算法中需要判断组件是否相同,决定是否递归更新子树
HOC每次会产生新组件,导致不断卸载-加载新子树
原组件上的静态方法不会跟随到新组件上
// 定义静态函数
WrappedComponent.staticMethod = function() {/*...*/}
// 现在使用 HOC
const EnhancedComponent = enhance(WrappedComponent);
// 增强组件没有 staticMethod
typeof EnhancedComponent.staticMethod === 'undefined' // true
需要把静态方法拷贝到组件上
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// 必须准确知道应该拷贝哪些方法 :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
或者把静态方法导出,按需导入
// 使用这种方式代替...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...单独导出该方法...
export { someFunction };
// ...并在要使用的组件中,import 它们
import MyComponent, { someFunction } from './MyComponent.js';
refs不会被传递
跟props中的key一样
8️⃣ 与第三方库协同
介绍了jQuery、Backbone
9️⃣ 深入JSX
JSX 仅仅只是 React.createElement(component, props, ...children) 函数的语法糖
React.createElement(
MyButton,
{color: 'blue', shadowSize: 2},
'Click Me'
)
所以每个使用了jsx的文件,必须引入React(即使没有看到直接调用的地方)。如果你不使用 JavaScript 打包工具而是直接通过 <script> 标签加载 React,则必须将 React 挂载到全局变量中。
参数1:type
-
react组件必须使用大写字母开头的标签,用户自定义的任何类型组件都必须大写字母开头
-
可以使用点语法来引用一个 React 组件
<MyComponents.DatePicker color="blue" /> -
动态决定元素类型:类型可以是变量,不可以是表达式
function Story(props) { // 错误!JSX 类型不能是一个表达式。 return <components[props.storyType] story={props.story} />; function Story(props) { // 正确!JSX 类型可以是大写字母开头的变量。 const SpecificStory = components[props.storyType]; return <SpecificStory story={props.story} />; }
参数2:props
-
可以使用表达式(if、for不是表达式)、字面量
<MyComponent foo={1 + 2 + 3 + 4} /> <MyComponent message="hello world" /> -
如果你没给 prop 赋值,它的默认值是
true,通常,我们不建议这样使用,因为它可能与 ES6 对象简写混淆 -
属性展开
const props = {firstName: 'Ben', lastName: 'Hector'}; const { kind, ...other } = props; <Greeting {...props} />
参数3:childrens
类型
- 字面量
<div>Hello World</div>
- jsx元素
<MyContainer>
<MyFirstComponent />
<MySecondComponent />
</MyContainer>
- 表达式
<li>{props.message}</li>
- 函数❗️
<Repeat numTimes={10}>
{(index) => <div key={index}>item{index}</div>}
</Repeat>
❗️布尔类型、Null 以及 Undefined 将会忽略
🔟 性能优化
使用生产版本的react
配合打包工具,或者在单文件中引入js库
使用 Chrome Performance 标签分析组件
在开发模式下,你可以通过支持的浏览器可视化地了解组件是如何 挂载、更新以及卸载的
使用开发者工具中的分析器对组件进行分析
在Chrome中安装扩展
虚拟化长列表
使用虚拟滚动库,在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建 DOM 节点的数量
避免调停
使用虚拟dom与diff算法,判断是否真的需要更新。
可以通过覆盖生命周期方法 shouldComponentUpdate 来进行提速
shouldComponentUpdate 的作用
返回false,则组件被调停(子组件也不进行判断),默认为true。
❗️在大部分情况下,你可以继承 React.PureComponent以代替手写 shouldComponentUpdate()。它用当前与之前 props 和 state 的浅比较覆写了 shouldComponentUpdate() 的实现。
‼️浅比较针对复杂数据结构时会报错
不可变数据的力量
function updateColorMap(colormap) {
return Object.assign({}, colormap, {right: 'blue'});
}
updateColorMap 返回了一个新的对象,而不是修改老对象。
❗️Object.assign 是 ES6 的方法,需要 polyfill。
⭕️ 参考 Immer 或 immutability-helper,帮助你编写高可读性的代码,且不会失去 immutability (不可变性)带来的好处。
1️⃣1️⃣ Portals
——将子节点渲染到父组件以外的DOM节点上,常用于对话框、悬浮卡、提示框
ReactDOM.createPortal(child, container)
❗️注意:挂载、卸载、渲染
class Modal extends React.Component {
constructor(props) {
super(props);
this.el = document.createElement('div');
}
componentDidMount() {
// 挂载-appendChild
modalRoot.appendChild(this.el);
}
componentWillUnmount() {
// 卸载-removeChild
modalRoot.removeChild(this.el);
}
render() {
// 渲染-ReactDOM.createPortal
return ReactDOM.createPortal(
this.props.children,
this.el,
);
}
}
事件冒泡❗️
portal中触发的事件会一直冒泡到react树的祖先(即使不是DOM树祖先)
1️⃣2️⃣ Profiler
- 测量渲染一次时间和代价
- 识别渲染较慢的部分
❗️增加额外开支,生产构建时被禁用
// 支持多个、嵌套
render(
<App>
<Profiler id="Navigation" onRender={callback}>
<Navigation {...props} />
</Profiler>
<Main {...props} />
</App>
);
onRender 回调
function onRenderCallback(
id, // 发生提交的 Profiler 树的 “id”
phase, // "mount" (如果组件树刚加载) 或者 "update" (如果它重渲染了)之一
actualDuration, // 本次更新 committed 花费的渲染时间
baseDuration, // 估计不使用 memoization 的情况下渲染整颗子树需要的时间
startTime, // 本次更新中 React 开始渲染的时间
commitTime, // 本次更新中 React committed 的时间
interactions // 属于本次更新的 interactions 的集合
) {
// 合计或记录渲染时间。。。
}
1️⃣3️⃣ 不使用ES6
不使用ES6的class关键字,需要引入create-react-class模块,使用createReactClass方法创建类
var createReactClass = require('create-react-class');
var Greeting = createReactClass({
render: function() {
return <h1>Hello, {this.props.name}</h1>;
}
});
❗️与class组件的不同
声明默认属性 - getDefaultProps
Greeting.defaultProps = {
name: 'Mary'
};
//----------------------
var Greeting = createReactClass({
getDefaultProps: function() {
return {
name: 'Mary'
};
},
});
初始化State - getInitialState
var Counter = createReactClass({
getInitialState: function() {
return {count: this.props.initialCount};
},
});
事件自动绑定到this
// 不需要
this.handleClick = this.handleClick.bind(this);
❗️mixins
ES6本身不支持mixin,所以使用ES6 class时不支持mixins。不建议在新代码中使用。
如果完全不同的组件有相似的功能,这就会产生“横切关注点(cross-cutting concerns)“问题。针对这个问题,在使用 createReactClass 创建 React 组件的时候,引入 mixins 功能会是一个很好的解决方案。
var SetIntervalMixin = {
componentWillMount: function() {
this.intervals = [];
},
setInterval: function() {
this.intervals.push(setInterval.apply(null, arguments));
},
componentWillUnmount: function() {
this.intervals.forEach(clearInterval);
}
};
var TickTock = createReactClass({
mixins: [SetIntervalMixin], // 使用 mixin
componentDidMount: function() {
this.setInterval(this.tick, 1000); // 调用 mixin 上的方法
},
render: function() {
return <p>React </p>
}
});
如果组件拥有多个 mixins,且这些 mixins 中定义了相同的生命周期方法,那么这些生命周期方法都会被调用的。使用 mixins 时,mixins 会先按照定义时的顺序执行,最后调用组件上对应的方法。
1️⃣4️⃣ 不使用JSX
当你不想在构建环境中配置有关 JSX 编译时,不在 React 中使用 JSX 会更加方便。可以创建快捷方式,使键入更方便:
const e = React.createElement;
ReactDOM.render(
e('div', null, 'Hello World'),
document.getElementById('root')
);
1️⃣5️⃣ 协调‼️‼️‼️
设计动力
更新UI的方法:
- render时,创建一颗新树
- update时,创建一颗新树
- 对比两颗树的差异,同步UI(问题:对比算法代价太高)
react基于以下两个假设,把算法复杂度由O(n^3 )降为O(n):
- 两个不同类型的元素会产生出不同的树;
- 通过
keyprop 来暗示哪些子元素在不同的渲染下能保持稳定;
Diffing算法
元素类型不同:拆掉原有的树(包括所有子组件),建立新的树
执行:componentWillUnmount(),componentWillMount() ,componentDidMount()
<div>
<Counter />
</div>
<span>
<Counter />
</span>
元素类型相同:保留DOM节点,对比更新属性
执行: componentWillReceiveProps() , componentWillUpdate()
<div className="before" title="stuff" />
<div className="after" title="stuff" />
子节点递归
在默认条件下,当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation。
-
在子元素列表末尾新增元素时,变更开销比较小;
-
在列表头部插入会很影响性能,那么更变开销会比较大
keys
——可以解决以上问题
<ul>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
<ul>
<li key="2014">Connecticut</li>
<li key="2015">Duke</li>
<li key="2016">Villanova</li>
</ul>
这个 key 不需要全局唯一,但在列表中需要保持唯一。
❗️不推荐用index作为key,会使diff性能变差,也会遇到一些问题
权衡
请谨记协调算法是一个实现细节:React 可以在每个 action 之后对整个应用进行重新渲染,得到的最终结果也会是一样的。在此情境下,重新渲染表示在所有组件内调用 render 方法,这不代表 React 会卸载或装载它们。React 只会基于以上提到的规则来决定如何进行差异的合并。
以下假设没有得到满足,性能会有所损耗:
- 该算法不会尝试匹配不同组件类型的子树。
- 如果你发现你在两种不同类型的组件中切换,但输出非常相似的内容,建议把它们改成同一类型。
- 在实践中,我们没有遇到这类问题。
- Key 应该具有稳定,可预测,列表内唯一的特质。不稳定的 key(比如通过
Math.random()生成的)会导致许多组件实例和 DOM 节点被不必要地重新创建,这可能导致性能下降和子组件中的状态丢失。
1️⃣6️⃣ Refs & DOM
refs可以访问DOM节点、react元素
何时使用?
- 管理焦点,文本选择或媒体播放。
- 触发强制动画。
- 集成第三方 DOM 库。
使用refs
❗️ 创建并附加在react元素上
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
❗️ 访问
const node = this.myRef.current;
// 当ref属性用于HTML元素时,current为底层DOM元素
// 当ref属性用于自定义class组件时,current为组件的挂载实例
‼️你不能在函数组件上使用 ref 属性,因为他们没有实例。你可以在函数组件内部使用 ref 属性,只要它指向一个 DOM 元素或 class 组件。
‼️如果想把refs暴露给父组件,推荐使用Ref转发。Ref 转发使组件可以像暴露自己的 ref 一样暴露子组件的 ref。
1️⃣7️⃣ Render Props
接受一个函数,返回调用此函数的值
<DataProvider render={mouse => (
<Cat mouse={mouse} />
)}/>
DataProvider中
render() {
return (
<div onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
);
}
解决横切关注点
**render prop 是一个用于告知组件需要渲染什么内容的函数 prop。**你可以使用带有 render prop 的常规组件来实现大多数高阶组件 (HOC)
❗️ 任何被用于告知组件需要渲染什么内容的函数 prop 在技术上都可以被称为 “render prop”.
1️⃣8️⃣ 静态类型检查
建议在大型代码库中使用flow或TS来代替PropTypes
Flow
现已不维护,跳过
TypeScript
-
安装typeScript
npm install --save-dev typescript -
配置typescript编译器,
tsconfig.jsonnpx tsc --init -
文件拓展名:
.ts,.tsx
1️⃣9️⃣ 严格模式
与 Fragment 一样,StrictMode 不会渲染任何可见的 UI。它为其后代元素触发额外的检查和警告。
function ExampleApplication() {
return (
<div>
<React.StrictMode>
<div>
<ComponentOne />
<ComponentTwo />
</div>
</React.StrictMode>
</div>
);
}
目前有助于:
- 识别不安全的生命周期
- 关于使用过时字符串 ref API 的警告
- 关于使用废弃的 findDOMNode 方法的警告
- 检测意外的副作用
- 检测过时的 context API
2️⃣0️⃣ 使用 PropTypes 进行类型检查
❗️请使用 prop-types库
为了通过类型检查捕获大量错误,大型项目可以使用flow或者ts。不使用这些扩展,可以使用react提供的类型检查功能。
import PropTypes from 'prop-types';
class Greeting extends React.Component {
render() {
return (
<h1>Hello, {this.props.name}</h1>
);
}
}
Greeting.propTypes = {
name: PropTypes.string,
children: PropTypes.element.isRequired //限制:只能有1个元素
};
// 指定 props 的默认值:
Greeting.defaultProps = {
name: 'Stranger'
};
2️⃣1️⃣ 非受控组件
因为非受控组件将真实数据储存在 DOM 节点中,所以在使用非受控组件时,有时候反而更容易同时集成 React 和非 React 代码。如果你不介意代码美观性,并且希望快速编写代码,使用非受控组件往往可以减少你的代码量。否则,你应该使用受控组件。
class NameForm extends React.Component {
constructor(props) {
super(props);
this.input = React.createRef();
}
handleSubmit=(event) =>{
alert('name' + this.input.current.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" ref={this.input} />
</label>
<input type="submit" value="Submit" />
</form>
);
}
}
❗️在非受控组件中,你经常希望 React 能赋予组件一个初始值,但是不去控制后续的更新。 在这种情况下, 你可以指定一个 defaultValue 属性,而不是 value。
‼️在 React 中,<input type="file" /> 始终是一个非受控组件,因为它的值只能由用户设置,而不能通过代码控制。
handleSubmit=(event)=>{
event.preventDefault();
alert(
`Selected ${this.fileInput.current.files[0].name}`
);
}
2️⃣2️⃣ Web Components
在 React 中使用 Web Components
class HelloMessage extends React.Component {
render() {
return <div>Hello <x-search>{this.props.name}</x-search>!</div>;
}
}
- Web Components 为可复用组件提供了强大的封装;
- React 则提供了声明式的解决方案,使 DOM 与数据保持同步。
❗️常见的误区是在 Web Components 中使用的是 class 而非 className。
在 Web Components 中使用 React
class XSearch extends HTMLElement {
connectedCallback() {
const mountPoint = document.createElement('span');
this.attachShadow({ mode: 'open' }).appendChild(mountPoint);
const name = this.getAttribute('name');
const url = 'https://www.google.com/search?q=' + encodeURIComponent(name);
ReactDOM.render(<a href={url}>{name}</a>, mountPoint);
}
}
customElements.define('x-search', XSearch);