核心概念
JSX简介
const element = <h1>Hello, world!</h1>
JSX是一个JS的语法扩展,是React.createElement(component, props, ...children)函数的语法糖,拥有JS的全部功能,可以生成React"元素",建议与React一起使用来描述UI的样子。
** 1. 在JSX中嵌入表达式,在大括号内放置任何有效的JS表达式**
const element = <h1>Hello,{formatName(user)}!</h1>
** 2. JSX是一个表达式,在编译之后JSX表达式会被转为普通JS函数调用,对其取值后得到JS对象,故可以在if、for代码块中使用JSX,赋值给变量、也可以当做参数传入或返回**
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
** 3. JSX特定属性,JSX语法上更接近JS而不是HTML,所以React DOM使用小驼峰命名来定义属性的名称。可以使用引号将字符串设置为属性值、使用大括号将JS表达式设置为属性值,但同一个属性不能同时使用这两种符号**
const element = <div tabIndex="0"></div>
const element = <img src={user.avatarUrl}></img>
** 4. JSX防止注入攻击,React DOM在渲染所有输入内容之前会默认进行转义,所有内容在渲染之前被转换成了字符串,可以有效防止XSS攻击**
** 5. JXS表示对象,Babel会把JSX转译成React.createElement()函数调用。在调用前先创建React元素,描述了要渲染的内容,React通过读取这些元素来构建DOM以及更新**
const element = (
<h1 className="greeting">
Hello, world!
</h1>
)
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
)
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
}
** 6. JSX标签的第一部分指定了React元素的类型**
- 由于JSX会被编译为
React.createElement()的调用形式,所以React虽然没有直接被使用,但是还是需要引入的 - 小写字母开头的元素代表一个HTML内置组件,编译时会生成相应的字符串"div"传递给
React.createElement("div")作为参数 - 大写字母开头的元素代表的是React组件,如
<Foo>,会被编译为React.createElement(Foo) - 在运行时选择类型,通过通用表达式决定元素类型,需要将其赋值给一个大写字母开头的变量
import React from 'react';
import { PhotoStory, VideoStory } from './stories';
const components = {
photo: PhotoStory,
video: VideoStory
};
function Story(props) {
// 正确!JSX 类型可以是大写字母开头的变量。
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
}
** 7. JSX中的Props**
- JS表达式作为Props,
<MyComponent foo={1 + 2 + 3 + 4} /> - 字符串字面量作为Props,
<MyComponent message="hello world" /> - Props默认值为true,
<MyTextBox autocomplete /> 等价于 <MyTextBox autocomplete={true} /> - 属性展开,如果已经有了一个props对象,可以使用...来在JSX中传递整个props对象
const Button = props => {
const { kind, ...other } = props;
const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
return <button className={className} {...other} />;
};
const App = () => {
return (
<div>
<Button kind="primary" onClick={() => console.log("clicked!")}>
Hello World!
</Button>
</div>
);
};
** 8. JSX中的子元素,包含在开始和结束标签间的JSX表达式将作为特定属性props.children传递给外层组件。可以将任何东西作为子元素传递给自定义组件,只要确保在该组件渲染之前能被转换成React理解的对象**
- 字符串字面量作为子元素,
<MyComponent>Hello world!</MyComponent> - JSX元素为子元素,
<MyContainer><MyFirstComponent /><MySecondComponent /></MyContainer> - JS表达式作为子元素,
<MyComponent>{'foo'}</MyComponent> - true、false、null、undefined是合法子元素,但是不会被渲染,可以用于条件渲染。但是有一些值仍然会被渲染,比如0
<div>
{showHeader && <Header />}
<Content />
</div>
<div>
{props.messages.length > 0 &&
<MessageList messages={props.messages} />
}
</div>
- 函数作为子元素,JSX中的JS表达式会被计算为字符串、React元素或列表,而props.children和其他props一样可以传递任意类型的数据,区别仅仅是props.children是React已知的可以渲染类型
// 调用子元素回调 numTimes 次,来重复生成组件
function Repeat(props) {
let items = [];
for (let i = 0; i < props.numTimes; i++) {
items.push(props.children(i));
}
return <div>{items}</div>;
}
function ListOfTenThings() {
return (
<Repeat numTimes={10}>
{(index) => <div key={index}>This is item {index} in the list</div>}
</Repeat>
);
}
元素渲染
将React元素渲染为DOM。元素是构成React应用的最小砖块,描述了你在屏幕上想看到的内容。与浏览器DOM元素不同,React元素是创建开销极小的普通对象,React DOM负责更新DOM来与React元素保持一致。
** 1. 将元素渲染为DOM,只需要把React元素和根DOM节点一起传入ReactDOM.render()**
ReactDOM.render(<h1>Hello, world</h1>, document.getElementById('root'))
** 2. 更新已渲染的元素。React元素是不可变对象,一旦被创建就无法更改它的子元素或者属性,一个元素就像电影的单帧,它代表了某个时候的UI。更新UI唯一的方式是创建一个新的元素,并传入ReatDOM.render()。**
下面例子会在setInterval()回调函数,每秒都调用ReactDOM.render()
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
)
ReactDOM.render(element, document.getElementById('root'));
}
setInterval(tick, 1000)
** 3. React只更新它需要更新的部分,React DOM会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使DOM达到预期的状态。尽管每一秒我们都会新建一个描述整个UI树的元素,React DOM只会更新实际改变了的内容**
** 4. 考虑在任意给定时刻如何展现UI,而不是如何随着时间变化去改变它**
组件&Props
组件允许将UI拆分为独立可复用的代码片段,并对每个片段进行独立构思。组件从概念上类似JS函数,它接受任意的入参即props,并返回用于描述页面内容的React元素
** 1. 组件名称必须是大写字母,React会将小写字母开头的组件视为原生DOM标签**
** 2. 函数组件与class组件。函数组件接受唯一带有数据的props对象并返回一个React元素,本质上就是JS函数。也可以使用ES6的class来定义组件**
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
** 3. 渲染组件。React元素可以使DOM标签也可以是用户自定义的组件,当为自定义组件时,它会将JSX所接收的属性以及子组件转换为props对象传递给组件**
- 调用ReactDOM.render()函数,并传入
<Welcome name="Sara" />作为参数 - React调用 Welcome 组件,并将
{name: 'Sara'}作为props传入 - Welcome组件将
<h1>Hello, Sara</h1>元素作为返回值 - React DOM将DOM高效地更行为
<h1>Hello, Sara</h1>
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
ReactDOM.render(
element,
document.getElementById('root')
)
** 4. Props的只读性,所有React组件都必须像纯函数一样保护它们的props不被更改**
State&生命周期
** 1. 不要直接修改State而使用setState()**
** 2. State的更新是异步的, 所以不要依赖this.state的值来更新下一个状态,可以使用setState()接收函数为参数,函数第一个参数为上一个state,第二个为此次应用更新的props**
this.setState((state, props) => ({
counter: state.counter + props.increment
}))
** 3. State的更新会被合并,当调用setState()时,React会把提供的对象合并到当前的State**
componentDidMount() {
fetchPosts().then(response => {
this.setState({
posts: response.posts
});
});
fetchComments().then(response => {
this.setState({
comments: response.comments
});
});
}
** 4. 数据流是向下流动的,State除了拥有并设置了它的组件,其他组件都无法访问。任何的state总是所属于特定的组件,而且该state派生的任何数据或UI只能影响树中低于他们的组件**
事件处理
React元素的事件处理和DOM元素相似,但是语法上有点不同。
** 1. React事件的命名采用小驼峰,而不是纯小写**
** 2. 使用JSX语法时需要传入一个函数作为事件处理函数,而不是一个字符串**
** 3. 不能通过返回false阻止默认行为,必须显示使用preventDefault**
** 4. JSX回调函数中的this,可以bind绑定this、使用实验性的public class fields语法、使用剪头函数。箭头函数在每次渲染时都会创建不同的回调函数,如果该回调函数作为prop传入子组件时,这些组件可能会进行额外的重新渲染**
class LoggingButton extends React.Component {
constructor(props) {
super(props);
// 为了在回调中使用 `this`,这个绑定是必不可少的
this.handleClickBind = this.handleClickBind.bind(this);
}
handleClickBind(){
console.log('this is:', this);
}
// 注意: 这是 *实验性* 语法。
handleClickTest = () => {
console.log('this is:', this);
}
handleClickFun() {
console.log('this is:', this);
}
render() {
return (
<div>
<button onClick={this.handleClickBind}>
Click me
</button>
<button onClick={this.handleClickTest}>
Click me
</button>
<button onClick={(e) => this.handleClickFun(e)}>
Click me
</button>
<div>
);
}
}
** 5. 向事件处理程序传递参数,通常我们会为事件处理函数传递额外的参数,可以使用箭头函数(e需要显示的传递)也可以使用bind(e为隐式的传递)**
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
条件渲染
在React中,可以创建不同的组件来封装各种需要的行为,然后根据应用的不同状态只渲染对应状态下的部分内容。React中的条件渲染和JS中的一样,使用JS运算符if或者条件运算符去创建元素来表现当前的状态,然后让React根据它们来更新UI。
** 1. 元素变量,可以使用变量来存储元素,可以有条件地渲染组件的一部分,而其他的渲染部分并不会因此而改变**
** 2. 与运算符 &&,通过花括号包裹代码,可以在JSX中嵌入任何表达式,可以很方便的进行元素的条件渲染**
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
)
}
** 3. 三目运算符,condition ? true : false **
<div>
The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
</div>
** 4. 阻止组件渲染,可以让render方法直接返回null,而不进行任何渲染。这并不会影响组件的生命周期**
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
列表 & Key
** 1. key 的规则**
- 渲染多个组件,通过{}在JSX内构建元素集合
const numbers = [1, 2, 3, 4, 5]
const listItems = numbers.map((number) =>
<li>{number}</li>
)
- Key是元素在列表中独一无二的标志,帮助React识别哪些元素改变了。如果列表的顺序可能会变化,不建议使用索引来作为key值,否则会导致性能变差还可能引起组件状态的问题。
- Key只有放在就近的数组上下文中才有意义(在map方法中的元素需要设置key)
- Key只是在兄弟节点之间唯一,不需要全局唯一
- 在JSX中嵌入map(),JSX允许在大括号中嵌入任何表达式,所有可以内联map()返回的结果
<ul>
{
numbers.map((number) =>
<ListItem
key={number.toString()}
value={number}
/>
)
}
</ul>
** 2. key 可以使用索引的情况(下面3个条件同时满足)**
- 列表和项是静态的,它们不计算也不更改
- 列表中的项目没有id
- 列表不会被重新排序或过滤
** 3. key 为索引为什么会影响性能**
- 在默认条件下
- 当递归 DOM 节点的子元素时,React 会同时遍历两个子元素的列表;当产生差异时,生成一个 mutation
- 在子元素列表末尾新增元素时,更新开销比较小
- 只是简单的将新增元素插入到表头,那么更新开销会比较大,因为React会重建每一个子元素
- 唯一key时
- React 使用 key 来匹配原有树上的子元素以及最新树上的子元素
- 使用元素已有的唯一 ID
- 或利用一部分内容作为哈希值来生成一个 key
** 4. key 为索引会产生什么问题**
如果 key 是一个下标,那么修改顺序时会修改当前的 key,导致非受控组件的 state(比如输入框)可能相互篡改导致无法预期的变动。
表单
在React里,HTML表单元素的工作方式和其他DOM元素有些不同,因为表单元素通常会保持一些内部的state。
** 1. 受控组件**
- 定义
- HTML 中,表单元素(如
<input>、 <textarea> 和 <select>)通常自己维护 state,并根据用户输入进行更新。 - React 中,可变状态(mutable state)通常保存在组件的 state 属性中,并且只能通过使用 setState()来更新。
- 使React的state成为唯一数据源,渲染表单的React组件还控制着用户输入过程中表单发生的操作。被React以这种方式控制取值的表单输入元素被称为**"受控组件"**
- HTML 中,表单元素(如
- 实现
- 在表单元素上设置value属性,因此显示的值始终为this.state.value这使得React的state成为唯一数据源
- 由于handleTextChange在每次按键时都会执行更新React的state,因此显示的值将随着用户输入更新
** 2. input, textarea 标签**
- 在 HTML 中,
<textarea>元素通过其子元素定义其文本 - 在React中,使用 value 属性代替
<input value={this.state.value} onChange={this.handleChange} />
<textarea value={this.state.value} onChange={this.handleChange} />
** 3. select 标签**
- 在 HTML 中,
<select>创建下拉列表标签,叶子选项selected属性为true时,被选中 - React 中,在根 select 标签上使用 value 属性
<select value={this.state.value} onChange={this.handleChange}>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
** 4. 文件input标签,非受控组件**
- 在 HTML 中,
<input type="file">允许用户从存储设备中选择一个或多个文件,将其上传到服务器 - 因为它的value只读,所有它是React中的一个非受控组件
<input type="file" />
** 5. 处理多个输入**
- 当需要处理多个input元素时,可以给每个元素添加name属性,并让处理函数根据event.target.name的值选择要执行的操作
- 可以使用 ES6 计算属性名称的语法更新给定输入名称对应的 state 值
handleInputChange(event) {
const target = event.target;
const value = target.name === 'isGoing' ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
});
}
<input name="isGoing" type="checkbox" checked={this.state.isGoing} onChange={this.handleInputChange} />
<input name="numberOfGuests" type="number" value={this.state.numberOfGuests} onChange={this.handleInputChange} />
this.setState({
[name]: value
});
** 6. 受控输入空值**
在受控组件上指定 value 的 prop 会阻止用户更改输入。如果你指定了 value,但输入仍可编辑,则可能是你意外地将value 设置为 undefined 或 null。
状态提升
在Reat中,将多个组建中需要共享的state向上移动到它们的最近共同父组件中,便可以实现共享state,这就是"状态提升"。
在React应用中,任何可变数据应当只有一个相对应的唯一数据源,通常state都是首先添加到需要渲染数据的组件中去,然后如果其他组件也需要这个state,那么你可以将他提升至这些组件的最近共同父组件中。应该依靠自上而下的数据流而不是尝试在不同组件间同步state。
如果某些数据可以由state和props推导得到,那么它就不应该存在于state中。
组合 VS 继承
Reat有十分强大的组合模式,我们推荐使用组合而非继承来实现组件间的代码重用。
Props和组合为你提供了清晰而安全地定制组件和行为的灵活方式,组件可以接受任意props,包括基本数据类型、React元素、函数。
如果想要在组件间复用非UI的功能,可以将其提取为一个单独的JS模块,如函数、对象或者类,组件可以直接引入而无需通过extend继承它们。
1. 包含关系,组件使用特殊的children props来将子组件传递到渲染结果中
function FancyBorder(props) {
return (
<div className={'FancyBorder FancyBorder-' + props.color}>
{props.children}
</div>
)
}
function WelcomeDialog() {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
Welcome
</h1>
<p className="Dialog-message">
Thank you for visiting our spacecraft!
</p>
</FancyBorder>
);
}
2. 特例关系,"特殊组件"可以通过props来定制渲染"一般组件"。比如 WelcomeDialog 可以说是 Dialog 的特殊实例
function Dialog(props) {
return (
<FancyBorder color="blue">
<h1 className="Dialog-title">
{props.title}
</h1>
<p className="Dialog-message">
{props.message}
</p>
</FancyBorder>
)
}
function WelcomeDialog() {
return (
<Dialog
title="Welcome"
message="Thank you for visiting our spacecraft!" />
)
}
React哲学
React是用JS构建快速响应的大型web应用程序的首选方式
1. 将设计好的UI划分为组件层级
根据单一功能原则来判定组件的范围,即一个组件只能负责一个功能。实践中,经常是在向用户展示JSON数据模型,所以如果模型设计恰当UI便会与数据模型一一对应
├─ FilterableProductTable(橙色): 是整个示例应用的整体
│ ├─ SearchBar (蓝色): 接受所有的用户输入
│ ├─ ProductTable (绿色): 展示数据内容并根据用户输入筛选结果
│ │ ├─ ProductCategoryRow(天蓝色): 为每一个产品类别展示标题
│ │ └─ ProductRow (红色): 每一行展示一个产品
2. 用React创建一个静态版本
用已有的数据模型渲染一个不包含交互功能的UI。在创建静态版本时,需要创建一些会重用其他组件的组件,然后通过props传入所需要的数据
3. 确定UI state的最小表示
想要使UI具备交互功能就需要触发基础数据模型改变的能力,React通过state来实现。只保留应用所需的可变state的最小集合,其他数据均由它们计算产生
- 数据是否是由父组件通过props传递而来?如果是,那么不是state
- 该数据是否随着时间的推移而保持不变?如果是,那么不是state
- 能否根据其他state或props计算出改数据的值?如果是,那么它不是state
应用中的数据:
● 包含所有产品的原始列表
● 用户输入的搜索词
● 复选框是否选中的值
● 经过搜索筛选的产品列表
属于state的有:
● 用户输入的搜索词
● 复选框是否选中的值
4. 确定state放置的位置,React中的数据流是单向的,并顺着组件层级从上往下传递
- 找到根据这个state进行渲染的所有组件
- 找到他们的共同所有者组件
- 该共同所有者组件或者比它层级更高的组件应该拥有该state
- 如果找不到一个合适的位置来存放state,可以创建一个高于共同所有者组件层级的新组件
根据以上策略重新考虑我们的示例应用
● ProductTable 需要根据 state 筛选产品列表
● SearchBar 需要展示搜索词和复选框的状态
● 他们的共同所有者是 FilterableProductTable
5. 添加反向数据流。
- 每当用户改变表单的值,我们需要改变state来反应用户的当前输入。
- 由于state只能由拥有它们的组件进行更改,
<FilterableProductTable>必须将一个能触发state改变的回调函数callback传递给<SearchBar>。 - 我们可以使用输入框的onChange事件来监视用户输入的变化,并通知
<FilterableProductTable >传递给<SearchBar>的回调函数,然后该回调函数将调用setState()从而更新应用。
高级指引
代码分割
代码分割是Webpack这类打包器支持的一项技术,能够创建多个包并在运行时动态加载。对应用进行代码分割能"懒加载当前用户所需要的内容",能显著提高应用性能。尽管没有减少应用的整体的代码体积,但是避免加载用户永远不需要的代码,并在初始加载的时候减少所需要加载的代码量。
1. 动态import()
当 Webpack 解析到该语法时,会自动进行代码分割。
import("./math").then(math => {
console.log(math.add(16, 26));
})
2. React.lazy()函数
- React.lazy()函数将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包
- Suspense 在模块未加载完成时做优雅降级处理,fallback 属性接受任何在组件加载过程中你想展示的 React 元素
- 如果模块加载失败它会出发一个错误,可通过 Error Boundaries 技术来处理这些情况,以显示良好的用户体验
import MyErrorBoundary from './MyErrorBoundary';
const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));
const MyComponent = () => (
<div>
<MyErrorBoundary>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</MyErrorBoundary>
</div>
)
3. 基于路由的代码分割
大多数网络用户习惯于页面之间能有个加载切换过程。你也可以选择重新渲染整个页面,这样您的用户就不必在渲染的同时再和页面上的其他元素进行交互。
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);
Context
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
1. Context API
- 创建一个 Context 对象,只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效
const MyContext = React.createContext(defaultValue)
- 订阅 Context 的变化,每个 Context 对象都会返回一个 Provider React 组件,它允许消费组件订阅 context 的变化
<MyContext.Provider value={/* 某个值 */}>props.children</MyContext.Provider>
- 将 Class 组件的 contextType 属性赋值为创建的 Context 对象,在组件总可以使用 this.context 来消费最近 Context 上的那个值
MyClass.contextType = MyContext
- 为函数式组件完成订阅 context,函数作为子元素接受当前的context值,返回一个react节点
<MyContext.Consumer>{value => ReactElement}</MyContext.Consumer>
- React DevTools 使用 context.displayName 确定 context 要显示的内容
MyContext.displayName = 'MyDisplayName';<MyContext.Provider> // "MyDisplayName.Provider" 在 DevTools 中<MyContext.Consumer> // "MyDisplayName.Consumer" 在 DevTools 中
2. 动态 Context
给 Context 使用动态值
this.toggleTheme = () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
};
<div>
<ThemeContext.Provider value={this.state.theme}>
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<Button onClick={toggleTheme}>
Change Theme
</Button>
</div>
3. 在嵌套组件中更新 Context
从一个在组件树中嵌套很深的组件中更新 context 是很有必要的。在这种场景下,你可以通过 context 传递一个函数,使得 consumers 组件更新 context:
<ThemeContext.Consumer>
{({theme, toggleTheme}) => (
<button onClick={toggleTheme} style={{backgroundColor: theme.background}}>
Toggle Theme
</button>
)}
</ThemeContext.Consumer>
this.state = {
theme: themes.light,
toggleTheme: () => {
this.setState(state => ({
theme:
state.theme === themes.dark
? themes.light
: themes.dark,
}));
},
};
<ThemeContext.Provider value={this.state}>
<Content />
</ThemeContext.Provider>
错误边界
错误边界是一种组件,可以捕获并打印发生在其子组件树的渲染期间、生命周期方法、构造函数中的JS错误,并渲染出备用UI。
1. 无法捕获以下场景产生的错误
- 事件处理
- 异步代码(例如 setTimeout 或 requestAnimationFrame 回调函数)
- 服务端渲染
- 错误边界组件抛出的错误(并非子组件)
2.错误边界组件:满足下面两个生命周期方法中的任意一个或两个
- 定义了 static getDerivedStateFromError(),用来渲染备用UI
- 定义了 componentDidCatch(),用来打印错误信息
3. 工作方式
- 类似于 JavaScript 的 catch {},不同的地方在于错误边界只针对 React Class 组件
- 如果一个错误边界无法渲染错误信息,则错误会冒泡至最近的上层错误边界
4. 组件栈追踪
开发环境下,react 16会把渲染期间发生的所有错误打印到控制台,包括错误信息,js 栈,组件栈追踪
- 在组件栈中查看文件名和行号,需添加插件(添加源代码文件和行号到 JSX 元素)到 babel 配置,但是生产环境必须禁用。
- 组件名称在栈追踪中的显示依赖于 Function.name 属性,需添加 function.name-polyfill 到打包(bundled)应用程序中,或者在所有组件上显示设置 displayName 属性
{
"plugins": ["transform-react-jsx-source"]
}
5. 事件处理器
React 不需要错误边界来捕获事件处理器中的错误。与 render 方法和生命周期方法不同,事件处理器不会在渲染期间触发。因此,如果它们抛出异常,React 仍然能够知道需要在屏幕上显示什么。
如果你需要在事件处理器内部捕获错误,使用普通的 JavaScript try / catch 语句
Refs转发
ref、key 不是 prop 属性,其被 React 进行了特殊处理,不会通过 props 传递到子组件
Refs 转发是一项将 ref 自动地通过组件传递到其子组件的技巧,允许某些组件接受 ref 并将其传递给子组件。
React.forwardRef 接受一个渲染函数(props 和 ref 为函数参数,返回 React 节点)
1. 转发 ref 到 DOM组件
- 调用 React.createRef() 创建了一个 ReactRef 并将其赋值给 ref 变量
- 通过制定 ref 为 JSX 属性,将其传递给
<FancyBuggon ref={ref}> - React 传递 ref 给 React.forwardRef 的函数参数
(props, ref) => ...作为第二个参数 - 向下转发该 ref 到
<button ref={ref}>,将其指定为 JSX 属性 - 当 ref 挂载完成,ref.current 将指向
<button>DOM节点
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
))
const ref = React.createRef()
<FancyButton ref={ref}>Click me!</FancyButton>
2. 高阶组件中转发 refs
如果你对 HOC 添加 ref,该 ref 将引用最外层的容器组件,而不是被包裹的组件。即 ref 将指向 LogProps 而不是内部的 FancyButton 组件
- React.forwardRef 回调的第二个参数 “ref”, 将其作为常规 prop 属性 forwardedRef 传递给 LogProps
- 它就可以被挂载到被 LogProps 包裹的子组件上
- 子组件将自定义的 prop 属性 “forwardedRef” 定义为 ref
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;
return <Component ref={forwardedRef} {...rest} />;
}
}
return React.forwardRef((props, ref) => {
return <LogProps {...props} forwardedRef={ref} />;
});
}
3. 在 DevTools 中显示自定义名称
- React.forwardRef 接受一个渲染函数,React DevTools 使用该函数来决定为 ref 转发组件显示的内容。
- 默认显示 "ForwardRef"
- 如果命名了渲染函数,DevTools 也将包含其名称(例如 “ForwardRef(myFunction)”)
- 可以设置函数的 displayName 属性来包含被包裹组件的名称
Refs & DOM
Refs提供了一种方式,允许我们访问DOM节点或者render方法中创建的React元素。在典型的React数据流中,props是父组件与子组件交互的唯一方式,要修改一个子组件,需要使用新的props来重新渲染它,但是在某些情况下,需要在典型数据流之外强制修改子组件。
1. 如何使用Refs
- 管理焦点,选本选择或媒体播放
- 触发强制动画
- 集成第三方DOM库
- 避免使用refs来做任何可以通过声明式实现来完成的事情,如Dialog组件中暴露open()或close()方法,最好传递isOpen属性
2. Refs通过React.createRef()创建,并通过ref属性附加到React元素,在构造组件时,通常会将Refa分配给实例属性,以便可以在这个组件中引用它们
class MyComponent extends React.Component {
constructor(props) {
super(props);
this.myRef = React.createRef();
}
render() {
return <div ref={this.myRef} />;
}
}
3. 访问Refs
- 当ref属性用于HTML元素时,构造函数中使用React.createRef()创建的ref接收底层DOM元素作为其current属性
- 当ref属性用于字定义class组件时,ref对象接收组件的挂载实例作为其current属性
- 不能在函数组件上使用ref属性,因为函数没有实例
4. 回调Refs,是传递一个函数,这个函数中接受React组件实例或者HTML DOM元素作为参数,以便它们能在其他地方被存储和访问。
如果ref回调函数是以内联的方法定义的,在更新过程中会被执行2次,第一次传入参数null,第二次传入参数DOM元素,这是因为在每次渲染时会创建一个新的函数实例,所以React清空旧的ref并且设置新的。
Fragments
是React中的一个常见的模式一个组件返回一个元素列表。
1. 使用<React.Fragment key={}>,key是唯一可以传递的属性
<dl>
{props.items.map(item => (
// 没有`key`,React 会发出一个关键警告
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
</dl>
2. 使用短语法,<></>此语法不支持key属性
<>
<td>Hello</td>
<td>World</td>
</>
与第三方库集成
React可以被用于任何web应用中,它可以嵌入到其他应用,其他应用也可以被嵌入React。
1. 集成带有DOM操作的插件,React不会理会React自身之外的DOM操作,它根据内部虚拟DOM来决定是否需要更新,而且如果同一个DOM节点被另一个库操作了,React会觉得困惑而且没有办法恢复,所以可以渲染无需更新的React元素,比如渲染一个空的<div>,这个元素既没有属性也没有子元素,所以React没有理由去更新它,故jQuery可以自由的管理这部分的DOM。而由于jQuery插件绑定事件监听到DOM上,所以需要在componentWillUNmount生命周期函数中注销监听。
class SomePlugin extends React.Component {
componentDidMount() {
this.$el = $(this.el);
this.$el.somePlugin();
}
componentWillUnmount() {
this.$el.somePlugin('destroy');
}
render() {
return <div ref={el => this.el = el} />;
}
}
2. 利用React替换基于字符串的渲染,Backbone视图通常使用HTML字符串或者产生字符串的模版函数来创建DOM元素的内容,这个过程可以通过渲染React组件来替换掉。
function Paragraph(props) {
return <p>{props.text}</p>;
}
const ParagraphView = Backbone.View.extend({
render() {
const text = this.model.get('text');
ReactDOM.render(<Paragraph text={text} />, this.el);
return this;
},
remove() {
ReactDOM.unmountComponentAtNode(this.el);
Backbone.View.prototype.remove.call(this);
}
})
3. 在React组件中使用Backbone的model和collection最简单的方法就是监听多种变化事件并且手动强制手动触发一个更新。可以通过在生命周期方法中订阅其更改并选择性地拷贝数据到本地React State,来将React用于任何model库。
性能优化
UI更新需要昂贵的的DOM操作,而React内部使用几种技巧以便更小化DOM操作。
1. 使用React的生产版本
2. 使用Chrome Performance标签分析组件
- 临时禁用所有的Chrome扩展,尤其是React开发者工具,他们会严重干扰度量结果
- 确保在React开发模式下运行应用程序
- 打开Chrome开发者工具的Performance标签并按下reload
- 对想分析的行为进行复现,尽量在20秒内完成
- 停止记录
- 在Timing标签下会显示React归类好的事件
3. 使用开发者工具中的分析器对组件进行分析
4. 虚拟化长列表,会在有限的时间内仅渲染有限的内容,并奇迹般地降低重新渲染组件消耗的时间,以及创建DOM节点的数量
5. 避免重新渲染,当一个组件的props或state更新,React会将最新返回的元素与之前渲染的元素进行比较,以决定是否有必要更新真实DOM。
shouldComponentUpdate()方法返回false,render以及render之后的生命周期方法都不会被调用- 继承React.PureComponent的组件,默认实现shouldComponentUpdate()方法,props和state进行浅比较,故当props和state本身可变的话,浅比较会有遗漏
6. 使用不可变的数据结构,不可变数据使得追踪变得非常容易,每次变更都会产生一个新的对象使得只需要检查对象引用是否改变
- 不可变: 一旦创建,一个集合便不能再被修改
- 持久化: 对集合进行修改,会创建一个新的集合,以前的集合仍然有效
- 结构共享: 新的集合会尽可能复用之前集合的结构,以最小化拷贝操作来提高性能
Portals
Portal提供了一种将子节点渲染到存在于父组件以外的DOM节点的优秀方案。
1. ReactDOM.createPortal(child, container)第一个参数child是任何可渲染的React元素,第二个参数container是一个DOM元素
2. 由于Portal仍存在React树中,且与DOM树中的位置无关,故context、事件冒泡这些特性是不变的。一个从portal内部触发的事件会一致冒泡到包含React树的祖先,即便这些元素不是DOM树的祖先
Diff算法
调用render()会创建一棵React元素组成的树,下一个state或props更新后,render()会返回下一棵不同的树,React需要判断两棵树的差别,并有效的更新UI同步最新的树。当对比两棵树时,React首先会比较两棵树的根节点,不同类型的根节点元素会有不同的形态。
1. 对比不同类型的根元素,如果从<a>变成<img>、从<Article>变成<Comment>、从<a>变成<Comment>都会卸载根元素,对应的DOM节点会被销毁,组件实例执行方法componentWillUnmount(),根节点以下的组件也会被卸载,他们的状态会被销毁。新树被创建时,对应的DOM节点会被创建及插入到DOM中,组件实例调用componentWillMount()和componentDidMount(),如React会销毁<Counter>组件并重新装在一个新组件<Counter>
<div>
<Counter />
</div>
<span>
<Counter />
</span>
2. 同类型的HTML DOM元素,React会保留DOM节点,修改对比后有改变的属性
3. 同类型的组件,当组件更新时,组件实例保持不变,state在跨越不同的渲染时保持一致。React更新该实例的props,并调用该实例的componentWillReceiveProps()、componentWillUpdate()、render()方法,diff算法将在之前的结果以及新的结果中进行递归
4. React使用key匹配原有树上的子元素和新树上的子元素,组件实例基于key来决定是否更新以及复用
Render Props
Render Props是指利用值为函数的prop来实现在组件间共享代码的技术。具有render props的组件接受一个函数,该函数返回一个React元素并调用它而不是实现自己的渲染逻辑。render props是一个用于告知组件需要渲染什么内容的函数prop
1. 组件封装state和行为,然后分享给其他需要相同state的组件。渲染一个有render prop的<Mouse>组件,render prop告诉组件使用当前鼠标坐标来渲染什么
<div style={styleObj} onMouseMove={this.handleMouseMove}>
{this.props.render(this.state)}
</div>
2. 可以使用带有render prop的常规组件来实现大多数高阶组件
function withMouse(Com) {
return class extends Component {
render() {
return (
<Mouse render={mouse => (
<Com {...this.props} mouse={mouse}/>
)} />
)
}
}
}
3. render props是因为模式才被如此称呼,任何用于告知组件需要渲染什么内容的函数prop在技术上都可以被称为render prop。故可以使用任何其他名称的prop代替render
Context
Context提供了一个无需为每个组件手动添加props,就能在组件树间进行数据传递的方法。
1. React.createContext()创建一个Context对象,当React渲染一个订阅了这个Context对象的组件,这个组件会从组件树中寻找离自身最近的那个匹配的Provider中读取到当前的Context值,如果没有匹配到Provider时,其defaultValue参数才会起作用,将undefined传递给Provider时,消费组件的defaultValue不会生效。
2. <MyContext.Provider value={/* 某个值 */}>每个Context对象都会返回一个Provider React组件,它允许消费组件订阅Context的变化,Provider接收一个Value属性传递给消费组件。当Provider的value值发生变化时,它内部的所有消费者都会重新渲染。Provider及其内部消费组件都不受制于shouldComponentUpdate()函数,因此当consumer组件在其祖先组件退出更新的情况下也能更新
3. 挂载在class上的contextType属性会被赋值为一个由React.createContext()创建的context对象。可以在组件中使用this.context来消费最新context上那个值,可以在任何生命周期中使用,包括render()函数中
import React, {Component} from 'react'
const ThemeContext = React.createContext('light')
class ThemedButton extends Component {
render() {
return (
<div>{this.context || "undefined"}</div>
);
}
}
ThemedButton.contextType = ThemeContext
export default class App extends Component {
render() {
return (
<div>
<ThemeContext.Provider>
<ThemedButton />
</ThemeContext.Provider>
<ThemeContext.Provider value="drack">
<ThemedButton />
</ThemeContext.Provider>
<ThemedButton />
</div>
)
}
}
运行结果:
undefined
drack
light
4. Context.Consumer,React组件可以订阅到context变更,可以在函数组件中完成订阅context。这个函数接收当前context值,返回一个React节点
import React, {Component} from 'react'
const ThemeContext = React.createContext('light')
export default class App extends Component {
render() {
return (
<div>
<ThemeContext.Provider value="drack">
<ThemeContext.Consumer>
{
(context) => (
<div>{context}</div>
)
}
</ThemeContext.Consumer>
</ThemeContext.Provider>
</div>
)
}
}
运行结果为: drack
高阶组件
高阶组件是一个函数,参数为组件新组件为返回值。React是将props转换为UI,而高阶组件是将组件转换为另一个组件。高阶组件HOC是React中用于复用组件逻辑的一种技巧,HOC自身不是React API的一部分,它是一种基于React的组合特性而形成的设计模式。
1. 不要改变原始组件,使用组合,不要试图在HOC中修改组件原型(或以其他方式改变它),这样会产生一些不良后果。
- 如果子组件的
componentWillReceiveProps()方法被修改,将无法在像HOC增强之前那样使用了 - 如果再用另外一个同样会修改
componentWillReceiveProps()的HOC增强子组件,那么前面的HOC修改的componentWillReceiveProps()方法会被覆盖从而失效 - HOC将无法应用于没有生命周期函数的函数组件,即不能增强无状态组件