官网 :https://zh-hans.reactjs.org
react 是一个js核心库,如同jQuery 一样,具有大量react生态(围绕‘react’核心开发的库)
1.特点
1-1 声明式
(js中的数据决定页面最终渲染结果)
1-2 组件化
(包含外观和行为,独立可运行的模块,称为组件)
1-3 一次学习,跨平台编写
(`react` 可以开发 桌面web页面,移动端页面,移动app,桌面app)
2.环境的搭建
## 使用cdn在html引入react
react可以在现用的html页面中直接使用,通过cdn引入以下几个库:
```html
<!-- react 需要引入 react 核心库 和 react-dom 库 -->
<script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<!-- 为了更方便书写 react 通常会使用 jsx 语法,为了支持该语法 需要引入 babel -->
<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>
```
然后可以在 body 中加入如下代码:
```html
<body>
<div id="root"></div>
<!-- 采用 jsx 语法 需要在 type 处加上 text/babel -->
<script type="text/babel">
ReactDOM.render(
<h1>Hello World</h1>,
document.getElementById('root')
);
</script>
</body>
```
这里需要介绍几个概念:
- react.js 这是框架核心库,用于解析 react 语法
- react-dom.js 这个库是用于 react 元素(也就是react的文档对象模型)的创建
- babel.js 这个库用来将 jsx 语法翻译为普通的 js
> 注意:此处为了学习 react 的语法引入了 jsx,在真实项目中不会在页面上直接使用 babel
## 使用create-react-app创建react应用
这种方法需要安装 node.js
首先安装 create-react-app 命令行工具
官网:<https://zh-hans.reactjs.org/docs/create-a-new-react-app.html#create-react-app>
github: <https://github.com/facebook/create-react-app>
create-react-app文档:<https://create-react-app.dev/>
执行以下命令安装 create-react-app 工具
```shell
npm install -g create-react-app
```
然后在任一目录下执行以下命令创建react应用:
```shell
npm init react-app <project-name>
# 或
create-react-app <project-name>
# 其中 <project-name> 是应用的名称
```
进入到项目目录下并运行 `npm run start` 启动项目
## 创建一个使用typescript书写的react项目
<https://create-react-app.dev/docs/adding-typescript>
3.jsx语法
<body>
<div id="app"></div>
</body>
<script type="text/babel">
const root = ReactDOM.createRoot(document.querySelector('#app'));
// 使用jsx创建一个react-dom对象
// react-dom 是 react 封装的类似 dom 对象的对象
// 作用: 用于描述页面元素
const h1 = <h1>hello world</h1>
// jsx 使用 圆括号 声明多行的 react-dom 对象
const box = (
<div className="box">
<span>hello box</span>
</div>
)
const element = (
<div>
root1
</div>
// react-dom 中只能有一个根节点
// <div>
// root2
// </div>
)
let now = new Date()
// 插值
const chazhi = (
<div>
{ /*使用花括号进行插值 插值的内容可以是js表达式*/}
{'当前时间: ' + now.toLocaleString()}
</div>
)
let id = 'thisIsMyId'
// 定义样式对象
let myStyle = {
width: '100px',
height: '100px',
backgroundColor: '#f00'
}
// 插值 html 属性
const htmlAttr = (
<div>
{/* 插值id */}
<div id={id}>
hello attr
</div>
{/* 插值 style */}
{/* style 属性无法直接赋值一个字符串如下: */}
{/*<div style="width: 100px; height: 100px; background-color: #f00"></div>*/}
<div style={myStyle}></div>
{/* 插值 class */}
<div className="box active"></div>
</div>
)
// 使用函数创建react-dom对象
const fnDom = React.createElement(
// 标签名
'div',
// 标签上的属性
{id: 'myFunctionDom', className: 'my-dom', style: {width: '500px', height: '50px', backgroundColor: '#00f'}},
// 子元素数组 或 标签体字符串
[
// 此处的第三个参数就是一个普通的字符串充当 span 标签的标签体
React.createElement('span', {key: '1'}, 'hello world'),
// 若此处 createElement 第三个参数是一个数组的话,需要给元素添加 key 属性
// key 属性是一个唯一值 不重复即可
React.createElement('span', {key: '2'}, 'hello world')
]
)
// root.render 函数可以渲染一个 react-dom 对象
root.render((
<div className="container">
{/* 插值的内容 若是 react-dom 对象 那么就会被页面显示出来 */}
{h1}
{box}
{element}
{chazhi}
{htmlAttr}
{fnDom}
</div>
))
</script>
4 条件渲染
<script type="text/babel">
let sex = 'male'
ReactDOM.createRoot(document.querySelector('#root')).render((
<div>
{/* 使用 && 进行短路运算 前一个表达式为true时 就显示后面表达式的内容 */}
{sex === 'male' && <div style={{color: '#00f'}}>男</div>}
{sex === 'female' && <div style={{color: 'pink'}}>女</div>}
{sex === 'other' && <div style={{color: '#ff0'}}>其他</div>}
{/* 使用三元运算符 按条件显示不同的内容 */}
{sex === 'male' ? <div style={{color: '#00f'}}>男</div> :
sex === 'female' ? <div style={{color: 'pink'}}>女</div> :
<div style={{color: '#ff0'}}>其他</div>
}
</div>
))
</script>
5.循环渲染
<script type="text/babel">
let students = [
{
name: '张三',
sex: 'male',
age: 17
},
{
name: '李四',
sex: 'female',
age: 24
},
{
name: '隔壁老王',
sex: 'other',
age: 30
},
]
ReactDOM.createRoot(document.querySelector('#root')).render((
<div>
<ul>
{/* 循环渲染,使用一个数组的map函数 返回一个由 react-dom 充当成员形成的一个新数组
循环渲染一定要添加 key
*/}
{students.map((item,index) => <li key={index}>姓名: {item.name}; 性别: {item.sex === 'male' ? '男' :
item.sex === 'female' ? '女' : '不详'
}; 年龄: {item.age}</li>)}
</ul>
</div>
))
</script>
6.受控组件
(被 react 的 state 控制显示和输入的表单元素称为受控组件, 受控组件的数据来自 state 而不是表单元素自身,react 中 所有的事件都不能通过 return false 来屏蔽默认事件)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Title</title>
<script src="js/babel.min.js"></script>
<script src="js/react.development.js"></script>
<script src="js/react-dom.development.js"></script>
</head>
<body>
<div id="root"></div>
</body>
<script type="text/babel">
// 知识点
// 什么是受控组件?
// 被 react 的 state 控制显示和输入的表单元素称为受控组件
// 受控组件的数据来自 state 而不是表单元素自身
// react 中 所有的事件都不能通过 return false 来屏蔽默认事件
// 声明 input[type=text] 、select 和 textarea 的受控组件
// 声明 input[type=radio] input[type=checkbox] 的受控组件
function App() {
const [name, setName] = React.useState('')
const [clazz, setClazz] = React.useState('')
const [desc, setDesc] = React.useState('')
const [sex, setSex] = React.useState('other')
const [hobbies, setHobbies] = React.useState(['dlq', 'ymq'])
function submit(ev) {
ev.preventDefault()
console.log(name)
console.log(clazz)
console.log(desc)
console.log(sex)
console.log(hobbies)
}
function onNameInput(ev) {
console.log(ev)
console.log(ev.target.value)
// 由于此处的输入框是受控组件
// 所以若不改变状态, name 值不发生改变,页面就不会更新
setName(ev.target.value)
}
function onClazzChange(ev) {
console.log(ev.target.value)
setClazz(ev.target.value)
}
function onDescInput(ev) {
console.log(ev.target.value)
setDesc(ev.target.value)
}
function onSexChange(ev) {
console.log(ev.target.value)
setSex(ev.target.value)
}
function onHobbiesChange(ev) {
console.log(ev.target.value)
if (hobbies.includes(ev.target.value)) {
// 若已经包含就了删除
setHobbies(_hobbies => {
let i = _hobbies.findIndex(item => item === ev.target.value)
_hobbies.splice(i, 1)
return [..._hobbies]
})
} else {
// 不存在时 就添加进数组
setHobbies(_hobbies => {
_hobbies.push(ev.target.value)
return [..._hobbies]
})
}
}
return (
<div>
<form>
<div>
<label>
姓名:
<input type="text" value={name} onInput={onNameInput}/>
</label>
</div>
<div>
<label>
班级:
<select value={clazz} onChange={onClazzChange}>
<option value="" disabled>请选择</option>
<option value="1">一班</option>
<option value="2">二班</option>
<option value="3">三班</option>
</select>
</label>
</div>
<div>
<label>
简介:
<textarea rows="4" value={desc} onInput={onDescInput}></textarea>
</label>
</div>
<div>
<label>
性别:
<label><input onChange={onSexChange} checked={sex === 'male'} type="radio" value="male"/>男</label>
<label><input onChange={onSexChange} checked={sex === 'female'} type="radio"
value="female"/>女</label>
<label><input onChange={onSexChange} checked={sex === 'other'} type="radio" value="other"/>其他</label>
</label>
</div>
<div>
<label>
爱好:
<label><input onChange={onHobbiesChange} checked={hobbies.includes('dlq')} type="checkbox"
value="dlq"/>打篮球</label>
<label><input onChange={onHobbiesChange} checked={hobbies.includes('tzq')} type="checkbox"
value="tzq"/>踢足球</label>
<label><input onChange={onHobbiesChange} checked={hobbies.includes('ymq')} type="checkbox"
value="ymq"/>羽毛球</label>
</label>
</div>
<div>
<button onClick={submit}>提交</button>
</div>
</form>
</div>
)
}
ReactDOM.createRoot(document.querySelector('#root')).render(<App/>)
</script>
</html>
7.状态提升
组件向上级组件汇报自己状态的过程叫做状态提升
<script type="text/babel">
// 文档:https://zh-hans.reactjs.org/docs/lifting-state-up.html
// 知识点
// 什么是状态提升?
// 组件向上级组件汇报自己状态的过程叫做状态提升
// 应用场景
// 1. 在父组件中需要读取子组件状态时,可以让子组件状态提升给父组件
// 2. 子组件需要发出某个事件,且事件将携带参数时
// 如何实现?
// 理论原理:
// 组件可以利用自身的 props 属性将自身状态提升到上级组件
// 表现形式类似于触发事件
// 上级组件只要绑定事件(本质是提供一个回调函数)接收参数即可
// 操作方法:
// 1. 父组件中,给子组件添加 props 属性,并分配一个函数值,该函数接收子组件的状态值为参数
// 2. 子组件中,在适当时机调用父组件提供的 props,并将自身状态作为参数传入父组件提供的函数
function Child(props) {
const [count, setCount] = React.useState(0);
function increase() {
setCount(_c => _c + 1)
}
// 监视 count 的变化
React.useEffect(() => {
// count 发生变化
// 通知父组件
// 将状态值作为参数传入
props.countChange(count)
}, [count])
return (
<div>
<div>count: {count}</div>
<div>
<button onClick={increase}>+</button>
</div>
</div>
)
}
function App() {
// 此函数的参数用于接收子组件状态
function onCountChange(args) {
console.log(args)
}
return (
<div>
{/* 给子组件分配一个函数 用于接收组件状态 */}
<Child countChange={onCountChange}></Child>
</div>
)
}
ReactDOM.createRoot(document.querySelector('#root')).render(<App/>)
</script>
8.元素分组 React.Fragment
React.Fragment 内置组件起到一个给元素分组的作用,最终不会显示到页面上 作用类似小程序中的 <block> 标签
9.组件通信
什么是组件通信: 父组件和子组件相互传递参数的过程,就是组件间的通信
应用场景
// 子组件的状态依赖于父组件传入的 props
// 由于 props 是只读的
// 所以要修改 props 子组件只能通知父组件更新数据
// 这个过程就会用到组件间的通信方法
通信方法:
// 1. 父组件将自身 state 作为参数传入子组件 props
// 2. 子组件依赖 props 显示内容
// 3. 子组件内希望修改 props 中的值,则发出一个事件
// 4. 父组件绑定子组件发出的事件并接收参数
// 5. 父组件接收事件后更新自身 state,此时 react 会自动更新子组件 props
10.错误边界
什么是错误边界
// react 的错误边界类似 js 的 catch
// 错误边界是一个 react 组件,其内部的子组件一旦发生异常就会触发错误边界
应用场景
// 需要对页面异常做出更友好的提示的时候,可以使用错误边界
如何使用?
// 1. 错误边界需要是一个 class 组件(函数组件不能充当错误边界)
// 2. 错误边界需要声明一个 hasError 状态
// 3. 错误边界需要实现生命周期钩子函数: static getDerivedStateFromError(error) 该函数返回要修改的状态,通常是{hasError: true} ; componentDidCatch(error, errorInfo) [该函数非必要]
// 4. 错误边界的渲染函数的内容,根据 hasError 状态进行调整
// 5. 使用错误边界的时候,在错误边界的标签体中嵌入其他子组件
// 6. 子组件生命周期任何阶段发生异常,都会被错误边界捕获到,从而显示错误的提示
// 使用 类组件声明错误边界
class ErrorBoundary extends React.Component {
state = {
// hasError 状态值用于指示 错误边界 内的子组件是否有错误
hasError: false
}
// 当组件发生异常时,在调用 render 函数前先会调用此函数
// error 当前正被触发的异常对象
static getDerivedStateFromError(error) {
console.log('getDerivedStateFromError')
console.log(error)
// 此处在渲染之前修改 hasError 状态为 true 代表产生了异常
return {hasError: true}
}
// 此为非必填函数
// 该生命周期将在捕获异常完成并渲染完成后触发
componentDidCatch(error, errorInfo) {
console.log('componentDidCatch')
console.log(error)
console.log(errorInfo)
}
render() {
// render 函数通过 hasError 判断应该显示什么内容
// 没有异常就显示 ErrorBoundary 的标签体里的内容 否则显示一个友好的错误提示
return this.state.hasError ? <div>错误时候看到的内容</div> :
<div>{this.props.children}</div>
}
}
11,context 上下文
什么是 context?
// context 是一对 组件,用于向后代组件提供参数
// 注意: context 的数据是只读的
// 应用场景
// 在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的
// 但此种用法对于某些类型的属性而言是极其繁琐的
// (例如:地区偏好,UI 主题,因为他们是全局属性,每个和ui相关组件都应该读取其值)
// 所以:很多不同层级的组件需要访问一些同样的数据,可以考虑使用 context
// 概念
// context 提供了一对组件 如下:
// Provider(供应商) 提供数据的组件
// Consumer(消费者) 消费数据的组件
// 使用方法
1. 创建 context
// 语法:const MyContext = React.createContext(defaultValue);
// defaultValue 若组件找不到对应 context 时会获取默认值
// context 的值可以是一个对象
2. 页面中将 MyContext.Provider 作为父节点使用,并传入 value 参数作为 Provider 提供的数据
3. 子组件接收 Provider 提供的数据
// 3.1 类组件接收方法:
// 1. 给类名的 contextType 赋值 context 对象,例如:
// MyClassComponent.contextType = MyContext
// 2. 在组件内使用 this.context 访问 Provider 提供的数据
// 3.2 函数组件接收方法:
// 1. 在页面中,给要使用数据的函数组件套上一个 Consumer 父组件
// 2. 在 Consumer 内 通过工厂函数返回要接收 Provider 数据的函数组件,工厂函数接收一个 value 参数,该参数即为 Provider 提供的数据
// 3. 函数组件通过 props 接收 value 参数
// 例如:
// <MyContext.Consumer>
// {value => <FunctionComponent theme={value}></FunctionComponent>}
// </MyContext.Consumer>
// Provider 提供一个动态可变的值
绑定多个 Context
// 官网:https://zh-hans.reactjs.org/docs/context.html#consuming-multiple-contexts
// 重点在于利用多重嵌套的 Context.Consumer,来实现绑定多个 Context
12.转发(ref)
类组件使用 React.createRef()
函数组件使用 React.useRef()
通用方法 使用箭头函数 el => { temp = el }
13.传送门
什么是传送门
// 传送门是 react 通过 ReactDOM.createPortal 方法创建的一个特殊的 react-dom
// 传送门的内容可以显示到 html 文档的任何位置 甚至是 react 根节点外面
应用场景
// 制作模态等脱离自身组件结构的内容
// 注意:传送门的使用必须配合无状态的函数组件,class组件是无法使用的
使用方法:
// 1. 创建一个函数组件充当传送门组件(该函数组件不能有状态)
// 2. 函数组件内使用 ReactDOM.createProtal 方法创建传送门
// ReactDOM.createPortal 函数将返回一个可以被渲染到页面的一组html元素
// 第一个参数:要传送的 react-dom
// 第二个参数:传送门的目标节点,最后元素将渲染到该节点
// 传送门组件
// 传送门组件必须是函数组件 且不能包含状态