什么是 React
React 的特点
React 是一个用于构建用户界面的 JavaScript 库,具有以下特点:
- 声明式
React 为应用的每个状态设计简洁的视图,当数据变动时 React 能高效更新并渲染合适的组件。
- 组件化
构建管理自身状态的组件,然后组合成复杂的 UI。
由于组件逻辑使用 JavaScript 编写而非模板,因此可以方便地在应用中传递数据,并保持状态和 DOM 分离。
- 跨平台
React 可以使用 Node 进行服务端渲染,使用 React Native 开发原生移动应用。
- 渐进式
无需重写现有代码,引入 React 即可开发新功能。React 也允许和其它框架或库结合起来使用。使用者可以按需引入或多或少的 React 特性。
React 组件
- 简单组件
React 组件使用 render() 方法,接收输入的数据并返回需要展示的内容,被传入的数据可以在组件中通过 this.props 来访问。
- 有状态组件
除了通过 this.props 使用外部数据之外,组件还可以维护内部的状态数据,并通过 this.state 来访问。当组件的状态数据改变时,组件会再次调用 render() 方法重新渲染。
JSX 简介
JSX 是一个 JavaScript 扩展语法,可以很好地描述 UI 应呈现出的交互形式。
为什么使用 JSX
React 认为渲染逻辑和 UI 逻辑本质上是耦合的,React 并没有人为地将标签(markup)和逻辑分离到不同文件,而是将两者共同存放在“组件”这一松散耦合单元中。
JSX 中嵌入表达式
JSX 语法中,任何有效的 JavaScript 表达式都可以放到大括号内。
const element = (
<h1>
Hello, {'World'}!
</h1>
);
如果 JSX 拆为多行,最好使用括号包裹。
JSX 也是一个表达式
JSX 最终会被编译成 JavaScript 函数调用,且返回值是一个对象。也就是说,JSX 可以直接作为值来使用。
function greeting(user) {
if (user !== '') {
return <h1>Hello {user}!</h1>;
} else {
return <h1>Hello React!</h1>;
}
}
JSX 中指定属性
JSX 中有两种方式指定属性:
- 使用双引号,指定属性值为字符串字面量;
- 使用大括号插入表达式作为属性值。
两种方式不能同时使用,JSX 更接近 JavaScript 而不是 HTML,因此 React DOM 使用 camelCase 定义属性名。
const element = <a href="https://zh-hans.reactjs.org/" className="link" />;
JSX 子元素
const element = <img src="chrome://branding/content/about-logo.png" />
const elementWithChildren = (
<div>
<h1>Hello</h1>
<h1>World</h1>
</div>
);
JSX 防止注入攻击
React DOM 在渲染所有输入内容之前,默认会进行转义,所有内容在渲染之前都被转成字符串,以防止 XSS 攻击。
const element = <h1>{title}</h1>;
JSX 表示对象
const element = (
<h1 className="greeting">
Hello, World!
</h1>
);
JSX 最终会编译成 React.createElement() 函数调用。
const element = React.createElement(
'h1',
{ className: 'greeting'},
'Hello, World!'
);
React.createElement() 函数的返回值是如下形式的对象:
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, World!',
},
};
这些对象被称为“React 元素”。
元素渲染
元素描述了用户想要看到的内容,是构成 React 应用的最小砖块。
const element = <h1>Hello</h1>;
和 DOM 不同,React 元素是一种开销极小的普通对象。React DOM 会负责根据 React 元素更新 DOM。
将元素渲染为 DOM
通常会指定 HTML 中的某个节点为“根” DOM 节点,该节点的内容由 React DOM 来管理。仅用 React 构建的应用通常只有一个根 DOM 节点,如果把 React 集成到一个已有的应用中,那么该应用中可能包含多个根 DOM 节点。
const element = <h1>Hello</h1>;
ReactDOM.render(element, document.getElementById('root'));
更新已渲染的元素
React 元素是不可变对象,一旦创建,无法改变它的子元素和属性。因此,更新 UI 的一种方式是创建一个全新的元素,并使用 ReactDOM.render() 来渲染。但在实践中,大多数 React 应用都只会调用一次 ReactDOM.render(),然后通过有状态的组件来实现自动更新。
React 按需更新
React 会将元素及其子元素与它们之前的状态进行比较,并只进行必要的更新。
组件和 Props
函数组件和 class 组件
函数组件,接收带有数据的 props 对象,返回 React 元素。
function Welcome(props) {
return <h1>Hello {props.name}!</h1>;
}
class 组件,通过自身实例上的 props 获取数据。
class Welcome extends React.Component {
render() {
return <h1>Hello {this.props.name}!</h1>;
}
}
渲染组件
React 元素可以是用户自定义的组件,此时,JSX 所接收的属性(attributes)和子组件(children)将会转换为 props 对象传递给组件。
function Welcome(props) {
return <h1>Hello {props.name}!</h1>;
}
const element = <Welcome name="Tom" />;
ReactDOM.render(element, document.getElementById('root'));
为了区分于原生 DOM 标签,组件名称必须以大写字母开头,并且只能在声明组件的作用域内使用。
组合组件
组件中可以使用其它组件。通常,每个 React 应用的顶层组件都是 App 组件。但是如果是把 React 集成到现有应用中,可能需要一些小组件,并自下而上地逐步应用到视图层中。
function Welcome(props) {
return <h1>Hello {props.name}!</h1>;
}
function App() {
return (
<div>
<Welcome name="Tom" />
<Welcome name="Jerry" />
</div>
);
};
ReactDOM.render(<App />, document.getElementById('root'));
提取组件
如果 UI 中有一部分被多次使用,或者组件本身足够复杂,可以考虑对组件进行抽离,以提高组件的可维护性和复用性。
应该根据组件自身的角度命名 props,而不是根据调用组件的上下文命名。
Props 的只读性
拥有下面两条性质的函数称为纯函数:
- 不管何时何地调用该函数,只要入参相同,函数返回值就相同(不会随局部静态变量、非局部变量、可变引用实参或输入流的变化而变化);
- 没有副作用(不改变局部静态变量、非局部变量、可变引用实参或输入输出流)。
这样的函数可作为数学函数的类比。
副作用:一个操作、函数或表达式,如果它修改了当前局部环境之外的状态变量,则称为具有副作用。如果存在副作用,一个程序的行为可能依赖于历史。
React 有一个严格的规则:所有 React 组件都必须像纯函数一样保护它们的 props 不被更改。
State 和生命周期
除了调用 ReactDOM.render() 更新 UI 之外,还可以通过 state 来实现。state 和 props 类似,但是 state 是私有的,完全受控于当前组件。
对于 class 组件,每次组件更新时,render 方法都会被调用。但只要在相同的 DOM 节点中渲染组件,就仅有一个组件实例被创建使用。这就使得可以使用如 state 或生命周期方法等特性。
class 组件中添加 state 和生命周期方法
class 组件可以在 constructor 中初始化 state。在 constructor 中,class 组件应始终使用 props 参数调用父类构造函数,以初始化 this。
当应用拥有许多组件时,需要在组件销毁时释放所占用的资源。
React 为组件定义了一些生命周期:
- 挂载(mount):组件第一次被渲染到 DOM 中时;
- 卸载(unmount):组件从 DOM 中删除时;
以及相应的生命周期方法,当组件进行到该生命周期时,就会执行相应的方法:
componentDidMount方法在组件被渲染到 DOM 中后执行;componentWillUnmount方法在组件从 DOM 中删除时执行。
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {
date: new Date(),
};
}
componentDidMount() {
this.timerID = setInterval(() => {
this.tick();
}, 1000);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date(),
});
}
render() {
return (
<div>
<h1>Hello React!</h1>
<h2>It is {this.state.date}</h2>
</div>
);
}
}
React 允许向 class 中添加任何不参与数据流(即,非 props/state)的额外字段。
setState
关于 state 和 setState,有以下注意事项:
- 不要直接给
state赋值
直接赋值无法重新渲染组件,只有使用 setState 方法才可以,构造函数(constructor)是唯一可以给 state 直接赋值的地方。
state的更新可能是异步的
出于性能的考虑,React 可能会把 setState 方法的多次调用合并成一次调用。由于 this.props 和 this.state 可能会异步更新,因此不要依赖它们的值来更新下一个状态。为解决这一问题,可以传给 setState 一个函数而不是对象,这个函数的第一个参数是上一个 state,第二个参数是当前更新被应用时的 props,React 将以该函数返回值更新 state。
JavaScript this.setState((state, props) => ({ counter: state.counter + props.increment, }));
state的更新会被合并
当调用 setState() 时,React 会把所提供的对象合并到当前的 state 中。但这种合并是浅合并:未提供的状态直接保留,提供的状态完全替换。
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
posts: [],
comments: [],
};
}
...
}
this.setState({
posts: [1],
});
数据流动自上而下
不管是父组件还是子组件都无法知道某个组件有无状态,它们也并不关心该组件是函数组件还是 class 组件。
除了拥有并设置 state 的组件之外,其它组件都无法访问它,这也是 state 被称为局部的或是封装的原因。
组件可以把 state 作为 props 向下传递到它的子组件中。
这通常被称为“自上而下”或是“单向”的数据流,任何 state 总是属于某个特定组件,而且从该 state 派生的任何数据或 UI 都只能影响组件树中“低于”它的组件。
事件处理
React 元素的事件处理和 DOM 元素的很相似,但也有些不同:
- React 事件名采用 camelCase 命名,而不是纯小写;
- 使用 JSX 语法时必须传入一个函数作为事件处理函数,而不能是字符串;
- React 中必须显示调用
preventDefault来阻止事件默认行为,而不能通过返回false的方式。 - React 中的事件对象是一个合成事件,React 事件和原生事件不完全相同。React 中,一般不必使用
addEventListener监听 DOM 元素,而只需在元素初始渲染时添加监听器即可。
function Form() {
function handleSubmit(e) {
e.preventDefault();
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
class 组件中监听事件时,通常将事件处理函数声明为 class 中的方法,但是在绑定该函数之前,必须确定该函数的 this 指向,有三种方式可以实现这一点:
- 手动调用
bind绑定this的指向
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {
user: 'React',
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({
user: 'Tom',
});
}
render() {
return (
<div onClick={this.handleClick}>
<h1>Hello, {this.state.user}!</h1>
</div>
);
}
}
- class 字段实验性语法
class Welcome extends React.Component {
// some code
handleClick = () => {
console.log(this);
}
constructor(props) {
super(props);
this.state = {
user: 'React',
};
}
render() {
return (
<div onClick={this.handleClick}>
<h1>Hello, {this.state.user}!</h1>
</div>
);
}
}
- 箭头函数
class Welcome extends React.Component {
constructor(props) {
super(props);
this.state = {
user: 'React',
};
}
handleClick() {
this.setState({
user: 'Tom',
});
}
render() {
return (
<div onClick={() => this.handleClick()}>
<h1>Hello, {this.state.user}!</h1>
</div>
);
}
}
如果使用箭头函数,那么每次渲染组件时都会创建不同的回调函数,这些回调函数如果作为 props 传入子组件,这些组件可能会进行额外的重新渲染。因此,建议使用方式 1 和 方式 2 来解决 this 指向的问题。
传递参数给事件处理程序
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
条件渲染
React 允许根据条件来决定渲染结果,一般有两种条件渲染的方式:
- 根据条件决定渲染出哪个 JSX
function Greeting(props) {
const isBlock = props.isBlock;
if (isBlock) {
return <div />
} else {
return <span />
}
}
- 根据条件表达式决定 JSX 的渲染内容
元素表达式:
function Greeting(props) {
const element = props.isBlock ? <div /> : <span />;
return (
<div>
<h1>Hello</h1>
{element}
<div />
);
}
逻辑运算符:
function Greeting(props) {
return (
<div>
<h1>Hello</h1>
{props.isBlock && <div />}
<div />
);
}
三元表达式:
function Greeting(props) {
return (
<div>
<h1>Hello</h1>
<div>{props.isTom ? 'Tom' : 'Jerry'}</div>
<div />
);
}
如果条件过于复杂,应考虑抽离出组件。
阻止条件渲染
让组件的 render 方法返回 null。
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div>
Warning!
</div>
);
}
组件的 render 方法返回 null 不会影响组件的生命周期。
列表和 key
JSX 中的子元素可以是由 React 元素所构成的数组,从而渲染出列表。列表中的每一个表项都必须有一个属性 key。
function NumberList(props) {
const ListItems = props.numbers.map((number, index) => {
return <li key={index}>number</li>;
});
return (
<ul>{ListItems}</ul>
);
}
ReactDOM.render(
<NumberList numbers={[1, 2, 3]}>,
document.getElementById('root'),
);
key
key 帮助 React 识别哪个元素发生了变化,比如添加或删除。
- 一个元素的 key 最好是所在列表中独一无二的字符串,一般使用数据中的 id 作为元素的 key。如果没有显式指明 key,那么 React 将默认使用索引作为列表项的 key。
- 如果列表项可能会发生变化,建议不要使用索引作为 key,这样做会导致性能变差,还可能会引起组件状态的问题。
- 元素的 key 只有放在就近的数组上下文中才有意义。换句话说,key 设置在列表项上面,而不是列表项的子元素上。
- key 值在兄弟节点中必须是唯一的,但不需要它们全局唯一。
- key 会传递给 React,但不会传递给组件,组件中要想获得 key 值,则需添加其它属性,如:id。
JSX 内部渲染列表
除了事先生成 React 元素数组之外,还可以直接在 JSX 中生成 React 元素数组。
function NumberList(props) {
return (
<ul>
{props.numbers.map((number, index) => <li key={index}>number</li>)}
</ul>
);
}
ReactDOM.render(
<NumberList numbers={[1, 2, 3]}>,
document.getElementById('root'),
);
但这种风格可能会被滥用,何时需要为了可读性来抽离组件,完全取决于使用者。
表单
受控组件
HTML 中,表单元素的状态由自身维护,并根据用户输入自动更新。
React 中的可变状态通常保存在组件的 state 里面,并且通过 setState 更新,这使得 React 的 state 成为唯一数据源。渲染表单的 React 组件还控制着用户输入时表单发生的操作,被 React 以这种方式控制取值的表单输入元素称为受控组件。
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '',
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({
value: event.target.value,
});
console.log('user input');
}
render() {
return (
<form>
<input value={this.state.value} onChange={this.handleChange} />
</form>
);
}
}
textarea 标签
HTML 中,<textarea> 元素由其子元素定义其文本。
React 中,<textarea> 的文本内容由 value 属性提供。
class Form extends React.Component {
// some code ...
render() {
return (
<form>
<textarea value={this.state.value} onChange={this.handleChange} />
</form>
);
}
}
select 标签
HTML 中,<select> 标签创建下拉列表,选项标签 <option> 的 selected 属性用于表示该选项是否选中。
React 中,根 <select> 标签上的 value 属性指明了哪些选项 <option> 被选中。
- 单选
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
value: 'apple',
};
// some code ...
}
// some code ...
render() {
return (
<form>
<select value={this.state.value} onChange={this.handleChange}>
<option value="apple" />
<option value="orange" />
</select>
</form>
);
}
}
- 多选
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
value: [],
};
// some code ...
}
// some code ...
render() {
return (
<form>
<select multiple={true} value={this.state.value} onChange={this.handleChange}>
<option value="apple" />
<option value="orange" />
</select>
</form>
);
}
}
文件 input 标签
在 HTML 中,<input type="file" /> 允许用户从存储设备中选择一个或多个文件,上传到服务器。因为它的 value 只读,所以它是 React 中的非受控组件。
在一个回调函数中处理多个表单输入
为每个元素添加 name 属性,可以对表单元素进行区分。
class Form extends React.Component {
constructor(props) {
super(props);
this.state = {
age: 0,
gender: 'male',
};
this.handleChange = this.handleChange.bind(this);
}
handleChange(event) {
this.setState({
[event.target.name]: event.target.value,
});
}
render() {
return (
<form>
<input name="age" value={this.state.age} onChange={this.handleChange} />
<input name="gender" value={this.state.gender} onChange={this.handleChange} />
</form>
);
}
}
受控输入空值
在受控组件中指定 value 属性的值,会阻止用户更改输入,但如果 value 值为 null/undefined,那么用户可以手动更改。
状态提升
如果多个组件需要反映共同的状态变化,那么最好把共享状态提升到最近的公共父组件中,这被称为状态提升。
状态提升之后,子组件失去了数据控制权,因而变成了受控组件,通过为父组件提供两个相应的属性 {valueName}/on{ValueName} 来实现数据控制权的向上转移。
React 应用中,任何可变数据应只有唯一数据源。组件间的数据共享应当依靠自上而下的数据流,而不是尝试在不同组件间同步状态。
提升 state 的方式比双向绑定方式需要编写很多的样板代码,但是排查和隔离 bug 所需的工作量将会变少。由于存在于组件中的任何 state,都只有组件自己才能修改,因此 bug 的排查范围被大大缩小。此外,还可以使用自定义逻辑来拒绝或转换用户输入。
如果某些数据可以由 props 和 state 推导出,那就不应该存在于 state 中。
组合与继承
React 具有十分强大的组合模式,推荐使用组合而非继承来实现组件代码重用。
包含关系
有些组件无法提前知道它们子组件的具体内容,比如:通用展示容器之类的组件。有两种方案可以解决这一问题:
- 使用 children prop,适用于子组件较多的情形
function DisplayBox(props) {
return (
<div>
{props.children}
</div>
);
}
function Welcome(props) {
return (
<DisplayBox>
<h1>Welcome</h1>
<p>Thanks</p>
</DisplayBox>
);
}
- 使用一般的 props,适用于子组件较少且具有明确含义的情形
React 元素本质上也是对象,可以作为 props 进行传递。
function DisplayBox(props) {
return (
<div>
<div className="left">
{props.left}
</div>
<div className="right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<DisplayBox
left={<LeftBox />}
right={<RightBox />}
/>
);
}
这种用法类似于插槽(slot),但是 React 中没有插槽的概念。
特例关系
有些组件可以看作是其它组件的特殊实例,比如:WelcomeDialog 是 Dialog 的特殊实例。React 中,可以通过为“一般”组件定制 props 而得到“特殊”组件。
function Dialog(props) {
return (
<div>
<h1>
{props.title}
</h1>
<p>
{props.message}
</p>
</div>
);
}
function WelcomeDialog() {
return (
<Dialog
title="Welcome"
message="Thanks"
/>
);
}
继承
目前没有发现必须使用继承才能构建组件层次的情况。
props 和组合可以清晰而安全地定制组件外观和行为,组件可以接收任意 props,包括基本数据类型、React 元素以及函数。
如果要复用非 UI 的功能,可以提取出一个单独的 JavaScript 模块,如函数、对象或类。组件可以直接导入它们而无需继承。
React 哲学
如何构建一个应用:
1. 将 UI 划分为组件层级
UI 设计师可能已经完成了这一工作,图层名称可能就是最终组件名。
应该由单一功能原则确定组件的范围,也就是说,一个组件原则上只能负责一个功能。
如果模型设计得恰当,UI 和数据模型应该是一一对应的,这是因为 UI 和数据模型会倾向于遵守相同的信息结构。将 UI 分离成组件,其中每个组件都会和数据模型的某部分相对应。
2. 创建静态版本
渲染 UI 和添加交互两个过程最好分开,编写静态版本通常需要大量代码,添加交互则要考虑大量细节。
创建静态版本时,完全不应该使用 state,应该使用 props 传入所需数据。state 代表随时间变化的数据,应只在实现交互时使用。
对于比较简单的应用,自上而下的方式更方便;对于较大型项目,自下而上的构建,并同时为底层组件编写测试是更加简单的方式。
单向数据流(也叫单向绑定)的思想使得组件模块化,易快速开发。
3. 确定 UI state 的最小完整表示
UI 交互需要有改变基础数据模型的能力,React 通过 state 来实现这一点。
为了正确地构建应用,需要找出应用所需的 state 的最小表示,其它数据均由它们计算产生。
一个 state 数据应该满足以下三点:
- 该数据无法由父组件通过
props传递而来; - 该数据随时间而变化;
- 该数据不能根据其它
state或props计算出来。
4. 确定 state 的位置
确定哪个组件能够改变这些 state,或者说拥有这些 state。
根据以下步骤判断 state 的位置:
- 找到根据这个
state进行渲染的所有组件; - 找到它们的共同所有者(common owner)组件;
- 该共同所有者组件或者比它层级更高的组件应该拥有该
state; - 如果没有合适的位置存放该
state,就直接创建一个新组件存放该state,并将这一新组件置于高于共同所有者组件层级的位置。
5. 添加反向数据流
数据反向传递:处于较低层级的组件更新较高层级中的 state。
较高层级的组件传给较低层级组件一个回调函数,以改变高层级组件的 state。
结束
比起写,代码更多是给人看的。
当构建更大的组件库时,代码的模块化和清晰度非常重要。并且随着代码复用程度逐渐加深,代码行数会显著减少。