react基础
react基础
创建项目
npx create-react-app my-app
cd my app
npm start
创建成功的初始项目结构是:
my-app
├─ 📁node_modules
├─ 📁public
│ ├─ 📄favicon.ico
│ ├─ 📄index.html
│ ├─ 📄logo192.png
│ ├─ 📄logo512.png
│ ├─ 📄manifest.json
│ └─ 📄robots.txt
├─ 📁src
│ ├─ 📄App.css
│ ├─ 📄App.js
│ ├─ 📄App.test.js
│ ├─ 📄index.css
│ ├─ 📄index.js
│ ├─ 📄logo.svg
│ ├─ 📄reportWebVitals.js
│ └─ 📄setupTests.js
├─ 📄.gitignore
├─ 📄package-lock.json
├─ 📄package.json
└─ 📄README.md
其中,index.html
是入口文件(单页面应用中唯一的那个页面),src
是源码文件夹。
JSX语法
react文件有两种后缀:.js
和.jsx
。后者就是javascript + xml。是react独有的一种文件,装了插件就有各种支持。
关于jsx
xml
xml是一种数据格式,类似于json,它以标签形式做到:
<user>
<username>fuufhjn</username>
<level>23</level>
</user>
可以理解为严格版html,它的每个标签都必须闭合。
解析jsx
遇到大括号时按js解析,遇到尖括号按xml语法处理。比如如下方式渲染列表:
class Hello extends Component {
render() {
let names = ["fuufhjn", "ChromaticVizier", "ZYP"];
return (
<div>
{
names.map((element, index) => {
return <li key={ index }>{ element }</li>
})
}
</div>
)
}
}
如果想在xml里插入js语句,就必须用大括号包裹。
基本组件写法
一个Hello类继承Component使它可以成为组件,内部定义了它的信息后通过export传递出去:
import react, {Component} from "react";
class Hello extends Component {
render() {
return (
<div>Hello.jsx</div>
)
}
}
export default Hello;
同时在主入口index.js接收并显示:
import React from 'react';
import ReactDOM from 'react-dom/client';
import Hello from './Hello';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div>
<Hello />
</div>
);
元素渲染
React DOM 会将元素和它的子元素与它们之前的状态进行比较,并只会进行必要的更新来使 DOM 达到预期的状态。即react只会更新需要更新的元素,而不是刷新整个页面。比如每隔一秒更新一次时间:
export default class Time extends Component {
render() {
return (
<div>
<h3>Current Time</h3>
<h2>{ new Date().toLocaleDateString() }</h2>
</div>
)
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
function tick() {
root.render(
<div>
<Time />
</div>
);
}
setInterval(tick, 1000);
本例中即使对整个页面的渲染函数做定时器,但是真正每秒更新的其实只有那个h2标签里的时间而已。这就是react刷新速度快的原理。
条件渲染
在 React 中,你可以创建不同的组件来封装各种你需要的行为。然后,依据应用的不同状态,你可以只渲染对应状态下的部分内容。比如,有时需要根据用户是否登录来决定显示哪一个组件。
假设两个组件<UserGreeting />
和<GuestGreeting />
分别表示登录和未登录状态应现实的界面,在index.js可以写一个状态控制函数来决定渲染谁:
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
之后可以把这个函数当做组件来用,在标签内写条件:
const root = ReactDOM.createRoot(document.getElementById('root'));
function main_rander() {
root.render(
<Greeting isLoggedIn={true} />
);
}
不过实际应用中这种看似方便的写法并不常用,更常见的是利用三目运算符:
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
}
另一种常见应用是判断数据是否存在(比如网络请求发送后的渲染):
render() {
let message = {};
// 请求逻辑,假设请求后:
message = {
title: "test data"
};
return (
<div>
{
message.title?
<p>{ message.title }</p>
:<div>waiting for data...</div>
}
</div>
)
}
列表渲染
基本的列表渲染就是用map
遍历再在li标签中返回:
return (
<ul>
{
data.map((element, index) => {
return <><li>{ element.id }</li><li key={ index }>{ element.content }</li></>
})
}
</ul>
)
注意,由于JSX最终会被编译成调用React.createElement()
的普通JavaScript代码,而React.createElement()
一次只能返回单个React节点。也就是说如果想用两个标签并列的情况,就必须在外层包一对标签,最后只能有唯一的父标签。
关于key
Keys 可以在 DOM 中的某些元素被增加或删除的时候帮助 React 识别哪些元素发生了变化,只有key变化的数据才会被重新渲染。因此你应当给数组中的每一个元素赋予一个确定的标识。也就是说每条数据都需要唯一索引。一般用数据的唯一id(后端返回)来当做这个索引,当然demo中由于不涉及数据增删,所以可以让index代劳。
组件
基本用法
组件的后缀可以是.js
或.jsx
,一般用后者。首先在开头导入Component:
import { Component } from "react";
之后可以开始写类组件,其中类名必须遵循大驼峰命名:
export default class ComponentsAndProps extends Component {
render() {
return (
<></>
)
}
}
其中render函数用来提供可渲染视图的方案。return里的内容就是这个组件对外暴露的部分。
组件嵌套
一个组件的返回模板中可以包含另一个组件:
import OutsideComponent from './OutsideComponent';
// ...
return (
<div>
<div>This is an outside component:</div>
<OutsideComponent />
</div>
)
Props
组件可复用,Props用来实现组件之间的交互。在外面可以传值给组件:
const value = "target value.";
function main_rander() {
root.render(
<CAP value={ value }/>
);
}
这样一来,组件内部可以通过this.props.value
接收到value
的值:
export default class ComponentsAndProps extends Component {
render() {
const value = this.props.value;
return (
<div>{ value }</div>
)
}
}
传参可以不止传一个,想传多少都行,组件内部都是通过props接收。props对传递数据的类型没有限制。
props是只读的
无论是函数还是类组件,都不能修改props。实际上函数组件都是纯函数,即它们不会去更改自己的参数。
函数形式的组件
组件也可以用函数来定义,这样相比于类其实好懂很多:
export default function ComponentAndProps(props) {
const value = props.value;
return(
<div>{ value }</div>
)
}
函数组件接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。由于 props
是作为参数直接传入的,所以不需要 this
来访问它。也就是说,它的props实际上并没有被挂载到组件上,并不是它自己的“属性”。一般来说this
指向当前实例(或调用上下文)。但函数组件只是一个普通函数,没有实例的概念,所以 this
不指向组件本身,在严格模式下甚至可能是 undefined
,所以不要手欠加this
。
对于类组件,props
会被挂载到组件实例上,是实例的属性,所以需要通过 this.props
访问。
State
用来记录组件内部的各种状态值。State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件,可以理解为组件内部的内存空间。一个简单的定义方式:
export default class StateAndLifeCycle extends Component {
state = {
userInfo: {
username: "fuufhjn",
level: 23
}
}
render() {
let userInfo = this.state.userInfo;
return (
<div>
<p>{ userInfo.username }</p>
<p>{ userInfo.level }</p>
</div>
)
}
}
一般用构造函数方式定义:
export default class Clock extends Component {
constructor(props) {
super(props);
this.state = {
date: new Date()
};
}
render() {
return (
<div>
<h2>Time: { this.state.date.toLocaleTimeString() }</h2>
</div>
)
}
}
这种写法的原理没啥区别,只是在这个组件类的构造函数里加上state,相当于让它刚创建就有初始的state。
使用State有以下注意事项:
- 不能直接修改state。
this.state.comment = 'Hello';
这种写法不会触发组件重绘。如果想要让组件实时更新,应该用setState
方法,例如this.setState({comment: 'Hello'});
。 - 因为
this.props
和this.state
可能会异步更新,所以不应该依赖他们的值来更新下一个状态。应该让setState()
接收一个函数而不是一个对象。这个函数用上一个 state 作为第一个参数,将此次更新被应用时的 props 做为第二个参数:
这是一段更新State的正确示例代码:
export default class Clock extends Component {
constructor(props) {
super(props);
this.state = {
date: new Date(),
refresh_num: 0
};
this.refresh = this.refresh.bind(this);
}
refresh() {
this.setState((state) => ({
refresh_num: state.refresh_num + 1
}));
console.log(`refresh button pressed: ${this.state.refresh_num.toString()}`);
this.setState(() => ({
date: new Date()
}));
}
render() {
return (
<div>
<h2>Time: { this.state.date.toLocaleTimeString() }</h2>
<button onClick={ this.refresh }>
refresh Time
</button>
</div>
)
}
}
按下刷新按钮后,用两个setState函数分别更新state。可以给它的参数函数传一个state,代表当前的state。
另外注意,在构造函数最后有一个额外的语句:
this.refresh = this.refresh.bind(this);
这是非常重要的。它的基本思想是先获取到refresh函数,之后把这个函数的this绑定到当前组件实例,把绑定后的新函数再赋值回去。如果不绑定,之后在进行这样的回调时:
<button onClick={ this.refresh }>refresh Time</button>
这时函数的this指向的是全局对象或undefined(因为此时refresh函数并不是在组件内部调用的,当你将一个类方法作为事件处理器传递时,这个回调函数的执行位置发生在 React 的事件系统内部)。
绑定后无论 refresh
方法如何被调用,其中的 this
始终指向组件实例,另外也可以在方法使用 this.setState()
、访问 this.state
。不绑定拿不到这俩东西。
当然,还有一种写法可以不用每次都加这么一句,那就是直接使用箭头函数定义:
refresh = () => {
// 箭头函数自动绑定 this
this.setState({ refresh_num: this.state.refresh_num + 1 });
}
事件处理
基本写法
React 事件的命名必须采用小驼峰式,而不是纯小写。使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串:
<button onClick={activateLasers}>
Activate Lasers
</button>
阻止默认事件
不能通过返回 false
的方式阻止默认行为。你必须显式地使用 preventDefault
:
function Form() {
function handleSubmit(e) {
e.preventDefault();
console.log('You clicked submit.');
}
return (
<form onSubmit={handleSubmit}>
<button type="submit">Submit</button>
</form>
);
}
e
是一个合成事件。React 根据 W3C 规范来定义这些合成事件,React 事件与原生事件不完全相同。
事件传参
有时事件需要包含额外的参数,例如当点击列表的每一条时,都需要获取到其中的内容:
printLabel = (element) => {
console.log(`you're clicking: ${element}`);
}
render() {
const data = ['a', 'b', 'c'];
return (
<div>
<ul>
{
data.map((element, index) => {
return <li onClick={ () => this.printLabel(element) } key={ index }>{ element }</li>
})
}
</ul>
</div>
)
}
即直接把回调函数改成形如() => this.printLabel(element)
的传参写法。注意,之所以要写成箭头函数,是因为如果直接写this.printLabel(element)
,渲染元素时就会直接调用该函数而无法等待用户点击。而箭头函数的写法能保证该语句仍然是一个函数类型,而不是它的返回值类型。
获取Event对象
如果想要在传参的同时获取event对象,可以写在后面:
printLabel = (element, event) => {
console.log(`you're clicking: ${element}`);
console.log(event);
}
生命周期
大致上,React的生命周期分为以下三个阶段:Mounting、Updating and Unmounting。

可以看出,mounoting和updating都要经过render,由于updating只是修改所以不需要经过constructor只需拿着新状态重绘。这两个阶段后续流程完全一样,React更新DOM后执行下面的两个钩子函数。而unmounting则是直接进入销毁阶段。这是更详细的示意图:

Mounting
这个阶段组件被创建并插入到 DOM 中。
- 首先调用**
constructor(props)
**。它会初始化组件状态,绑定事件,并且通常不做副作用操作(如 API 请求等)。 - 之后是**
static getDerivedStateFromProps(props, state)
,这个钩子在虚拟DOM生成之后、真实DOM生成之前**执行。根据传入的props
更新状态。它是一个静态方法,不能访问this
,适合需要根据props
调整state
的场景。 - 然后**
render()
**,创建虚拟DOM,执行diff算法,更新DOM树。它会返回组件的 JSX 元素。 - 执行完上面那些,react才会开始更新DOM。
- 更新完后,
componentDidMount()
,组件首次渲染完成后触发,适合做副作用操作,如数据请求或设置订阅,仅在挂载时执行一次。
关于diff算法
这部分的内容请参见这篇专门的帖子:React diff算法详解。
Updating
static getDerivedStateFromProps(props, state)
,和上边一样。- **
shouldComponentUpdate(nextProps, nextState)
**返回一个布尔值,控制组件是否重新渲染,用于性能优化,避免不必要的渲染。对于无需渲染的,这个阶段到此结束。 - **
render()
**和上面一样。 getSnapshotBeforeUpdate(prevProps, prevState)
:在更新前调用的钩子函数,返回的值会传递给componentDidUpdate
。常用于在 DOM 更新前保存信息(如滚动位置)。- **
componentDidUpdate(prevProps, prevState, snapshot)
**组件更新后调用,可处理 DOM 变化后的逻辑,此处可以使用第三个参数snapshot
来处理getSnapshotBeforeUpdate
返回的数据,也可以没有。
Unmounting
- **
componentWillUnmount()
**组件从 DOM 中移除前调用,适合清除副作用(如取消订阅、清除定时器等)。
受控组件
基本写法
受控组件用来处理数据,表单元素的值由 React 的 state 控制,并通过 onChange 等事件处理器来更新 state。比如:
export default class ControlledComponent extends Component {
constructor() {
super();
this.state = {
username: "this is a username"
}
}
changeHandler = (event) => {
console.log(event.target.value);
this.setState(() => ({
username: event.target.vlaue
}))
}
render() {
return (
<div>
<input type='text' value={ this.state.username } onChange={ this.changeHandler } />
</div>
)
}
}
这段代码能够实时获取到一个输入框中的值,每次改变时都打印在控制台。这是通过onChange
实现的。
它首先从事件对象里拿到当前输入框的值:event.target.value
,之后用setState更新状态。由于输入框的值和state绑定,所以改了输入框的值就相当于改了state。如果没有onChange
,是不能在前端修改输入框里的值的,因为那样它相当于是写死了的state中的username值。
应用
假设有三个输入框分别需要获取username、password、email,如果分别写三个onchange函数会很麻烦。可以给每个输入框指定name:
<input type='text' value={ this.state.username } name='username' onChange={ this.handleChange } />
<br/>
<input type='text' value={ this.state.password } name='password' onChange={ this.handleChange } />
<br/>
<input type='text' value={ this.state.email } name='email' onChange={ this.handleChange } />
<br/>
<button>Get data</button>
这样一来只需要一个onChange就能处理这个事:
handleChange = (event) => {
this.setState(() => ({
[event.target.name]: event.target.value
}));
}
这里利用了es6中动态key的特性,它能自动按name更新state。
非受控组件
非受控组件的表单元素的值由 DOM 自身管理,React 通过 ref 来获取 DOM 值。上面的受控组件想获取到里面的值不需要操作DOM,从state拿就行,但是非受控组件state里没有,想要只能从DOM里拿。
有时候用受控组件会导致需要写很多处理事件,很麻烦,不如直接操作DOM。
这部分代码略过,和普通html的用法基本上一样。
状态提升
状态提升是 React 中一种组件间共享状态的技术,指的是将多个组件需要共享的状态提升到它们最近的共同父组件中管理。通过这种方式,子组件可以通过 props 接收数据和回调函数,实现数据的同步和交互。
当多个组件需要反映相同的变化数据时应该使用状态提升。
利用参数实现子组件向父组件传值
基本思想:父组件通过props向子组件传一个带参的函数过去。子组件用自己的参数来调这个函数,实际上的调用位置是在父组件,父组件就可以把这个函数内部写成把参数放入state的形式,这样就能获取到。父组件核心代码:
handleMessage = (message) => {
console.log(message);
this.setState(() => ({
Rcev_message: message
}));
}
这是一个吧参数放入state的函数。另外,它需要把这个函数用props传给子组件:
<Child onMyEvent = { this.handleMessage } />
子组件核心代码:
passToParent = () => {
this.props.onMyEvent(this.state.message);
}
当子组件用自己的state参数调用了父组件传来的函数时,父组件内部就把这个参数放进自己的state了。这里写成一个事件的形式,当然也可以在事件外部这么干。
写成完整形式:

用按钮来触发上面的向父组件传值事件,另外输入框写成一个受控组件的形式,两边都用state来管理状态。
状态提升的基本思想
将多个组件需要共享的状态提升到它们最近的共同父组件中管理。比如,要实现一个摄氏度和华氏度的实时转换器,就可以用状态提升实现。具体实现方式是:两个输入框中的值都由它们共同的父类传过来的props: temprature
控制:
<TempratureInputbox
name='Celsius'
temprature={ celsius }
onTempratureChange={ this.handleCelsiusChange }
/>
<TempratureInputbox
name='Fahrenheit'
temprature={ fahrenheit }
onTempratureChange={ this.handleFahrenheitChange }
/>
同时,父组件给它们分别传了个函数,让它们能修改父组件的temprature
值。但是为什么要分别传两个不一样的呢?这就要看整体的改值流程:
首先,假设改变了摄氏度输入框的值, 就会触发onChange
,改掉父组件里temprature
的值。在父组件里会用新的temprature
重新计算两个温度值再用props传给两个输入框,大致流程如下:

下面的问题是如何计算新值并分配。首先需要判断是谁在改变。假设摄氏度改变了,那么在重新分配时就只需要算一下华氏度对应的值,对于摄氏度只需要原样传回,写成js是这样:
const celsius = this.state.trans_flag === 'f'? tryConvert(this.state.temprature, toCelsius): this.state.temprature;
const fahrenheit = this.state.trans_flag === 'c'? tryConvert(this.state.temprature, toFahrenheit): this.state.temprature;
这个trans_flag
就是改值者的标识,当其中一个子组件改值时会把它连同要改的值一起传给父组件:
handleCelsiusChange = (temprature) => {
this.setState(() => ({
trans_flag: 'c',
temprature
}));
}
这样一来父组件就可以判断是谁在试图改temprature
。如果确定了是摄氏度那边在试图修改,会调一个tryConvert
函数,实现如下:
const tryConvert = (temprature, convert) => {
const input = parseFloat(temprature);
const output = Math.round(convert(input) * 1000) / 1000;
return output;
}
它接收一个函数作为参数,这个函数会传具体的处理逻辑:是摄氏度转华氏度还是反过来。具体传哪个也是由trans_flag
决定。
状态提升设计原则
其实总结下来就是,子组件中的值都由父组件通过props传过来,真正的值本身存在父组件的state
里。子组件想改值就通过参数传,传到父组件后经过处理再分别传给所有子组件。
组合和继承
组合
支持这样的写法:
<Composition>
<p>Content of Composition</p>
</Composition>
export default class Composition extends Component {
render() {
return (
<div>
{ this.props.children }
</div>
)
}
}
即可以在组件标签内直接放东西,组件中可以通过this.props.children
读取。
继承
极少用,略过。