React 18.2.0 已经发布很久了,这几天抽空重新完整的过了一遍 React
的官网,并结合当时学习React 16.8
的总结,将笔记和案例记录下来,方便以后查阅
和迭代
。
总的来说react的语法不算太多,打算按照 React 官网
文档的教程顺序,从 安装 -> 核心概念 -> 高级指引 -> hook
开始。
本文档会结合一些案例(基于react18)
,方便大家阅读和实践,以备 开箱即用
。
Hello World
最简易的 React 示例如下:
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<h1>Hello, world!</h1>);
它将在页面上展示一个 “Hello, world!” 的标题。
JSX 简介
JSX
- 什么是JSX?
-
Facebook起草的JS扩展语法
-
本质是一个
JS对象
,会被babel编译,最终会被转换为React.createElement
-
每个JSX表达式,有且仅有
一个根节点
-
每个JSX元素必须结束(XML规范)
- 当根节点为空时
真实语法:React.Fragment
// const d1 = (<>
// <h1>Hello</h1>
// <p>world</p>
// </>)
const d1 = (<React.Fragment>
<h1>Hello</h1>
<p>world</p>
</React.Fragment>)
JSX中嵌入表达式
{}
使用表达式
import React from 'react';
import ReactDOM from 'react-dom';
const a = 100
const b = 9
const d1 = (<div>
{a} * {b} = {a * b}
</div>)
// const d1 = React.createElement('div', {}, `${a} * ${b} = ${a * b}`)
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(d1);
react-note/1.JSX 中嵌入表达式.html at master · PantherVkin/react-note (github.com)
- 在JSX中使用
注释
- 将表达式作为内容的一部分
null、undefined、false
不会显示。
普通对象,不可以作为子元素
。
可以放置React元素对象
。
- 可以放置数组
import React from 'react';
import ReactDOM from 'react-dom';
const obj = <p>React Obj</p>
const arr = new Array(10)
arr.fill(obj)
const d1 = (<div>
{arr}
</div>)
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(d1);
react-note/2.表达式放置数组.html at master · PantherVkin/react-note (github.com)
- 将表达式作为
元素属性
,属性使用小驼峰命名法
你可以通过使用引号,来将属性值指定为字符串字面量
:
const element = <a href="https://www.reactjs.org"> link </a>;
也可以使用大括号,来在属性值中插入一个 JavaScript 表达式
:
const element = <img src={user.avatarUrl} className={c1}></img>;
防止注入攻击
- 自动编码
React自我保护机制、底层Innertext
赋值,保证内容为纯为本。
import React from 'react';
import ReactDOM from 'react-dom';
const content = "<h1>Hello<p>world</p></h1>"
const d1 = <div>
{content}
</div>
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(d1);
react-note/3.放置注入攻击.html at master · PantherVkin/react-note (github.com)
- 想作为元素结构时,配置
dangerouslySetInnerHTML
import React from 'react';
import ReactDOM from 'react-dom';
const content = "<h1>Hello<p>world</p></h1>"
const d1 = <div dangerouslySetInnerHTML={{
__html: content
}}>
</div>
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(d1);
react-note/4.dangerouslySetInnerHTML.html at master · PantherVkin/react-note (github.com)
元素的不可变性
- 虽然JSX元素是一个对象,但是该对象中的所有属性不可更改
底层使用 Object.freeze()
冻结一个对象。
- 如果确实需要
更改
元素的属性,需要重新创建JSX元素
JSX 表示对象
JSX -> React.createElement()
Babel 会把 JSX
转译成一个名为 React.createElement()
函数调用。
以下两种示例代码完全等效:
const element = (
<h1 className="greeting">
Hello, world!
</h1>
);
const element = React.createElement(
'h1',
{className: 'greeting'},
'Hello, world!'
);
- React 元素(虚拟DOM)
React.createElement()
会预先执行一些检查,以帮助你编写无错代码,但实际上它创建了一个这样的对象:
// 注意:这是简化过的结构
const element = {
type: 'h1',
props: {
className: 'greeting',
children: 'Hello, world!'
}
};
这些对象被称为 React 元素
。它们描述了你希望在屏幕上看到的内容。React 通过读取这些对象,然后使用它们来构建 DOM 以及保持随时更新。
元素渲染
组件 & Props
- 组件
包含内容、样式和功能的UI单元
。
组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即 “props
”),并返回用于描述页面展示内容的 React 元素
。
- 组件的名称首字母必须大写。
使用组件,生成的,仍然是一个React元素,变化的,只是type值;React 靠首字母
区分普通
的react元素和组件元素
。
-
小写:普通的react元素(
内置组件
); -
大写:
组件元素
;
函数组件
返回一个React元素。
- 当做函数使用
import React from 'react';
import ReactDOM from 'react-dom';
function MyFuncComp() {
return <h1>组件内容</h1>
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(<div>
{MyFuncComp()}
</div>);
- 缺点:没有构建出组件结构。
react开发者工具
查看组件结构。
- 当做React元素使用
import React from 'react';
import ReactDOM from 'react-dom';
function MyFuncComp() {
return <h1>组件内容</h1>
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(<div>
<MyFuncComp/>
</div>);
react开发者工具
查看组件结构。
- 完整案例
react-note/1.函数组件.html at master · PantherVkin/react-note (github.com)
class 组件
必须继承React.Component
必须提供render
函数,用于渲染组件,返回React元素;
import React from 'react';
import ReactDOM from 'react-dom';
class MyClassComp extends React.Component {
render() {
// 该方法必须返回React元素
return <h1>类组件内容</h1>
}
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(<div>
<MyClassComp/>
</div>);
Props 的只读性
给组件传递的数据。
- 函数组件
对于函数组件,属性会作为一个对象的属性,传递给函数的参数
import React from 'react';
import ReactDOM from 'react-dom';
function MyFuncComp(props) {
return <h1>组件属性:{props.name}</h1>
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(<MyFuncComp name="Panther"/>);
- 类组件
对于类组件,属性会作为一个对象的属性,传递给构造函数的参数
import React from 'react';
import ReactDOM from 'react-dom';
class MyClassComp extends React.Component {
// 自动完成
// constructor(props) {
// super(props); // this.props = props;
// console.log(props, this.props, props === this.props);
// }
render() {
// 该方法必须返回React元素
return <h1>类组件属性:{this.props.name}</h1>
}
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(<MyClassComp name="Panther"/>);
- 传递一个对象时
import React from 'react';
import ReactDOM from 'react-dom';
function MyFuncComp(props) {
return <h1>
姓名:{props.name}
地址:{props.address}
</h1>
}
const obj = {
name: 'Panther',
address: 'SZ'
}
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 把react元素(虚拟dom) 变成真实的dom对象。
root.render(<div>
<MyFuncComp {...obj}></MyFuncComp>
{/* 相当于 name={obj.name} address={obj.address}*/}
</div>);
react-note/2.函数组件Props.html at master · PantherVkin/react-note (github.com)
- 注意
组件无法改变自身的属性。
之前学习的React元素,本质上,就是一个组件(内置组件) 首字母小写。
React中的哲学:数据属于谁,谁才有权力改动。
React中的数据,自顶而下流动。
State & 生命周期
State
- 组件状态
组件可以自行维护的数据;
组件状态仅在类组件中有效;
状态(state
),本质上是类组件的一个属性,是一个对象。
- 状态初始化
import React, { Component } from 'react'
export default class Tick extends Component {
//初始化状态,JS Next 语法,目前处于实验阶段
state = {
left: this.props.number,
n: 123
}
constructor(props) {
super(props);
//初始化状态
// this.state = {
// left: this.props.number
// };
}
render() {
return (
<h1>
倒计时剩余时间:{this.state.left}
</h1>
)
}
}
- 状态的变化
不能直接改变状态:因为React无法监控到状态发生了变化;
必须使用this.setState({})
改变状态(同名属性的值会被替换);
一旦调用了this.setState
,会导致当前组件重新渲染;
组件中的数据
props
该数据是由组件的使用者传递的数据,所有权不属于组件自身,因此组件无法改变该数组。
state
该数据是由类组件自身
创建的,所有权属于类组件自身,因此类组件有权改变该数据。
深入setState
不要直接修改 State
// Wrong
this.state.comment = 'Hello';
而是应该使用 setState()
:
// Correct
this.setState({comment: 'Hello'});
构造函数是唯一可以给 this.state
赋值的地方。
setState可能是异步的
- 改变状态的代码处于某个HTML元素的事件中,则其是异步的,否则是同步
import React from 'react'
export class CompSetstate extends React.Component {
state = {
n: 0
}
handleClick = () => {
this.setState({
n: this.state.n + 1
})
// 改变状态的代码处于HTML事件中,是异步的;
// n 打印在render 之前,说明此时状态任然没有改变
console.log(this.state.n)
}
render() {
console.log('render')
return <div>
<h1>
{this.state.n}
</h1>
<button onClick={this.handleClick}>
点击+1
</button>
</div>
}
}
react-note/1.setState 是异步的.html at master · PantherVkin/react-note (github.com)
- 第二个参数
状态完成改变之后触发
,该回调运行在render之后。
import React from 'react'
export class CompSetstate extends React.Component {
state = {
n: 0
}
handleClick = () => {
this.setState({
n: this.state.n + 1
}, () => {
// 状态完成改变之后触发,该回调运行在render之后
console.log(this.state.n)
})
}
render() {
console.log('render')
return <div>
<h1>
{this.state.n}
</h1>
<button onClick={this.handleClick}>
点击+1
</button>
</div>
}
}
react-note/2.第二个参数.html at master · PantherVkin/react-note (github.com)
- 第一个参数是函数的方式
如果遇到某个事件中,需要同步调用多次用到之前的状态
,第一个参数使用函数的方式得到最新状态。
import React from 'react'
export class CompSetstate extends React.Component {
state = {
n: 0
}
handleClick = () => {
// 第一个参数是函数的情况
// cur 表示当前的状态,会混合(覆盖)掉之前的状态
// 该函数是异步执行
this.setState((cur) => {
console.log('cur', cur);
return {
n: cur.n + 1
};
});
this.setState((cur) => {
console.log('cur', cur);
return {
n: cur.n + 1
};
});
this.setState((cur) => {
console.log('cur', cur);
return {
n: cur.n + 1
};
});
}
render() {
console.log('render')
return <div>
<h1>
{this.state.n}
</h1>
<button onClick={this.handleClick}>
点击+1
</button>
</div>
}
}
react-note/3.第一个参数是函数的方式.html at master · PantherVkin/react-note (github.com)
最佳实践
-
把所有的setState当作是异步的
-
永远不要信任setState调用之后的状态
-
如果要使用改变之后的状态,需要使用回调函数(setState的第二个参数)
-
如果新的状态要根据之前的状态进行运算,使用函数的方式改变状态(setState的第一个函数)
-
React会对异步的setState进行优化
,将多次setState进行合并(将多次状态改变完成后,再统一对state进行改变,然后触发render)。
import React from 'react'
export class CompSetstate extends React.Component {
state = {
n: 0
}
handleClick = () => {
// 第一个参数是函数的情况
// cur 表示当前的状态,会混合(覆盖)掉之前的状态
// 该函数是异步执行
this.setState(cur => ({
n: cur.n + 1
}), ()=>{
//所有状态全部更新完成,并且重新渲染后执行
console.log("state更新完成", this.state.n);
});
this.setState(cur => ({
n: cur.n + 1
}), ()=>{
//所有状态全部更新完成,并且重新渲染后执行
console.log("state更新完成", this.state.n);
});
this.setState(cur => ({
n: cur.n + 1
}), ()=>{
//所有状态全部更新完成,并且重新渲染后执行
console.log("state更新完成", this.state.n);
});
}
render() {
console.log('render')
return <div>
<h1>
{this.state.n}
</h1>
<button onClick={this.handleClick}>
点击+1
</button>
</div>
}
}
react-note/4.setState 合并.html at master · PantherVkin/react-note (github.com)
生命周期
组件从诞生到销毁会经历一系列的过程,该过程就叫做生命周期。
React在组件的生命周期中提供了一系列的钩子函数
(类似于事件),可以让开发者在函数中注入代码,这些代码会在适当的时候运行。
生命周期仅存在于类组件中,函数组件每次调用都是重新运行函数,旧的组件即刻被销毁。
- constructor
-
同一个组件对象只会创建一次
-
不能在第一次挂载到页面之前,调用setState,为了避免问题,构造函数中严禁使用setState
- render
-
返回一个虚拟DOM,会被挂载到虚拟DOM树中,最终渲染到页面的真实DOM中
-
render可能不只运行一次,只要需要重新渲染,就会重新运行
-
严禁使用setState,因为可能会导致无限递归渲染
- componentDidMount
-
只会执行一次
-
可以使用setState
-
通常情况下,会将
网络请求、启动计时器
等一开始需要的操作,书写到该函数中
组件进入活跃状态
- shouldComponentUpdate
-
指示React是否要重新渲染该组件,通过返回true和false来指定
-
默认情况下,会直接返回true
- componentDidUpdate
- 往往在该函数中使用dom操作,改变元素
- componentWillUnmount
- 通常在该函数中销毁一些组件依赖的资源,比如计时器
- getDerivedStateFromProps
-
两个参数,通过参数可以获取新的属性和状态
-
该函数是静态的,不能获取this
-
该函数的返回值会覆盖掉组件状态
-
该函数几乎是没有什么用
- getSnapshotBeforeUpdate
-
真实的DOM构建完成,但还未实际渲染到页面中。
-
在该函数中,通常用于实现一些附加的dom操作(直接操作Dom)
-
该函数的返回值,会作为componentDidUpdate的第三个参数
事件处理
在React中,组件的事件,本质上就是一个属性。
按照之前React对组件的约定,由于事件本质上是一个属性,因此也需要使用小驼峰命名法。
内置组件的事件
会在合适的时候,自动调用事件 。
import React from 'react'
import ReactDOM from 'react-dom'
const btn = <button onClick={(e) => {
console.log('点击了', e)
}}>点击</button>
ReactDOM.render(btn, document.getElementById('root'))
自定义组件的事件
不会自动调用事件。
如果没有特殊处理,在事件处理函数中,this指向undefined ?
export class HelloWorldCls extends React.Component {
constructor(props) {
super(props);
// this.handleClick = this.handleClick.bind(this);
}
render() {
return <h1 onClick={this.handleClick}> HelloWorldCls </h1>;
}
handleClick() {
console.log('点击了', this);
}
}
- 使用bind函数,绑定this
export class HelloWorldCls extends React.Component {
constructor(props) {
super(props)
this.handleClick = this.handleClick.bind(this)
}
render() {
return <h1 onClick={this.handleClick}>
HelloWorldCls
</h1>
}
handleClick() {
console.log('点击了', this)
}
}
export class HelloWorldCls extends React.Component {
render() {
return <h1 onClick={this.handleClick.bind(this)}>
HelloWorldCls
</h1>
}
handleClick() {
console.log('点击了', this)
}
}
- 使用箭头函数
export class HelloWorldCls extends React.Component {
render() {
return <h1 onClick={this.handleClick}>
HelloWorldCls
</h1>
}
handleClick = ()=> {
console.log('点击了', this)
}
}
向事件处理程序传递参数
在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id
是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:
<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>
上述两种方式是等价的,分别通过箭头函数
和 Function.prototype.bind
来实现。
在这两种情况下,React 的事件对象 e
会被作为第二个参数传递。
如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind
的方式,事件对象以及更多的参数将会被隐式的进行传递。
传递元素内容
内置组件
内置组件:div、h1、p
<div>
asdfafasfafasdfasdf
</div>
自定义组件
- 通过属性
- children
如果给自定义组件传递元素内容,则React会将元素内容作为children属性传递过去。
- 传多个元素内容
列表 & Key
key
- key 帮助 React 识别哪些元素改变了,比如被添加或删除。因此你应当给数组中的每一个元素赋予一个确定的标识。
const todoItems = todos.map((todo) =>
<li key={todo.id}> {todo.text}
</li>
);
- 当元素没有确定 id 的时候,万不得已你可以使用元素索引 index 作为 key:
如果列表项目的顺序可能会变化
,我们不建议使用索引来用作 key
值,因为这样做会导致性能变差,还可能引起组件状态的问题。可以看看 Robin Pokorny 的深度解析使用索引作为 key 的负面影响这一篇文章。
如果你选择不指定显式的 key 值,那么 React 将默认使用索引用作为列表项目的 key 值。
要是你有兴趣了解更多的话,这里有一篇文章深入解析为什么 key 是必须的可以参考。
用 key 提取组件
元素的 key 只有放在就近的数组上下文中才有意义。
比方说,如果你提取出一个 ListItem
组件,你应该把 key 保留在数组中的这个 <ListItem />
元素上,而不是放在 ListItem
组件中的 <li>
元素上。
- 例子:不正确的使用 key 的方式
function ListItem(props) {
const value = props.value;
return (
// 错误!你不需要在这里指定 key: <li key={value.toString()}> {value}
</li>
);
}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 错误!元素的 key 应该在这里指定: <ListItem value={number} /> );
return (
<ul>
{listItems}
</ul>
);
}
- 例子:正确的使用 key 的方式
function ListItem(props) {
// 正确!这里不需要指定 key: return <li>{props.value}</li>;}
function NumberList(props) {
const numbers = props.numbers;
const listItems = numbers.map((number) =>
// 正确!key 应该在数组的上下文中被指定 <ListItem key={number.toString()} value={number} /> );
return (
<ul>
{listItems}
</ul>
);
}
一个好的经验法则是:在 map()
方法中的元素需要设置 key 属性。
key 值在兄弟节点之间必须唯一
数组元素中使用的 key 在其兄弟节点之间应该是独一无二
的。
然而,它们不需要是全局唯一的。当我们生成两个不同的数组时,我们可以使用相同的 key 值。
function Blog(props) {
const sidebar = ( <ul>
{props.posts.map((post) =>
<li key={post.id}> {post.title}
</li>
)}
</ul>
);
const content = props.posts.map((post) => <div key={post.id}> <h3>{post.title}</h3>
<p>{post.content}</p>
</div>
);
return (
<div>
{sidebar} <hr />
{content} </div>
);
}
const posts = [
{id: 1, title: 'Hello World', content: 'Welcome to learning React!'},
{id: 2, title: 'Installation', content: 'You can install React from npm.'}
];
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Blog posts={posts} />);
key 会传递信息给 React ,但不会传递给你的组件。如果你的组件中需要使用 key
属性的值,请用其他属性名显式传递
这个值:
const content = posts.map((post) =>
<Post
key={post.id} id={post.id} title={post.title} />
);
上面例子中,Post
组件可以读出 props.id
,但是不能读出 props.key
。
表单
受控组件
组件的使用者,有能力完全控制该组件的行为和内容。通常情况下,受控组件往往没有自身的状态
,其内容完全受到属性
的控制。
非受控组件
组件的使用者,没有能力控制该组件的行为和内容,组件的行为和内容完全自行控制
。
React 的表单元素
表单组件,默认情况下是非受控组件,一旦设置了表单组件的value属性,则其变为受控组件(单选和多选框需要设置checked)。
- value受控组件
import React from 'react'
export class CompForm extends React.Component {
constructor(props) {
super(props)
}
state = {
value: "请输入。。。"
}
handleChange = (e) => {
this.setState({
value: e.target.value
}, () => {
console.log(this.state.value)
})
}
render() {
return <div>
{/* 默认是一个非受控组件 */}
{/* <input type="text" /> */}
<input type="text" value={this.state.value} onChange={this.handleChange}/>
</div>
}
}
- checked 受控组件
<input type="checkbox" checked={this.state.checked} onChange={ e=> {
this.setState({
checked: e.target.checked
})
}}/>