
React 介绍
React
是前端最流行的框架之一,由JSX
语法与组件化开发模式,将原本前端基于DOM
的编程方式变成了基于组件和数据编程,给前端带来的益处是颠覆性的,因为我们知道DOM
操作是 “昂贵” 的,React
在提高应用性能的同时又大大提高了开发效率,所以受到很多前端开发者的支持,也就有了庞大的生态,如今React
已经成为前端工程师之必备技术栈。
创建 React 项目
在创建 React
项目时,可以使用当下最流行的脚手架 create-react-app
和 generator-react-webpack
,前者是由 Facebook
官方出品,后者是社区提供。
create-react-app
create-react-app
适用于大部分项目,集成了对 React
、JSX
、ES6
和 Flow
的支持,支持热更新,默认情况下无需对 Webpack
进行配置,如果要单独配置 Webpack
,需要执行命令弹出配置项,下面命令分别对应安装脚手架工具、构建项目和弹出配置项。
# 安装脚手架
$ npm install -g create-react-app
# 创建项目
$ create-react-app project-name
# 弹射 Webpack 配置文件
$ npm run eject
注意:创建
React
项目时,项目名称不能含大写字母,使用eject
命令弹出配置项的过程不可逆。
generator-react-webpack
generator-react-webpack
适用于构建大型项目,它是需要 yeoman
的支持,几乎具备了 create-react-app
的全部功能,不同的是默认可以对 Webpack
进行配置,生成项目需要手动创建项目根目录,安装脚手架工具和构建项目的命令如下:
# 安装脚手架及依赖
$ npm install -g yo generator-react-webpack
# 创建项目根目录
$ mkdir project-name
# 进入项目目录
$ cd project-name
# 创建项目
$ yo react-webpack
目录结构
我们本次使用 create-react-app
来构建一个项目,并弹出配置项,src
目录为我们主要的开发文件,必须含有一个入口文件 index.js
,所以我们在构建项目后删除 src
中的无用文件,目录结构如下(可以通过 npm run start
启动项目)。
react-demo
|- config
| |- jest
| | |- cssTransform.js
| | |- fileTransform.js
| |- env.js.js
| |- paths.js
| |- webpack.config.dev.js
| |- webpack.config.prod.js
| |- webpackDevServer.config.js
|- public
| |- favicon.ico
| |- index.html
| |- manifest.json
|- scripts
| |- build.js
| |- start.js
| |- test.js
|- src
| |- index.js
|- .gitignore
|- package.json
|- README.md
|- yarn.lock
探索 React
引入 React 变量必须大写
React
的核心模块分为两个,分别为 react
和 react-dom
,前者为 React
的核心逻辑,后者为 React
的渲染逻辑,在 React
中规定引入 react
模块的变量名必须大写。
/* 文件位置:~react-demo/src/index.js */
import react from 'react';
import ReactDOM from 'react-dom';
// 创建一个 JSX
const h1 = (
<h1>hello world</h1>
)
// 渲染到页面
ReactDOM.render(h1, window.root);
如果向上面代码中将引入 react
的变量小写,报错信息如下:

该报错信息的意思是当前使用了 JSX
,必须要有一个大写的 React
,从而可以看出这是 React
所规定的,当将接收 react
的变量改成大写后,页面正常渲染。
React 必须有 createElement 方法
/* 文件位置:~react-demo/src/index.js */
// 创建一个大写的 React 对象
const React = {};
// 创建一个 JSX
const h1 = (
<h1>hello world</h1>
)
为了进一步验证,上面代码中创建一个名为 React
的对象,报错信息如下:

这个报错非常明显的在告诉我们,React
对象中缺少了 createElement
方法,我们将代码修改如下后发现报错信息消失。
/* 文件位置:~react-demo/src/index.js */
// 创建一个大写的 React 对象
const React = {
createElement() {}
};
// 创建一个 JSX
const h1 = (
<h1>hello world</h1>
)
页面 “白屏” 是因为并没有使用 react-dom
进行渲染,我们定义的 h1
是一个组件,同时也是 JSX
,所以会调用 createElement
对 JSX
进行解析。
解析后的 JSX 长什么样
/* 文件位置:~react-demo/src/index.js */
import React from 'react';
// 创建一个 JSX
const h1 = (
<h1>hello world</h1>
)
// 查看 JSX 解析后的结果
console.log(h1);
打开 Chorme
浏览器控制台查看打印结果如下:

从结果可以看出 createElement
方法最终将 JSX
解析成了一个对象结构,其中 props
带表属性对象,其中的 children
代表子元素,也就是文本节点 hello world
,type
代表标签类型为 h1
,这样用来表述 DOM
结构的对象被称为虚拟 DOM
。
模拟解析和渲染过程
在上面我们知道了 React
可以自动将 JSX
转换成虚拟 DOM
,而 ReactDOM
的 render
方法将虚拟 DOM
渲染成了真实的 DOM
,用法如下:
/* 文件位置:~react-demo/src/index.js */
import React from 'react';
import ReactDOM from 'react-dom';
// 创建 JSX
const el = (
<h1 name="hi">
hello
<span>world</span>
</h1>
)
// 渲染到页面
ReactDOM.render(el, window.root);
查看页面可以看到正常渲染了,现在就用前面对 React
的了解来简单模拟解析与渲染的过程,代码如下:
/* 文件位置:~react-demo/src/index.js */
// 创建 React 对象和 createElement 方法
const React = {
createElement(type, props, ...children) {
return { type, props, children };
}
};
// 创建 JSX
const el = (
<h1 name="hi">
hello
<span>world</span>
</h1>
)
// 渲染的 render 方法
function render(vnode, container) {
// 如果是字符串说明是文本节点,创建文本节点并插入到父元素中
if (typeof vnode === 'string') {
return container.appendChild(document.createTextNode(vnode));
}
// 如果不是字符串说明是元素节点,解构元素类型、属性和子元素的数组
const { type, props, children } = vnode;
// 创建元素
const tag = document.createElement(type);
// 循环添加属性
for (let key in props) {
tag.setAttribute(key, props[key]);
}
// 循环子元素,并递归创建子元素
children.forEach(child => {
render(child, tag);
});
// 将元素插入到容器中,root
container.appendChild(tag);
}
// 渲染虚拟 DOM
render(el, window.root);
通过上面实现的代码同样可以完成渲染,当然仅限于简单结构,React
内部的实现更为复杂,兼容了多种组件类型和复杂的 DOM
结构。
JSX 最外层只能有一个元素
/* 文件位置:~react-demo/src/index.js */
import React from 'react';
import ReactDOM from 'react-dom';
// 创建 JSX
const el = (
<h1 name="hi">hello</h1>
<div>world</div>
)
ReactDOM.render(el, window.root);
在对上面代码中的 JSX
进行渲染时会有如下报错信息。

上面的报错信息告诉我们 JSX
元素必须包裹在一个闭合的标签内,所以说在写 JSX
语法的时候我们必须保证最外层只有一个元素节点。
React 的基本使用
在
JSX
全称为JavaScript XML
,但是和普通的HTML
相比,有一些不同的用法,如元素属性class
、for
、style
、dangerouslyInnerHTML
以及注释写法等等。
className 属性
在 JSX
语法中,在标签中应使用 className
替代 HTML
中的 class
属性,因为在 JavaScript
中 class
为关键字。
/* class 属性在 JSX 中的写法 */
import React from 'react';
import ReactDOM from 'react-dom';
ReactDOM.render(<h1 className="active">hello</h1>, window.root);
htmlFor 属性
在 HTML
中,通过点击 label
标签让 input
输入框获取焦点是很常见的,只需要让 label
标签 for
属性的值与 input
标签的 id
值相等即可,但是在 JSX
中这这样的写法会报错,必须将 label
标签的 for
属性使用 htmlFor
替代,代码如下:
/* for 属性在 JSX 中的写法 */
import React from 'react';
import ReactDOM from 'react-dom';
const el = (
<div>
<h1>hello</h1>
<label htmlFor="username">用户名</label>
<input type="text" id="username" />
</div>
)
ReactDOM.render(el, window.root);
style 属性
/* style 属性错误的写法 */
import React from 'react';
import ReactDOM from 'react-dom';
const el = (
<h1 style="color: red;">hello</h1>
)
ReactDOM.render(el, window.root);
在 JSX
中关于 style
属性的写法发生了变化,如果用 HTML
中的写法会报错,错误信息如下:

报错信息中明确的告诉我们 style
属性必须是一个含有代表样式键值的对象,而不是一个字符串,并给出正确的结构,正确的写法如下:
/* style 属性在 JSX 中的写法 */
import React from 'react';
import ReactDOM from 'react-dom';
const el = (
<h1 style={{color: 'red'}}>hello</h1>
)
ReactDOM.render(el, window.root);
注意:在解析
JSX
的过程中,<
和>
包裹JSX
元素,元素属性中最外层的{
和}
包裹JS
代码,而内层的{
和}
则代表一个JS
对象,所以style
是被两层 “花括号” 所包裹,并不是mustache
语法。
取值表达式
在 JSX
中,所有的 JS
代码都可以写在 JSX
元素起始和闭合标签中间的 {
和 }
内,会将执行结果渲染到该元素上。
/* 取值表达式的使用 */
import React from 'react';
import ReactDOM from 'react-dom';
const str = 'world';
const obj = { hello: 'world' };
const fn = () => <p>hello</p>;
const el = (
<div>
<h1>{fn()}</h1>
<div>{str}</div>
<div>{JSON.stringify(obj)}</div>
<div>{true ? <span>nihao</span> : null}</div>
</div>
)
ReactDOM.render(el, window.root);
启动项目可以看到页面上已经成功的渲染了 hello
、world
、{ hello: 'world' }
和 nihao
,上面三元运算符结果如果为 null
则不会渲染这个节点,viod 0
与 null
作用相同。
dangerouslySetInnerHTML 属性
在 JSX
中,如果想要把一个含有标签元素的字符串插入到某一个节点中,应该使用 dangerouslySetInnerHTML
替代原生 JS
中的 innerHTML
。
/* dangerouslySetInnerHTML 的用法 */
import React from 'react';
import ReactDOM from 'react-dom';
const str = '<h1>hello</h1>';
const el = (
<h1 dangerouslySetInnerHTML={{__html: str}}></h1>
)
ReactDOM.render(el, window.root);
在上面的代码中,dangerouslySetInnerHTML
属性的值为对象,将要插入的 HTML
字符串作为对象中 __html
属性的值即可,设置 dangerouslySetInnerHTML
属性的 JSX
元素中不能有任何的子元素。
注意:
dangerouslySetInnerHTML
属性非常危险,容易引发XSS
攻击,轻易不要使用。
JSX 中注释的写法
在 JSX
的 DOM
结构中,如果需要对代码进行注释不能使用 JS
中的 // 注释
,也不能使用 HTML
中的 <!-- 注释 -->
,注释必须使用 { }
包裹,写法如下:
/* 注释在 JSX 中的写法 */
import React from 'react';
import ReactDOM from 'react-dom';
const el = (
<div>
<h1 name="hi">hello</h1>
{/* 这是注释,支持多行 */}
<span>world</span>
</div>
)
ReactDOM.render(el, window.root);
Fragment 组件
在 React 16.3
中提供了一个组件,类似于原生 JS
中的文档碎片,可以将多个元素包裹起来,却不会被渲染,用法如下:
/* Fragment 组件的使用 */
import React, { Fragment } from 'react';
import ReactDOM from 'react-dom';
const el = (
<Fragment>
<h1>hello</h1>
<div>world</div>
</Fragment>
)
ReactDOM.render(el, window.root);
循环动态创建 JSX 结构
在 React
中不存在过多的 API
,最大的特点就是 JSX
语法可以将 JS
与 HTML
混写(函数式编程),借助原生 JS
的方法实现功能,比如可以使用循环创建 JSX
结构。
/* 循环在 JSX 中的应用 */
import React from 'react';
import ReactDOM from 'react-dom';
const arr = [1, 2, 3];
const el = (
arr.map((item, index) => {
return (
<li key={index}>{item}</li>
)
})
)
ReactDOM.render(el, window.root);
上面成功的渲染除了一个列表,但是有两点需要注意:
- 第一点是循环一定要使用具有返回值的方法,如
map
、filter
等;- 第二点是每一个循环出来的
JSX
元素必须绑定一个key
属性,可以使用数据的id
(优先),也可以使用数组的索引。
组件
在上面所有代码中的
JSX
都很不优雅,如果一个项目非常大,这样的混乱的结构是难以维护的,组件就是为了更好的维护和复用相同的JSX
结构以及提高工作效率而存在的。
函数组件
在 React
中可以通过函数创建组件,函数名称就是组件名,必须大写,必须有返回值,可以为 JSX
,也可以为 null
,通过单闭合和双闭合两种方式调用组件,可以通过属性传参,并通过函数组件的第一个参数接收,实现代码如下:
/* 函数组件 */
import React from 'react';
import ReactDOM from 'react-dom';
// 创建一个函数组件
function Build(props) {
return (
<div>
<h1>{props.title}</h1>
<div>{props.content}</div>
</div>
)
}
// 渲染组件
ReactDOM.render((
<div>
<Build title='1' content='1xx'></Build> {/* 双闭合 */}
<Build title='2' content='2xx' /> {/* 单闭合 */}
</div>
), window.root);
函数组件缺点(
16.3
以前):
- 在函数组件内部
this
为undefined
;- 在函数组件内部没有状态,即只能使用通过属性传递的参数,却没有更改的能力;
- 函数组件没有生命周期,无法使用生命周期 “钩子” 完成一些操作。
由于函数组件的缺陷,所以更适合渲染一些静态的不需要数据变化的结构,如果想要让传入的属性变化可以通过不断执行 React.render
的方式不断更新传入组件参数的值,下面是一个时钟案例,通过函数组件实现时间的变化。
/* 函数组件多次渲染 */
import React from 'react';
import ReactDOM from 'react-dom';
// 创建函数组件
function Clock(props) {
return (
<div>
<h1>当前时间</h1>
<div>{props.time}</div>
</div>
)
}
// 每秒渲染一次组件
setInterval(() => {
ReactDOM.render(
<Clock time={new Date().toLocaleString()} />,
window.root
);
}, 1000);
类组件
类组件解决了函数组件所有的缺陷,是通过类声明的。
/* 类组件 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 创建类组件
class Clock extends Component {
// constructor(props) {
// super(props);
// this.state = {
// time: new Date().toLocaleString();
// }
// }
// 等价于 constructor 的写法,更简洁
state = {
time: new Date().toLocaleString()
}
// 生命周期
componentDidMount() {
// Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
setInterval(() => {
this.setState({ time: new Date().toLocaleString() });
}, 1000);
}
// 渲染这个组件会调用 render 方法
render() {
return (
<div>
时间:<span>{this.state.time}</span>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Clock />, window.root);
在上面的类组件中,我们同样使用了一个简单的时钟功能,可以看出类组件即有 this
,又能创建和更新状态,也可以通过生命周期进行一些操作。
所有的类组件都需要继承 React.Component
,这样就可以使用 React.Component
的原型方法 setState
对状态进行更新,每次更新,都会使组件重新渲染,但是只会重新渲染变化的 DOM
,这是 ReactDOM
通过 diff
算法所做的优化。
类组件中添加事件
在平时开发中每个组件都会有一些对应的功能,这就需要事件的配合,在类组建中绑定事件大概有四种方式,我们还是用上面的时钟案例,给该组件添加一个按钮,在点击时卸载这个组件。
/* 方式 1:使用箭头函数直接绑定事件 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 创建类组件
class Clock extends Component {
state = {
time: new Date().toLocaleString()
}
// 生命周期
componentDidMount() {
// Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
setInterval(() => {
this.setState({ time: new Date().toLocaleString() });
}, 1000);
}
// 渲染这个组件会调用 render 方法
render() {
return (
<div>
时间:<span>{this.state.time}</span>
<button onClick={() => {
// 卸载组件的方法
ReactDOM.unmountComponentAtNode(window.root);
}}>
kill
</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Clock />, window.root);
/* 方式 2:使用 bind 绑定函数 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 创建类组件
class Clock extends Component {
state = {
time: new Date().toLocaleString()
}
// 生命周期
componentDidMount() {
// Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
setInterval(() => {
this.setState({ time: new Date().toLocaleString() });
}, 1000);
}
// 点击事件
handleClick() {
ReactDOM.unmountComponentAtNode(window.root);
}
// 渲染这个组件会调用 render 方法
render() {
return (
<div>
时间:<span>{this.state.time}</span>
<button onClick={this.handleClick.bind(this)}>kill</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Clock />, window.root);
上面两种方式都有一个共同的问题,箭头函数的方式在每次执行 render
时都会创建新的箭头函数,而将函数作为原型方法,通过 bind
是为了修正方法内部的 this
指向,但是每次执行 render
时,bind
也会返回一个新的函数。
/* 方式 3:在方式 2 的基础上提前生成函数 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 创建类组件
class Clock extends Component {
constructor(props) {
super(props);
this.state = {
time: new Date().toLocaleString()
};
this.fn = this.handleClick.bind(this);
}
// 生命周期
componentDidMount() {
// Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
setInterval(() => {
this.setState({ time: new Date().toLocaleString() });
}, 1000);
}
// 点击事件
handleClick() {
ReactDOM.unmountComponentAtNode(window.root);
}
// 渲染这个组件会调用 render 方法
render() {
return (
<div>
时间:<span>{this.state.time}</span>
<button onClick={this.fn}>kill</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Clock />, window.root);
这样就解决了上面每次执行 render
就创建新函数的问题,但是这样的写法并不优雅,又产生了新的问题,所有的事件执行函数全都添加到了组件的实例上,而且代码会随着事件的增加而越来越乱。
/* 方式 4:使用 ES7 语法将原型方法使用箭头函数 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 创建类组件
class Clock extends Component {
state = {
time: new Date().toLocaleString()
}
// 生命周期
componentDidMount() {
// Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
setInterval(() => {
this.setState({ time: new Date().toLocaleString() });
}, 1000);
}
// 点击事件
handleClick = () => {
ReactDOM.unmountComponentAtNode(window.root);
}
// 渲染这个组件会调用 render 方法
render() {
return (
<div>
时间:<span>{this.state.time}</span>
<button onClick={this.handleClick}>kill</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Clock />, window.root);
使用 ES7
的新语法,既解决了事件处理函数方法内部 this
指向问题,又解决了每次执行 render
创建新函数的问题,但需要依赖 @babel/plugin-proposal-class-properties
插件来解析。
卸载组件后不能再更新状态
还是上面的时钟案例,我们知道卸载一个组件应该使用 ReactDOM.unmountComponentAtNode
方法,参数一个组件,执行后会卸载这个组件内部所有的组件。
当真正点击时钟组件的按钮去卸载组件,组件虽然成功卸载了,但是控制台报错了,报错信息如下:

这个报错信息的意思是告诉我们在组件卸载后不能再通过 setState
更新状态,所以我们要在组件卸载之前先清空调用 setState
的定时器,代码修改如下:
/* 完整的时钟组件 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 创建类组件
class Clock extends Component {
state = {
time: new Date().toLocaleString()
}
// 生命周期
componentDidMount() {
// Component 组件有一个 setState 方法可以更新状态,每次调用组件会重新渲染
this.timer = setInterval(() => {
this.setState({ time: new Date().toLocaleString() });
}, 1000);
}
// 组件将要卸载时清空定时器
componentWillUnmount() {
clearInterval(this.timer);
}
// 点击事件
handleClick = () => {
ReactDOM.unmountComponentAtNode(window.root);
}
// 渲染这个组件会调用 render 方法
render() {
return (
<div>
时间:<span>{this.state.time}</span>
<button onClick={this.handleClick}>kill</button>
</div>
)
}
}
// 渲染组件
ReactDOM.render(<Clock />, window.root);
在这个组件中用到了两个生命周期 “钩子”,componentDidMount
钩子在组件挂载后执行,类似于原生 JS
的 window.onload
,componentWillUnmount
钩子在组件将要卸载之前执行,后面会涉及更多生命周期钩子,我们会在这个 React
基础篇系列文章中一一说明。
类组件的参数传递
/* 类组件传参第一种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
const p = { name: 'panda', age: 28 };
class Person extends Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<p>{this.props.name}</p>
<p>{this.props.age}</p>
</div>
)
}
}
// 分别传入想要的属性
ReactDOM.render(<Person name={p.name} age={p.age} />, window.root);
/* 类组件传参第二种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
const p = { name: 'panda', age: 28 };
class Person extends Component {
render() {
const { name, age } = this.props;
return (
<div>
<p>{name}</p>
<p>{age}</p>
</div>
)
}
}
// 传入整个对象
ReactDOM.render(<Person {...p} />, window.root);
上面两种传参方式第一种是将对象中希望传入的属性传递给组件,第二种方式是将整个对象通过解构的方式直接传递给组件,而组件中可以在 constructor
中的第一个参数接收 props
,也可以直接使用 this.props
,因为 React
在组件创建实例调用 super
之前就已经将 props
作为了实例属性。
组件参数的类型校验
在 React
组件传递参数时,是通过 props
取出传入的参数直接使用,传入的值类型并没有做任何的校验,这就可能造成传参时出现错误,在 React
生态中有一个第三方模块 prop-types
可以规定参数的类型,并对传入的参数进行校验,使用前需安装。
$ npm install prop-types
/* 使用 prop-types 校验传给组件的参数 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types';
const p = {
name: 'panda',
age: 28,
gender: '男',
hobby: ['basketball', 'swim'],
pos: { x: 433, y: 822 },
salary: 5000
}
class Person extends Component {
// 定义默认属性,React 自带
static defaultProps = {
name: 'shen'
}
// 定义属性类型
static propTypes = {
name: PropTypes.string.isRequired, // 类型必须为字符串,必填项
age: PropTypes.number, // 类型必须为数字
gender: PropTypes.oneOf(['男', '女']), // 性别只能为男或女
hobby: PropTypes.arrayOf(PropTypes.string), // 数组成员类型必须是字符串
pos: PropTypes.shape({ // 限制模型内部类型
x: PropTypes.number.isRequired,
y: PropTypes.number.isRequired
}),
// 第一个参数为原对象,第二个参数为当前属性,第三个参数为类
salary(obj, key, P) {
// 自行校验
if (obj[key] < 3000) {
throw new Error('工资太低');
}
}
}
render() {
const { name, age } = this.props;
return (
<div>
<p>{name}</p>
<p>{age}</p>
</div>
)
}
}
ReactDOM.render(<Person {...p} />, window.root);
使用 prop-types
必须在类组件上添加一个静态属性 propTypes
,在内部定义属性的类型,其中 isRequired
为必填项,如果没有传参会报错,在检测是会优先检测 React
的静态属性 defaultProps
,即默认属性,如果 defaultProps
存在则视为已经有该参数。
oneOf
方法参数为一个数组,传给组件对应的参数值必须是传给 oneOf
数组中的其中一项,否则会报错,arrayOf
方法用于限制数组成员的类型,shape
方法用于限属性值为对象的内部属性类型,参数为对象。
在 propTypes
静态属性中以传入的属性名作为方法名,则该方法为自定义校验该属性的函数,参数的前三项为原对象,属性名和所属类,可以在函数内部自行实现校验逻辑。
setState 更新状态
在前面的时钟组件中已经简单的使用过 setState
,在这里我们会对 setState
的用法通过一个计数器案例来做详细说明。
/* 计数器案例 1 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
state = { num: 0 }
handleClick = () => {
this.setState({ num: this.state.num + 1 });
this.setState({ num: this.state.num + 1 });
}
render() {
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
在上面的计数器中,当我们点击按钮时会执行 handleClick
,而在 handleClick
内部调用了两次 setState
更新状态,但是我们启动项目后发现只有一次是有效的,这也说明了一个问题,setState
是异步执行的,最后一次执行的会覆盖前一次,其实在 setState
方法调用时支持传入一个回调函数,代码如下:
/* 计数器案例 2 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
state = { num: 0 }
handleClick = () => {
this.setState({ num: this.state.num + 1 }, () => {
this.setState({ num: this.state.num + 1 });
});
}
render() {
console.log('render');
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>+</button>
</div>
)
}
}
ReactDOM.render(<Counter />, window.root);
setState
传入的回调会在更新状态成功后执行,所以将代码修改后两次 setState
都生效了,render
执行了两次,这样的写法如果调用 setState
次数多了就形成了 “回调地狱”,setState
还有另一种用法如下:
/* 计数器案例 3 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Counter extends Component {
state = { num: 0 }
handleClick = () => {
this.setState(prevState => ({ num: prevState.num + 1 }));
this.setState(prevState => ({ num: prevState.num + 1 }));
}
render() {
console.log('render');
return (
<div>
{this.state.num}
<button onClick={this.handleClick}>+</button>
</div>
);
}
}
ReactDOM.render(<Counter />, window.root);
setState
方法可直接传入一个函数,函数的参数为上一次更新的 state
,也就是 this.state
,此时执行 setState
只更新状态,不重新渲染,当最后一次更新状态后统一渲染一次(也叫 setState
合并)。
触发组件重新渲染的两种方式:
props
发生变化,如调用render
并传入新的属性值;- 调用
setState
重新设置状态。
受控组件和非受控组件
对于组件的分类除了可以按照组件的创建方式分为函数组件和类组件,还有另外一种分类方式,就是受控组件和非受控组件,简单来说 “受控” 和 “非受控” 就是指是否受到状态的控制,这种分类方式多用于表单元素,同时也指对于表单元素数据的不同处理方式。
受控组件
下面是一个受控组件的写法,输入框的初始值是通过 value
和 defaultValue
属性绑定的状态的值。
/* 受控组件 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Control extends Component {
state = {
msg1: 'hello',
msg2: 'world'
}
render() {
return (
<div>
<input type="text" value={this.state.msg1} /> {/* 报错 */}
<input type="text" defaultValue={this.state.msg2} /> {/* 不报错 */}
</div>
)
}
}
ReactDOM.render(<Control />, window.root);
上面的代码中是两种绑定初始值的方式,使用 defaultValue
属性可以正常的将状态中的属性作为初始值绑定到页面的输入框内,但是随着输入的变化并没更新状态的作用,而使用 value
做了同样的绑定后,虽然页面正常显示初始值,但是控制台报错了,报错信息如下:

输入框的值可以通过输入改变,但受控组件要求状态的值要随着输入框内的值改变而更新,而报错信息告诉我们想要达到这样的目的必须要给表单元素绑定一个 onChange
事件,这个功能其实就是输入框与数据的双向绑定,修改后的实现如下:
/* 受控组件 —— 修改后 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Control extends Component {
state = {
msg: 'hello'
}
changeHandler = e => {
this.setState({ msg: e.target.value });
}
render() {
return (
<div>
<input
type="text"
value={this.state.msg}
onChange={this.changeHandler}
/>
{this.state.msg}
</div>
)
}
}
ReactDOM.render(<Control />, window.root);
上面的代码中在 onChange
事件中调用了 setState
并更新了状态,但是如果有多个输入框,要保证 onChange
事件的复用,实现不同的输入框输入时 onChange
事件时更新不同的状态,实现如下:
/* 受控组件 —— 多个输入框复用 onChange */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class Control extends Component {
state = {
msg1: 'hello',
msg2: 'world'
}
changeHandler = e => {
const val = e.target.name;
this.setState({ [val]: e.target.value });
}
render() {
return (
<div>
<input
type="text"
name="msg1"
value={this.state.msg1}
onChange={this.changeHandler}
/>
{this.state.msg1}
<br />
<input
type="text"
name="msg2"
value={this.state.msg2}
onChange={this.changeHandler}
/>
{this.state.msg2}
</div>
)
}
}
ReactDOM.render(<Control />, window.root);
上面通过给 input
标签添加和状态的变量名相同的 name
属性,在触发 onChange
事件时用 name
属性作为更新状态数据的键值。
受控组件的好处是,可以实时对输入框输入的值进行校验,并可以随着输入框的内容更新而更新状态,进而更新视图。
非受控组件
非受控组件与受控组件相比就是直接操作 DOM
来操作表单元素,直接操作 DOM
可以在 componentDidMount
生命周期内(DOM
完全挂载),写法如下:
/* 非受控组件 —— 直接操作 DOM(不建议) */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class UnControl extends Component {
componentDidMount() {
const username = document.getElementById('username');
username.value = 123;
console.log(username.value);
}
render() {
return (
<div>
<input type="text" id="username" />
</div>
)
}
}
ReactDOM.render(<UnControl />, window.root);
当然在 React
中并不会这么写,React
专门给我们提供了操作 DOM
属性 ref
,用法如下:
/* 非受控组件 —— ref 常用写法 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class UnControl extends Component {
handleClick = () => {
// 打印输入框的值
console.log(this.userDom.value);
}
render() {
return (
<div>
<input
type="text"
id="username"
ref={dom => this.userDom = dom}
/>
<button onClick={this.handleClick}>Click</button>
</div>
)
}
}
ReactDOM.render(<UnControl />, window.root);
使用 ref
属性的方式通常会在其中传入一个函数,这个函数的参数就是当前表单元素对应的 DOM
,通常情况下会使用类组件的一个属性来存储这个 DOM
,方便在其他的事件或生命周期 “钩子” 中使用。
在 React 16.3
中推出了操作非受控组件的新的 API
React.createRef
方法,返回值是一个对象,将这个对象绑定在表单元素的 ref
上,则可以通过这个对象的 current
属性获取这个表单元素的 DOM
元素。
/* 非受控组件 —— React 16.3 新 API */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
class UnControl extends Component {
userDom = React.createRef();
handleClick = () => {
// 打印输入框的值
console.log(this.userDom.current.value);
}
render() {
return (
<div>
<input type="text" id="username" ref={this.userDom} />
<button onClick={this.handleClick}>Click</button>
</div>
)
}
}
ReactDOM.render(<UnControl></UnControl>, window.root);
我们其实把 React.createRef
的返回值存储为了类组件的一个属性,并将这个属性传入 ref
,这样可以在其他的事件或生命周期 “钩子” 中操作 DOM
,如果存在多个这样的表单元素,许多次调用 React.createRef
,并分别将存储返回值的类组件属性传入各个表单的 ref
中。
非受控组件的好处是,操作
DOM
方便,可以与更多基于DOM
操作的第三方库结合。
复合组件
复合组件指的就是存在父子关系的组件嵌套,在
React
中有三种形式的父子组件嵌套:
- 父组件中返回
JSX
中直接包含子组件;children
的方式引入子组件;render props
的方式引入子组件。
第一种是直接将子组件在父组件中引入,并放在父组件 render
方法返回的 JSX
中。
/* 复合组件 —— 第一种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 父组件
class Parent extends Component {
render() {
return (
<div>
这是父组件
<Child />
</div>
)
}
}
// 子组件
class Child extends Component {
render() {
return (
<div>这是子组件</div>
)
}
}
ReactDOM.render(<Parent />, window.root);
我们前面提到过组件可以通过单闭合或者双闭合的方式调用,第二种方式就是利用双闭合的调用方式,在父组件中引入子组件,把父组件中某些 JSX
放在双闭合的子组件标签中,作为参数传递给子组件,在子组件中通过 props
的 children
属性进行接收,并放入对应的位置。
/* 复合组件 —— 第二种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 父组件
class Parent extends Component {
render() {
return (
<div>
这是父组件
<Child>
<div>父组件传递给子组件的 JSX</div>
</Child>
</div>
)
}
}
// 子组件
class Child extends Component {
render() {
return (
<div>
这是子组件
{this.props.children}
</div>
)
}
}
ReactDOM.render(<Parent />, window.root);
第三种方式是将子组件作为一个函数的返回值,而函数作为父组件的 props
参数传入父组件,父组件返回的 JSX
中调用函数返回子组件,又叫 render props
。
/* 复合组件 —— 第三种方式 */
import React, { Component } from 'react';
import ReactDOM from 'react-dom';
// 父组件
class Parent extends Component {
render() {
return (
<div>
这是父组件
{this.props.buildChild()}
</div>
)
}
}
// 子组件
class Child extends Component {
render() {
return (
<div>
这是子组件
{this.props.children}
</div>
)
}
}
// render props 函数
const buildChildFn = () => {
return <Child />
}
ReactDOM.render(<Parent buildChild={buildChildFn} />, window.root);
总结
这是系列关于
React
基础的文章,本篇是关于React
的一些基础知识,也包含了一些React 16
版本的一些新增内容,比较适合不了解React
框架的同学们从零开始入门,在后面会陆续更新关于复合组件参数传递、生命周期等内容。