React
来点前端
1. 介绍
React 是一个用于构建用户界面的 JavaScript 库
空模板展示
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Hello World</title>
<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
</head>
<body>
<div id="root"></div>
<!-- 开始写脚本 -->
<script type="text/babel">
// React 将替换 DOM 容器中的任何现有内容,所以建议为空
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<h1>Hello, world!</h1>);
</script>
<!-- 结束写脚本 -->
</body>
</html>
2. JSX
被称为 JSX,它是 JavaScript 的语法扩展,JSX 产生 React “元素”,可渲染到 DOM 中
const element = <h1>Hello, world!</h1>;
const element = (
<div>
<h1>Hello!</h1>
<h2>Good to see you here.</h2>
</div>
);
JSX 中嵌入表达式
任何有效的 JavaScript 表达式 放在 JSX 中的花括号内解析
const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
JSX 也是一个表达式,编译后成为常规的 javascript 函数调用并计算为 javascript 对象,意味着可在 if、for 中使用,将其分配给变量,或作为参数接受,或函数中返回
function getGreeting(user) {
if (user) {
return <h1>Hello, {formatName(user)}!</h1>;
}
return <h1>Hello, Stranger.</h1>;
}
JSX 是一个语法糖,Babel 将 JSX 转成 React.createElement() 产生 React element,React 读取这个对象来构造 DOM
// 语法糖
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
// 创建 React element
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
// React 对象
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
2. 渲染元素
React element 描述了在屏幕上展示的内容
const element = <h1>Hello, world</h1>;
React DOM 负责更新浏览器 DOM 以匹配 React 元素
渲染一个 React 元素,首先要将 DOM 元素传递给 ReactDOM.createRoot() 创建出 React DOM 元素(root),然后再将 React 元素传递给 root.render()
const root = ReactDOM.createRoot(document.getElementById('root'));
const element = <h1>Hello, world</h1>;
root.render(element);
React 元素是不可变的,一旦你创建了一个元素,你就不能改变它的子元素或属性。目前更新的方法是创建新元素并传递给 root.render()
const root = ReactDOM.createRoot(
document.getElementById('root')
);
function tick() {
const element = (
<div>
<h1>Hello, world!</h1>
<h2>It is {new Date().toLocaleTimeString()}.</h2>
</div>
);
root.render(element);
}
setInterval(tick, 1000);
React 只会更新必要的部分,React DOM 比较元素及其子元素内容先后的不同,而在渲染过程中只会更新改变了的部分
3. 组件
组件在概念上类似于 JavaScript 函数,它接受任意的入参(即 “props”),并返回 React 元素用于描述页面展示内容
// DOM 标签(非组件)
const element = <div />;
// 下面二者是等效的
// 函数组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
// 类组件
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
// 标签组件,自定义标签为组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
const element = <Welcome name="Sara" />;
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(element);
组件名为大写,JSX 属性为小写驼峰
组合组件
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
function App() {
return (
<div>
<Welcome name="Sara" />
<Welcome name="Cahal" />
<Welcome name="Edite" />
</div>
);
}
函数也作为标签使用
props 的只读性,所有 React 组件都必须像纯函数一样保护它们的 props 不被更改,提供了 state 来动态变化
function sum(a, b) {
return a + b;
}
function change(a, b) {
a = b;
return a + b;
}
4. state & 生命周期
React 元素是不可变的,但我们希望只编写一次代码就可以实现组件自动更新,需要借助 state 来实现(State 与 props 类似,但是 state 是私有的,并且完全受控于当前组件,state 变更会调用 render方法),首要要先函数组件转成类组件:
- 创建一个同名的 ES6 class,并且继承于
React.Component
- 添加一个空的
render()
方法 - 将函数体移动到
render()
方法之中 - 在
render()
方法中使用this.props
替换props
- 删除剩余的空函数声明
class Clock extends React.Component {
render() {
return (
<div>
<h1>Hello, world!</h1>
<h2>It is {this.props.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
每次组件更新时 render 方法都会被调用
生命周期方法
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, world!</h1>
<h2>It is {this.state.date.toLocaleTimeString()}.</h2>
</div>
);
}
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Clock />);
不要直接修改 state:
// Wrong
this.state.comment = 'Hello';
// Correct
// 应该使用 setState():
this.setState({comment: 'Hello'});
State 的更新可能是异步的,可能会把多个 setState()
调用合并成一个调用
// Wrong
// 多个setState调用会合并,结果可能不正确
this.setState({
counter: this.state.counter + this.props.increment,
});
// Correct
// 改成函数即可
this.setState((state, props) => ({
counter: state.counter + props.increment
}));
state的更新合并
// 里面有 posts、comments
constructor(props) {
super(props);
this.state = {
posts: [],
comments: []
};
}
// 只会更新 comments、posts还是存在的
this.setState({comments}
5. 事件处理
React 元素的事件需要传入的一个字符串函数解析,而传统的 html 是传入一个方法调用
<button onclick="activateLasers()">
Activate Lasers
</button>
//
<button onClick={activateLasers}>
Activate Lasers
</button>
class Toggle extends React.Component {
constructor(props) {
super(props);
this.state = {isToggleOn: true};
// 为了在回调中使用 `this`,这个绑定是必不可少的
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// xxx
}
render() {
return (
// 这里的this.handleClick 或者 handleClick()
// 但 react 用前者,所以得绑定
<button onClick={this.handleClick}></button>
);
}
}
小写驼峰命名法
class 的方法不会绑定 this,没有绑定而传入了 onClick,调用时 this 为 undefined
向事件处理函数传递参数
// 显隐式的传递 e 事件
// this 的 react 方式隐式传递了 event
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
6. 条件渲染
if 语句
function UserGreeting(props) {
return <h1>Welcome back!</h1>;
}
function GuestGreeting(props) {
return <h1>Please sign up.</h1>;
}
function Greeting(props) {
const isLoggedIn = props.isLoggedIn;
if (isLoggedIn) {
return <UserGreeting />;
}
return <GuestGreeting />;
}
const root = ReactDOM.createRoot(document.getElementById('root'));
// 直接传参
root.render(<Greeting isLoggedIn={false} />);
元素变量判断
render() {
const isLoggedIn = this.state.isLoggedIn;
let button;
if (isLoggedIn) {
button = <LogoutButton onClick={this.handleLogoutClick} />;
} else {
button = <LoginButton onClick={this.handleLoginClick} />;
}
return (
<div>
{button}
</div>
);
}
&& 内联判断
function Mailbox(props) {
const unreadMessages = props.unreadMessages;
return (
<div>
<h1>Hello!</h1>
{unreadMessages.length > 0 &&
<h2>
You have {unreadMessages.length} unread messages.
</h2>
}
</div>
);
}
const messages = ['React', 'Re: React', 'Re:Re: React'];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Mailbox unreadMessages={messages} />);
JavaScript 中:true && expression 返回 expression, false && expression 返回 false
三目运算符
render() {
const isLoggedIn = this.state.isLoggedIn;
return (
<div>
{isLoggedIn
? <LogoutButton onClick={this.handleLogoutClick} />
: <LoginButton onClick={this.handleLoginClick} />
}
</div>
);
}
阻止组件渲染
function WarningBanner(props) {
if (!props.warn) {
return null;
}
return (
<div className="warning">
Warning!
</div>
);
}
返回 null,不会影响生命周期,componentDidUpdate 还会继续执行
7. list & key
数组转为元素列表
// 使用 {} 在 JSX 内构建一个元素集合
const numbers = [1, 2, 3, 4, 5];
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((item) =>
<li key={number.toString()> // 为每个列表元素分配一个key
{item} // 解析
</li>
);
return (
<ul>{listItems}</ul> // 解析-构建元素集合
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<NumberList numbers={numbers} />);
a key should be provided for list items(创建一个元素时必须包括一个特殊的
key
属性)key 帮助 React 识别哪些元素改变了,比如被添加或删除,因此要为数组中的每一个元素赋予一个确定的标识
列表的 key
// key 是在该列表中唯一标识,通常选择数据的id,当没有时可以使用index代替
const todoItems = todos.map((todo) =>
<li key={todo.id}>
{todo.text}
</li>
);
// 万不得已才使用
// 列表顺序可能变化了,这将导致错误状态
const todoItems = todos.map((todo, index) =>
<li key={index}>
{todo.text}
</li>
);
key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key 属性的值,请用其他属性名显式传递这个值
8. 表单
受控组件
class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
// evnet.target
handleChange(event) {
this.setState({value: event.target.value});
}
// 阻止跳转
handleSubmit(event) {
alert('提交的名字: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
名字:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
textarea 标签
// 原本 <textarea> 元素通过其子元素定义其文本
// 而 react 中可使用 value 属性
class EssayForm extends React.Component {
constructor(props) {
super(props);
this.state = {
value: '请撰写一篇关于你喜欢的 DOM 元素的文章.'
};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('提交的文章: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
文章:
<textarea value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="提交" />
</form>
);
}
}
select 标签
// 传统的在 option 里面有个 selected 属性
<select>
<option value="grapefruit">葡萄柚</option>
<option value="lime">酸橙</option>
<option selected value="coconut">椰子</option>
<option value="mango">芒果</option>
</select>
// 而 React 在根标签 select 上使用 value 属性
class FlavorForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: 'coconut'};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange(event) {
this.setState({value: event.target.value});
}
handleSubmit(event) {
alert('你喜欢的风味是: ' + this.state.value);
event.preventDefault();
}
render() {
return (
<form onSubmit={this.handleSubmit}>
<label>
选择你喜欢的风味:
<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>
</label>
<input type="submit" value="提交" />
</form>
);
}
}
9. 状态提升
两个组件需要数据同步时,将 state 提升到父组件(此时调用将变成 this.props.attr),因为父组件会将参数 props 传递给子组件。又因为 state 是私有的,且提升后属于父组件,不受子组件控制,此时子组件想要改变父组件的 state 只能依靠 父组件将 setState 方法包装成函数通过 props 传递给子组件调用
class TemperatureInput extends React.Component {
constructor(props) {
super(props);
this.handleChange = this.handleChange.bind(this);
}
handleChange(e) {
this.props.onTemperatureChange(e.target.value);
}
render() {
const temperature = this.props.temperature;
const scale = this.props.scale;
return (
<fieldset>
<legend>Enter temperature in {scaleNames[scale]}:</legend>
<input value={temperature}
onChange={this.handleChange} />
</fieldset>
);
}
}
class Calculator extends React.Component {
constructor(props) {
super(props);
this.handleCelsiusChange = this.handleCelsiusChange.bind(this);
this.handleFahrenheitChange = this.handleFahrenheitChange.bind(this);
this.state = {temperature: '', scale: 'c'};
}
handleCelsiusChange(temperature) {
this.setState({scale: 'c', temperature});
}
handleFahrenheitChange(temperature) {
this.setState({scale: 'f', temperature});
}
render() {
const scale = this.state.scale;
const temperature = this.state.temperature;
const celsius = scale === 'f' ? tryConvert(temperature, toCelsius) : temperature;
const fahrenheit = scale === 'c' ? tryConvert(temperature, toFahrenheit) : temperature;
return (
<div>
<TemperatureInput
scale="c"
temperature={celsius}
onTemperatureChange={this.handleCelsiusChange} />
<TemperatureInput
scale="f"
temperature={fahrenheit}
onTemperatureChange={this.handleFahrenheitChange} />
<BoilingVerdict
celsius={parseFloat(celsius)} />
</div>
);
}
}
10. 组合关系
包含关系
// 展示的是
// Welcome
// Thank you for visiting our spacecraft!
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>
);
}
porps.children 也是一个保留字段,里面有该标签中的所有内容(包括属性、子元素、文本)
也可不使用 children 属性
function SplitPane(props) {
return (
<div className="SplitPane">
<div className="SplitPane-left">
{props.left}
</div>
<div className="SplitPane-right">
{props.right}
</div>
</div>
);
}
function App() {
return (
<SplitPane
left={
<Contacts />
}
right={
<Chat />
} />
);
}
传递一个元素进去也是可以的
特别关系
// 一些组件看作是其他组件的特殊实例,比如 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!" />
);
}