发布于 

React-Notes

第一次学习(失败):挖坑系列

React简介

React是什么?

一个用于构建用户界面的JavaScript库。
强调将数据渲染为HTML视图

React由谁开发的?

Meta(Facebook)

原生JS的问题

  1. DOM-API操作UI效率低
  2. JS直接操作DOM会导致浏览器进行大量重绘重排操作
  3. 原生JS代码复用率低

React的特点

  1. 声明式编码,组件化模式
  • 命令式编码: 你先坐电梯下楼左转,走到自动贩卖机前,买一杯水,按原路返回给我
  • 声明式编程: 我渴了
  1. React Native支持移动端开发
  2. 虚拟DOM+Diffing算法,减少与DOM的交互,性能好

React入门

Hello React

需要引入依赖:

  • babel.js
    • ES6 → ES5
    • jsx → js
  • react.js
    • react核心库
  • react-dom.js
    • react扩展库(dom操作)
  • prop-types.js
hello_react.html
1
<div id="test"></div>

依赖的引入需要按照一定顺序:

1
2
3
4
5
6
<!--引入react核心库 -->
<script src="../React-js/react.development.js"></script>
<!-- 引入react操作dom的扩展库 -->
<script src="../React-js/react-dom.development.js"></script>
<!-- 引入babel -->
<script src="../React-js/babel.min.js"></script>

script标签的src一定要是text/babel

1
2
3
4
5
6
7
8
9
<script type="text/babel">
// src一定要为text/babel

// 1. 创建虚拟DOM
const VDOM = <h1>Hello React</h1>
// 2. 渲染虚拟DOM到页面
// ReactDOM.render(虚拟DOM,容器)
ReactDOM.render(VDOM,document.getElementById('test'))
</script>

JSX

jsx与js的对比

jsx能够简化js的dom操作
但简化操作只针对编码者而言,实际由babel编译后的jsx依旧是按部就班的在操作dom

jsx

1
2
3
4
5
6
7
8
  <script type="text/babel">
let VDOM = (
<h1>
<span>Hello World</span>
</h1>
)
ReactDOM.render(VDOM,document.getElementById('test'))
</script>

js

1
2
3
4
5
6
7
8
  <script>
const VDOM = React.createElement(
'h1', // 标签名
{id:'title'}, // 标签属性
React.createElement('span',{},'Hello World') //标签内容
)
ReactDOM.render(VDOM,document.getElementById('test'))
</script>
虚拟DOM与真实DOM对比

真实DOM

1
2
3
4
5
<div id="test">
<h1>
<span>Hello World</span>
</h1>
</div>

虚拟DOM

  • Object对象
  • 挂载的属性数量较少
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"type": "h1",
"key": null,
"ref": null,
"props": {
"children": {
"type": "span",
"key": null,
"ref": null,
"props": {
"children": "Hello World"
},
"_owner": null,
"_store": {}
}
},
"_owner": null,
"_store": {}
}
JSX语法规则
  1. 标签中混入js表达式时使用 花括号{} 包裹
1
2
let name = 'John'
let VDOM = <h1>{name}</h1>

  1. 类名指定使用className

之所以避开class,是由于class与ES6类定义的关键字有冲突

1
let VDOM = <h1 className='title'>Hello</h1>

  1. 内联样式需要注意以下要点:
  • style = {{样式内容需要使用双括号包裹}}
  • 样式的赋值采用键值对格式
  • 样式值采用字符串格式
  • 样式名采用驼峰标识
1
2
3
4
5
6
7
8
9
let VDOM = (
<h1 style={{
color:'#fff',
border:"1px solid #000",
fontSize:"14px"
}}>
Hello World
</h1>
)
jsx内的遍历案例
1
2
3
4
5
6
7
8
9
const data = ['AAA', 'BBB', 'CCC']
const VDOM = (
<div>
<ul>
{data.map(item => (<li key={item}>{item}</li>))}
</ul>
</div>
)
ReactDOM.render(VDOM, document.getElementById('test'))

React组件

组件定义

React提供两种定义组件的方式:

  • 函数式定义组件
  • 类式定义组件

组件使用函数形式定义,函数名首字母一定要大写

1
2
3
4
function MyComponent(){
return <h1>My Component</h1>
}
ReactDOM.render(<MyComponent/>,document.getElementById('root'))

定义组件的函数内部,this指向哪里?

答: this指向undefined。
这是因为代码经过babel翻译之后,会进行严格模式规范:

1
"use strict"

严格模式禁止自定义函数的this指向window。

组件类一定要继承React.Component

1
2
3
4
5
6
7
8
class MyComponent extends React.Component{
render(){
return (
<h1>Hello MyComponent</h1>
)
}
}
ReactDOM.render(<MyComponent/>, document.getElementById('root'))
类定义流程
  1. 找到MyComponent组件类
  2. 创建新实例
    1
    new MyComponent()
  3. 通过创建出来的实例调用原型的render方法
  4. 将render返回的虚拟DOM转为真实DOM呈现在页面上

类组件中的this指向哪里?

答: 组件类实例

组件实例三大属性

  • state
  • props
  • refs

state

state中用于存放组件中的一些状态信息

1
2
3
4
5
6
7
8
9
 class MyComponent extends React.Component {
constructor(props) {
super(props)
this.state = { // 构造器中初始化state
country: "UK",
color: '#00ffff'
}
}
}

在render中使用state中的数据,
注意: 每次更新state数据都会重新调用一次render

1
2
3
4
5
6
render(){return(
<h1 style={{color:this.state.color}}>
{this.state.country === 'UK'?'Hello':'こにちは'}
</h1>
)
}
组件中事件函数的声明方式

dom事件对应的回调函数并非由组件实例调用,
因此会导致回调函数中的this实际上指向的是undefined
因此需要先一步指定事件触发的回调方法的上下文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyComponent extends React.Component{
constructor(props){
super(props)
this.state = {
country:"UK",
color:'#00ffff'
}
// bind指定上下文
this.sayHello = this.sayHello.bind(this)
}
render(){
return(
<h1 style={{color:this.state.color}} onClick={this.sayHello}>
{this.state.country === 'UK'?'Hello':'こにちは'}
</h1>
)
}
sayHello(){
if(this.state.country == 'UK'){
this.state.country = 'Japan'
}else{
this.state.country = 'UK'
}
this.setState({
country:this.state.country
})
}
}
state语法糖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyComponent extends React.Component{
state = { // 类中直接定义state
country:'UK',
color:'#00ffff'
}
sayHello = ()=>{ //类中定义事件回调函数一定要用箭头函数
if(this.state.country == 'UK'){
this.state.country = 'Japan'
}else{
this.state.country = 'UK'
}
this.setState({
country:this.state.country
})
}
render(){
return(
<h1 style={{color:this.state.color}} onClick={this.sayHello}>
{this.state.country === 'UK'?'Hello':'こにちは '}
</h1>
)
}
}
ReactDOM.render(<MyComponent/>,document.getElementById('root'))

props

props用于存放需由外界传入组件的动态数据

基本用法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const sayHello = ()=>console.log('sayHello')
class Person extends React.Component{
render(){
return (
<ul>
<li onClick={this.props.sayHello}>Name:{this.props.name}</li>
<li>Age:{this.props.age+1}</li>
<li>Sex:{this.props.sex}</li>
</ul>
)
}
}
React.render( // 通过标签的属性实现props值的传入
<Person name={"aaa"} age={19} sayHello={sayHello}/>,
document.getElementById('root')
)

传入组件的参数还可以通过解构赋值实现更简洁的写法

1
2
3
4
5
6
7
8
9
let p = {
name:'bbb',
age:20,
sex:'male'
}
React.render( // 此处使用解构语法
<Person {...p}/>,
document.getElementById('root')
)

babel在对JSX做转换的时候会对下面这种语法做特殊处理:

1
<Component {...obj}/>

实际上就相当于将obj的键值对作为Component组件的属性值

props限制

需要引入prop-types.js类型检验库

1
2
3
4
5
6
Person.propTypes = {
name:PropTypes.string.isRequired,
age:PropTypes.number,
sex:PropTypes.string,
sayHello:PropTypes.func
}

类型检验需要放在组件类的propTypes属性中:

1
2
3
Person.propTypes = {
// 类型限制
}

类型配置之前需要加上PropTypes类的引用:

1
2
3
{
age:PropTypes.number
}

多个限制使用链式声明:

1
2
3
{
name:PropTypes.string.isRequired // 必须字符串
}

数据类型标志与关键字的冲突避免:

  • 为防止关键字与数据类型的冲突,将类型首字母小写处理:
    • PropTypes.number
    • PropTypes.string
    • PropTypes.bool
    • PropTypes.object
    • PropTypes.array
  • function由于是关键字,因此简写处理:
    • PropTypes.func
      1
      2
      3
      {
      sayHello:PropTypes.func
      }
定义属性默认值

使用组件类的defaultProps属性定义默认值:

1
2
3
Person.defaultProps = {
sex: 'female'
}
props语法糖

注意:props一旦传入类组件后即为只读属性,不可修改

props的语法糖实际上就是使用static静态属性给类本身定义属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class Character extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number,
sex: PropTypes.string,
makeQitatapu: PropTypes.func
}
static defaultProps = {
sex: 'male'
}

render() {
return (
<ul>
<li onClick={this.props.makeQitatapu}>Name:{this.props.name}</li>
<li>Age:{this.props.age + 1}</li>
<li>Sex:{this.props.sex}</li>
</ul>
)
}
}
function makeQitatapu(){
// to do
}
ReactDOM.render(<Person name="aaa" age={19} sayHello={sayHello} />, document.getElementById('root'))
函数式组件的props

组件函数只能用props,并且propTypesdefaultProps只能定义在函数外
(这是因为函数内的this指向undefined)
函数组件无法使用state和refs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function MyComponent(props){
return (
<div>
<h1>Hello {props.name}</h1>
<h2>{props.age}</h2>
</div>
)
}
MyComponent.propTypes = {
name: PropTypes.string.isRequired,
age: PropTypes.number
}
MyComponent.defaultProps = {
age: 20
}
ReactDOM.render(<MyComponent name={'Jerry'} />,document.getElementById('root'))

refs

ref用于标识dom容器,
一般有三种定义形式:

  • 字符串形式
    • 影响效率,不推荐
  • 内联回调函数形式
    • 官方更为推荐的方法
    • 但是重绘时会导致每次render调用两次
      • 第一次:传入参数null
      • 第二次:传入当前dom元素
  • 绑定回调函数形式
    • 能够解决重绘参数传入null的问题
  • createRef
字符串形式的ref
1
2
3
4
5
6
7
8
9
10
11
12
13
class MyComponent extends React.Component{
alertInput = ()=>{
this.alert(this.refs.input.value)
}
render(){
return(
<div>
{/* 定义字符串格式的ref */}
<input ref='input' onBlur={this.alertInput}/>
</div>
)
}
}
回调格式的ref

内联绑定两种定义格式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyComponent extends React.Component{
bindRef = (currentNode)=>{
this.input2Ref = currentNode
}
render(){
return (
<div>
{/% 内联样式 %/}
<input ref={currentNode=>this.input1Ref = currentNode}/>
{/% 绑定样式 %/}
<input ref={this.bindRef}/>
</div>
)
}
}
createRef

React.createRef调用后返回一个容器,存储被ref标识的节点,
每个容器中只能存储一个dom元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyComponent extends React.Component{
state = {
value:''
}
myRef = React.creaeRef()
myRef2 = React.createRef()
setValue = ()=>{
this.setState({
value:this.myRef.current.value
})
}
render(){
return(
<div>
<input ref={myRef} onBlur={this.setValue} />
<input ref={myRef2} />
<h1>{this.state.value}</h1>
</div>
)
}
}

受控与非受控组件

非受控组件
组件中的数据现用现取,一般需要依赖ref

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
myRef = React.createRef()
state = {value:''}
// 非受控组件借助ref获取到数据
inputInfo = ()=>{
this.setState({
value:this.myRef.current.value
})
}
render(){
return(
<form>
<input ref={this.myRef} type="text"/>
<button onClick={this.inputInfo}>Click me</button>
</form>
)
}

受控组件
state中与组件相关联的数据实时更新,类似Vue的双向绑定组件,
受控组件可以减少ref的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
state = {value:''}
// 受控组件通过原生事件中的event参数获取组件的状态值
inputInfo = (e)=>{
this.setState({
value:e.target.value
})
}
render(){
return(
<form>
<input ref={this.myRef} onChange={this.inputInfo} type="text"/>
</form>
)
}

虚拟DOM的Diffing算法

React中使用key属性对虚拟DOM进行唯一标识,
每次调用render重新渲染前,会将用最新数据生成的虚拟dom和旧的虚拟dom进行比较,
比较是将key值相同的两个dom元素拿来对比其中的内容,
如果内容改变,就用新的虚拟dom代替旧dom,
因此,dom元素的key值必须唯一并且尽量可以保持不变

使用变化key值和不变key值更新dom时的对比:

key值的唯一性会决定组件更新的dom元素有哪些
key值的唯一性会决定组件更新的dom元素有哪些

将key值全部更换

1
2
3
4
5
6
7
<ul ref='list'>
{
inputList.map((item, index)=>{
return <li key={index}>{item}=<input type="text"/></li>
})
}
</ul>

使用唯一标识作为key值

1
2
3
4
5
6
7
<ul ref='list'>
{
inputList.map((item, index)=>{
return <li key={item}>{item}=<input type="text"/></li>
})
}
</ul>

React生命周期

旧生命周期

React旧生命周期
React旧生命周期
组件挂载视角

组件初始化

constructor

组件挂载前

componentWillMount

渲染组件

render

组件挂载后

componentDidMount

组件卸载前

componentWillUnmount

父组件更新子组件视角

子组件即将获取到props传参

componentWillReceiveProps

判断组件是否更新

shouldComponentUpdate

组件更新之前

componentWillUpdate

组件重渲染

render

组件更新之后

componentDidUpdate

组件卸载之前

componentWillUnmount

两种引发组件更新的操作

两种操作会引发组件更新:

  • setState 更新state
  • forceUpdate 强制更新

setState

判断组件是否更新

shouldComponentUpdate

组件更新前

componentWillUpdate

组件重渲染

render

组件更新后

componentDidUpdate

forceUpdate

组件更新前

componentWillUpdate

组件重渲染

render

组件更新后

componentDidUpdate

新生命周期

React新生命周期
React新生命周期
React17弃用的钩子函数
  • ComponentWillMount
  • ComponentWillUpdate

React17对弃用的旧钩子函数做了兼容,需要在函数名前加上 UNSAFE_ 前缀

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillUpdate
新添加的钩子函数

三种会触发getDerivedStateFromProps的操作:

  • New props 传入新参数
  • setState 修改状态
  • forceUpdate 强制更新

getDerivedStateFromProps主要用于处理组件的state依赖props的情况

getDerivedStateFromProps
1
2
3
static getDerivedStateFromProps(nextProps,prevState){
return nextState
}

将更新前页面的状态传递到更新后的生命周期函数里

getDerivedStateFromProps
1
2
3
4
5
6
7
getSnapshotBeforeUpdate(){
// 返回值会作为第三个参数传递到ComponentDidUpdate函数中
return snapshot
}
ComponentDidUpdate(prevProps,prevState,snapshot){
// 第三个参数来自getSnapshotBeforeUpdate的返回值
}
React17生命周期

组件挂载流程

组件初始化

constructor

状态管理

getDerivedStateFromProps

组件渲染

render

组件挂载后

componentDidMount

组件更新流程

状态管理

getDerivedStateFromProps

判断组件是否更新

shouldComponentUpdate

组件渲染

render

组件即将更新

getSnapshotBeforeUpdate

组件挂载后

componentDidMount

React脚手架

create-react-app

全局安装react脚手架依赖包

1
npm i -g create-react-app

使用脚手架创建新项目

1
create-react-app 新建项目名
脚手架搭建项目的目录结构
脚手架搭建项目的目录结构

默认提供四个运行脚本:

  • start 运行项目,默认端口3000
  • build 项目打包,使用webpack项目管理工具
  • test 前端测试
  • eject 将隐藏的webpack配置文件暴露出来,此过程不可逆
启动项目后的初始化界面
启动项目后的初始化界面
public文件夹

public文件夹存放一些静态文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>

网页做app套壳时的配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

网页爬取内容的相关文件

1
2
3
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:
src文件夹

src为开发使用到的的主要目录,
初始化的src目录结构如下:

  • App.css 根组件样式文件
  • App.js 根组件js文件
  • App.test.js 根组件测试文件
  • index.css 全局样式
  • index.js 项目入口js文件
  • reportWebVitals.js 用于记录页面性能,依赖web-vitals库
  • setupTests.js 测试文件,依赖jest-dom库
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react'
import ReactDOM from 'react-dom/client'
import './index.css'
import App from './App'
import reportWebVitals from './reportWebVitals'

// 创建根节点
const root = ReactDOM.createRoot(document.getElementById('root'))
// 渲染根组件
root.render(
// React.StricMode用于检查App内子组件是否合理
// 如 字符串类型ref 等 类似Eslint
<React.StrictMode>
<App />
</React.StrictMode>
)
// 引入reportWebVitals文件
// 依赖web-vitals库 用于记录页面性能
reportWebVitals();

React Ajax

跨域

本机3000端口服务需要请求本机5000端口数据,

可以直接请求吗?

1
axios.get('http://localhost:5000/simData')

答: 不能,因为跨域

到底是在哪一步跨域失败呢?

  • 跨域发送请求 √
  • 跨域返回数据 ×
跨域的解决

解决方法实际上就是在3000(Client)5000(Server) 之间,
再配置一台代理服务器(同样在3000端口),
3000端口同时提供微型代理功能客户端功能

解决方式1:package.json配置

适合的业务场景:代理目标唯一

配置位置:package.json
1
"proxy":"http://localhost:5000"
这样项目中所有发送到3000端口的请求都会被转发到5000端口
解决方式2:http-proxy-middleware

适合的业务场景:需要代理多个目标地址时

配置位置:src/setupProxy.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
let {createProxyMiddleware} = require('http-proxy-middleware')
module.exports = function(app){
app.use(
createProxyMiddleware('/api1',{
target: 'http://localhost:5000',
changeOrigin: false,
pathRewrite:{
"^/api1" : ""
}
}),
createProxyMiddleware( '/api2',{
target: 'http://localhost:5001',
changeOrigin: false,
pathRewrite:{
"^/api2" : ""
}
} )
)
}
changeOrigin

changeOrigin用于决定是否在服务器端暴露请求源

changeOrigin = true 不暴露请求源

此时3000请求5000,服务器端收到响应头中Host如下:

1
HOST = localhost:5000(服务器端地址)

changeOrigin = false 暴露请求源

此时3000请求5000,服务器端收到响应头中Host如下:

1
HOST = localhost:3000(客户端地址)
pathRewrite

表示是否对url的部分路径进行重写,
被替换的部分往往是用于进行代理转发识别的路径段,
比如配置如下:

1
2
3
4
5
6
createProxyMiddleware('/api1', {
target: 'http://localhost:5000',
changeOrigin: false,
pathRewrite: {
'^/api1':''
})

则当请求的url为 http://localhost:3000/api1/simData 时,
url会经历如下处理:

第二次学习(失败):快速系列

1
npx create-react-app app-name
  • 数据注入
  • 列表渲染
  • 事件
  • 状态处理 useState
    • 基础类型
    • 数组类型
    • 对象类型
  • 参数传递
    • 父传子:props
    • 子传父:函数参数裹挟值传递
    • 同级传递:父组件中转
    • 多级传递:Context Hook

参数传递场景练习

一个用户评论列表,有三层结构:
List 列表
ListItem 列表项
Button 点赞按钮
context状态项:
theme 控制样式风格
filterOption 筛选项
- userName 用户名
- isSort 是否按照点赞数排序

第三次学习(完结):面向Vue基础系列

b站教程

jsx基础

组件首字母一定要大写

  • 函数式组件

    1
    2
    3
    function FunHelloComp(){
    return <div>Func Comp</div>
    }
  • 类式组件

    1
    2
    3
    4
    5
    6
    7
    8
    class ClassHelloComp extends React.Component{
    constructor(prop) {
    super(prop);
    }
    render(){
    return <div>Class Comp</div>
    }
    }

react中创建组件的2种方法:

  • jsx:需要由babel将jsx翻译为js
  • React.createElement 实际实现jsx的接口

事件传递

react事件绑定的函数被触发时,
如果不做处理,this的指向是undefined

有如下方式使得this指向当前组件:

  • 箭头函数定义事件方法
  • 事件绑定时用bind函数处理事件方法
  • 行内定义事件方法

传参方式:

  • 高级函数
  • 使用bind获取新函数
    • 第一个参数为this
    • 最后一个参数为事件对象event

响应式数据

react响应式原理
  • vue:监听响应式变量的get和set方法
  • react:手动调用setState进行更新
    • 往setState内传入一个新对象,其中包含需要更新的数据
    • 将传入的新对象与旧state进行合并
    • 使用合并后的state对页面视图进行更新
响应式的优化

无论数据有没有发生变化,直接调用setState都会触发页面的重绘,

要想优化这一点可以使用PureComponent:

1
class App extends React.PureComponent{}

使用PureComponent时,如果响应式数据没有被更新,就不会触发多余的页面重绘。

注意:
使用PureComponent是对数据更新的一种优化
会监听变量的内存地址的变化
因此仅仅是数组和对象的成员变化是监听不到的
必须要对原数组或者对象进行拷贝更新

组件间参数传递

props

react不需要在子组件内对prop声明,
只要传入的参数, 都可以直接拿来用

  • prop类型验证:propTypes
    • 类型验证库:proptypes
    • npm install proptypes –save

1
2
3
4
5
6
7
8
9
ChildComp.propTypes = {
// react中,类型验证必须是个函数
mes: function(props){
if(typeof(props.mes) !== 'string'){
throw new Error("Mes must be a string");
}
},
count: PropTypes.number
}
  • 默认值:defaultProps
1
2
3
ChildComp.defaultProps = {
mes:"Default Mes"
}
插槽
  • 默认插槽
    • props.children
  • 具名插槽
    • 使用HtmlElement元素作为props参数传递
  • 作用域插槽哦
    • 返回HtmlElement元素的props函数
    • 由父组件创建,能获取到子组件值的插槽

样式

  • 引用.css后缀的样式文件,作用域为全局。
  • 引用.module.css后缀样式文件,相当于vue中的scoped,对全局作用域做了限制
    • 这种引入方式需要对样式文件进行命名引用
      1
      2
      3
      import childStyle from "child.module.css";
      //.....
      <div className={childStyle.title}></div>
classnames库

使用classnames库,能够快速用js变量控制class类名的toggle切换

npm install classnames –save

1
2
3
4
5
6
7
8
9
10
11
12
import classnames from "classnames";
// ...
state={
hasClass2:false
}
//...
<div className={classnames({
class1:true,
class2:this.state.hasClass2
})}>

</div>

也可以通过classnames.bind,将样式文件使用这种方式进行控制

1
2
3
4
5
6
7
8
import classnames from 'classnames/bind'
import childStyle from "./child.module.css"
const childBindClassNames = classnames.bind(childStyle)
//...
<div className={childBindClassNames({
class1: true,
class2: false
})}></div>

生命周期

  • React.StrictMode模式下,生命周期执行两次
  • shouldComponentUpdate
    • 表示是否应该更新
    • PureComponent优化的原理
    • 根据获取到的新值判断是否进行更新
  • componentDidMount
    • 相当于vue中的mounted
对比vue和react的更新
  • vue
    • 在get和set函数中触发更新
    • 在get函数中进行依赖收集
    • 在数据更新后,只更新用到该数据的地方
  • react
    • 调用方法触发更新
    • 更新整个组件树

增删改查案例

ref && context

ref

父组件访问子组件参数

1
2
const childRef = React.createRef();
childRef.current.addCount() // current代表子组件的this
context
  • 使用 React.createContext 创建context
  • 使用ContextName.Provider进行value的传递
    • 使用ContextName.Consumer接受参数
    • 并用回调函数获取到value值
1
2
3
4
5
6
7
8
9
10
11
12
13
// 父组件
export const Context1 = React.createContext();
export default class Parent extends React.PureComponent {
render(){
return (
<>
<Context1.Provider value={{key:'123',label:'外太空'}} >
<Child ref={childRef}></Child>
</Context1.Provider>
</>
)
}
}
1
2
3
4
5
6
// 子组件
export default class Parent extends React.PureComponent {
render(){
return <GrandSon/>
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 孙组件
import {Context1} from "./Parent.js";
export default class GrandSon extends React.PureComponent {
render(){
return (
<Context1.Consumer>
{value=>{return (
<div>
<p>{value.key}</p>
<p>{value.label}</p>
</div>
)}}
</Context1.Consumer>
)
}
}

函數组件

和类组件的区别
  • 无生命周期
  • 不用考虑this
  • 通过hook完成操作
  • 函数体相当于render函数
  • props作为函数的第一个参数
Hook

React内置Hook

  • useState
  • useEffect
    • 类watch功能
  • useMemo
    • 类computed功能
    • 防止不必要的数据更新操作
  • useCallback
    • 方法的缓存,防止不必要函数更新操作
  • useRef
  • useContext
    • 更方便获取Context.Provider中提供的值

高阶组件

和高阶函数(返回函数的函数)概念相似,
高阶组件相当于一个返回组件的函数,
主要用于:

  • 在多个组件上装载相同的逻辑片段
  • 对多个组件进行相同的生命周期处理
    • 场景:修改父组件的state数据时,对子组件进行选择性更新(shouldComponentUpdate)

使用一套逻辑封装的鼠标定位组件:

关于性能优化

React的性能优化需要开发者手动去实现,
如果不做任何处理,
一个父组件的更新往往会触发它所有子组件的更新。

  • React.memo 对组件尽心优化的高阶组件,避免不必要的组件更新
    • 相当于类组件中的PureComponent
  • useMemo 对静态类进行优化
  • useCallback 对静态方法进行优化

React-router

npm依赖
  • React-router 服务端渲染SSR
  • React-router-dom 浏览器渲染
  • React-router-native ReactNative混合开发
React-router-dom

首先要给需要路由的组件添加最外层标签包裹:
两种包裹方式:

  • HashRouter
  • BrowserRouter
1
2
3
<HashRouter>
<App/>
</HashRouter>

路由跳转

  • Routes/Route 声明路由跳转的页面
  • NavLink 多用于导航栏,最近被点击的navlink会多出active类
  • Link 纯路由跳转用标签

路由重定向

  • Navigate

子路由

  • Outlet
1
2
3
4
<Route path="/wish" element={token?<Wish/>:<Navigate to="/home"></Navigate>}>
<Route path="createWish" element={<CreateWish></CreateWish>}></Route>
<Route path="destroyWish" element={<DestroyWish></DestroyWish>}></Route>
</Route>
1
2
3
4
5
6
7
8
export default function Wish(props){
return (
<div>
Best Wish:
<Outlet></Outlet>
</div>
)
}

异步加载路由

  • Lazy
  • Suspense
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const LazyKill = lazy(()=>{return import ("./pages/Kill");})
function App(){
let token = 12
return (
<div className="App">
<ul className="nav-ul">
<li className="nav-link"><NavLink to="/home">HOME</NavLink></li>
<li className="nav-link"><NavLink to="/mine">MINE</NavLink></li>
<li className="nav-link"><NavLink to="/kill">KILL</NavLink></li>
<li className="nav-link"><NavLink to="/wish/createWish">Create</NavLink></li>
<li className="nav-link"><NavLink to="/wish/destroyWish">Destroy</NavLink></li>
<li className="nav-link"><NavLink to="/work/123">Work</NavLink></li>
</ul>
{/* v5:Switch v6:Routes*/}
<Suspense fallback={<h1>加载中。。。</h1>}>
<Routes>
<Route path="/home" element={<Home></Home>}></Route>
<Route path="/mine" Component={token?Mine:Home}></Route>
<Route path="/kill" element={<LazyKill></LazyKill>}></Route>
<Route path="/wish" element={token?<Wish/>:<Navigate to="/home"></Navigate>}>
<Route path="createWish" element={<CreateWish></CreateWish>}></Route>
<Route path="destroyWish" element={<DestroyWish></DestroyWish>}></Route>
</Route>
<Route path="/work/:id" Component={Work}></Route>
</Routes>
</Suspense>
</div>
)
}
路由参数
params参数

useParams 直接返回params参数

1
2
3
<Routes>
<Route path="/work/:id" Component={Work}></Route>
</Routes>
1
2
3
4
5
6
7
8
export default function Work(){
let params = useParams()
return (
<div>
Work No. {params.id}
</div>
)
}
query参数

useSearchParams 返回类似useState格式的数据
/mine?name=LilyAndAndy

1
2
3
4
5
6
7
8
9
10
11
12
export default function Home(){
const [searchParams, setSearchParams] = useSearchParams()
console.log(searchParams.get("name")) // LilyAndAndy
setSearchParams({
name:"NiceBoy"
})
return (
<div className="Home app-page">
mine mine mine
</div>
)
}
location

路由跳转:useNavigate

返回路由跳转时的一些数据:

1
const location = useLocation()

路由跳转时,可以跳过params和query方式,传递数据:

1
2
3
4
{/* state内的数据在nextPage中可以通过location的方式获取到 */}
<button onClick={()=>goto("/nextPage",{
state:{mes:"hello world"}
})}>Go to kill</button>
权限控制
  • Route.element/Component中做判断:
  • 使用Navigate重定向
1
<Route path="/wish" element={token?<Wish/>:<Navigate to="/home"></Navigate>}>
异步路由
  • Lazy
  • Suspense
1
2
// 异步引入组件
const LazyKill = lazy(()=>{return import("./pages/Kill")})
1
2
3
4
5
<Suspense fallback={<h1>加载中。。。</h1>}>
<Routes>
<Route path="/kill" element={<LazyKill></LazyKill>}></Route>
</Routes>
</Suspense>

全局状态管理

npm库
  • react-redux
    • Redux
    • @reduxjs/toolkit
  • react-mobx
    • Mobx
Redux

需要手动实现store逻辑的修改,比较麻烦:

reducer仓库创建
  • state:管理的状态
  • action:操作相关的自定义参数
    • 约定俗成type为操作类型,payload为目标值
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {legacy_createStore as createStore} from "redux";
// 创建reducer状态管理函数
function mesReducer(state={mes:"hello"}, action){
switch(action.type){
case "changeMes":
state.mes = action.payload;
return {...state}
case "resetMes":
state.mes = "hello";
return {...state}
default:
return state
}
}

// 包装成store并返回
let store = createStore(reducer)
export default store

拆分成模块合并包装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import {combineReducers} from "redux";
function mesReducer(state={mes:"hello"}, action){/**/}

function numReducer(state={version:1}, action){
switch(action.type){
case "addVersion":
state.version ++;
return {...state}
case "resetVersion":
state.version = 1;
return {...state}
default:
return state
}
}

const reducer = combineReducers({
mesReducer,
numReducer
})
store.state的双向绑定

Redux中维护的状态并不会像state或props一样,
被监听是否改变并刷新页面。

  • 思路一:store.subscribe(不推荐)
    • 使用store.subscribe进行监听,每当有数据被修改时,重新渲染组件
  • 思路二:使用react-redux
    • react-redux提供connect函数,能够在state和props之间建立映射关系,从而实现响应式更新

react-redux使用步骤:

  • 对根组件进行Provider包装
  • 在调用状态的组件中使用connect进行关联
    • connect是一个返回包装后组件的高级组件
    • connect关联的状态在新组件中的props中包裹
    • connect的2个参数:
      • 参数1:对state进行映射返回
      • 参数2:对dispatch进行包装返回
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {connect} from "react-redux";
const connectApp = connect(
(state=>{
console.log(state)
return {
mes:state.mesReducer.mes
}
}),
(dispatch=>{
return {
dispatch,
customFun:()=>console.log("print something...")
}
})
)(App)

export default connectApp;
@reduxjs/toolkit

toolkit是redux的优化版,不同点在于:

  • 状态管理模块创建方式不同:
    • redux创建reducer函数,legacy_createStore对store进行包装
    • toolkit使用createSlice创建切片slice
      • configureStore对切片进行整合
  • 调用方式不同
    • redux中dispath传递的actions.type类型要亲自定义可选操作和逻辑
    • toolkit中根据切片的name和reducers中的成员名进行调用

创建状态切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
let mesSlice = createSlice({
name:"mes", // 切片名称,用来在dispatch中定位
initialState:{
mes:"hello",
},
reducers:{
changeMes(state,action){
console.log(state,action)
state.mes = action.payload
},
resetMes(state,action){
state.mes = "hello"
}
}
})

组合切片:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let mesSlice = createSlice({
name:"mes",
initialState:{/**/},
reducers:{/**/}
})
let numSlice = createSlice({
name:"num",
initialState:{/**/},
reducers:{/**/}
})
let store = configureStore({
reducer:{
mesReducer:mesSlice.reducer,
numReducer:numSlice.reducer,
}
})

切片reducer的调用

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function App(){
const state = useSelector((state=>{
return state.numReducer
}))
const dispatch = useDispatch()
return (
<div>
Version: {state.version}
<br/>
<button onClick={()=>dispatch({type:"num/addVersion"})}></button>
</div>
)
}
响应式修改状态的几种方式
  • connect函数
    • 通过将props和store.state进行映射关联
  • Hook
    • useState:组件的state和store.state进行映射关联
    • useDispatch:获取到store的可操作方法

useDispatch不需要用action调用到目标方法,可以直接将方法从切片中暴露出来:

1
2
3
// 将方法从切片的actions中暴露出来
export const {changeMes} = mesSlice.actions
export const {addVersion,asyncAddVersion} = numSlice.actions
1
2
3
4
// 直接在dispatch中调用
import {addVersion} from "./store/toolkit_index";
const dispatch = useDispatch();
<button onClick={()=>dispatch(addVersion())}>新增版本号(引入版本)</button>
异步修改全局状态

redux的action中是不支持直接执行异步修改状态的操作的,
要想异步修改状态,必须对异步的三个状态(pending、failing、fulfilled)各自的操作进行定制(好麻烦)

其间需要使用到createAsyncThunk,创建异步执行对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* createAsyncThunk 为异步操作做包装:
* 参数1:操作名
* 参数2:异步执行函数
*/
export let changeNumThunk = createAsyncThunk('numSlice/asyncAddVersion', async (params)=>{
return await new Promise((res,rej)=>{
console.log(params)
setTimeout(()=>{
res(999)
},1000)
})
})

执行对象需要包裹在切片中的extraReducers中,
这是一个专门处理切片内部逻辑(不用于暴露到dispatch)的reducers定义模块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let numSlice = createSlice({
name:"num",
initialState:{/**/},
reducers:{/**/},
extraReducers:(chunk)=>{
// pending状态的配置
chunk.addCase(changeNumThunk.pending,()=>{
console.log("pending")
}).addCase(changeNumThunk.fulfilled,(state, action)=>{
console.log("fulfilled")
state.version = action.payload
})
}
})
// 暴露出来
export const {asyncAddVersion} = numSlice.actions

异步操作的使用:

1
2
3

import {asyncAddVersion} from "./store/toolkit_index";
<button onClick={()=>dispatch(changeNumThunk(456))}>新增版本号(异步+引入版本)</button>

路由权限案例

案例描述
  • 根据用户登录身份,服务端返回不同的路由
  • 客户端根据返回的路由生成路由映射
  • 当用户访问没有生成的路由时,重定向到默认页面

React生态

React生态常用库
  • UI组件:Ant Design / Ant Design Mobile
  • 应用框架:Umi

官方文档阅读

Hook

memo/useCallback/useMemo

memo

React子组件更新的触发方式有几种:

  • 父传子的props改变
    • 基础变量类型props(memo包装)
    • 函数类型props(useCallback)
    • 引用类型props(useMemo)
  • 自身state改变
  • 内部用到的context改变
优化意见
  • 需要将props的变化范围控制在最小
    • 防止一个总会变化的props无效化memo
  • 如果不是绘图级别细粒度的组件更新,memo优化程度不大
  • 数据视图的展示放在父组件,修改数据方法用useCallback包装传入子组件

useContext

  • useContext
  • createContext

搭配实现组件对context的订阅

  • context支持基础类型、引用类型
  • 防止Provider结构过于冗杂,可以将Provider封装成组件
  • context可用于在传递对象和函数时进行优化重新渲染

useReducer

useReducer旨在对大量state相关操作进行逻辑集中书写,
reducer函数需要两个参数:

  • state:被操作的state数据
  • action:选择的操作

reducer函数的返回值就是state被修改的结果

比如需要对同一个列表list进行增删改查操作,
action一共有4种:增删改查
reducer中根据action的不同,集中实现四种操作对应的逻辑,
对外只暴露出4种action

使用reducer往往出于以下几点考虑:

  • 代码可读性
  • 可调试性
  • 可测试性

useEffect

一般用于将组件和外部组件同步

  • 常常将重复的useEffect用自定义Hook的方式单独拉出来封装
  • 可以定义setup和cleanup操作
  • 常用场景
    • 封装和第三方组件之间的接口
    • 挂载和清理Event事件
    • 请求数据

useEffect可以指定依赖项:

  • 依赖为[],只执行一次
  • 不加依赖数组,每次useEffect内容都会重新执行
  • 有依赖项,依赖项改变,useEffect内容会重新执行
    • 在使用引用类型(函数/对象)作为依赖项时需要谨慎

forwardRef && useImperativeHandle

用forwardRef进行包装的子组件,可以将ref节点暴露给父组件,
相当于vue中的defineExpose:
这种暴露可以跨辈获取。
配合useImperativeHandle可以自定义子组件暴露的内容

useLayoutEffect

在浏览器重新绘制屏幕之前触发

使用场景:
网页内容的布局有时会根据显示内容而改变,
当希望用页面上一帧的信息计算下一帧的布局时,
就可以在useLayoutEffect中进行

useOptimistic

允许在异步操作时更新state,
增强用户体验,
使用场景:
类似于数据未加载到客户端时的loading作用

useSyncExternalStore

主要是为了在组件中,响应式展示第三方数据源。
需要2个参数:

  • subscribe
    • 参数为触发组件渲染的函数
    • 返回值为clenaup函数
  • getSnapshot
    • 返回值决定暴露那些数据

使用场景:

  • 类似store的用法,作为全局状态管理器
  • 监听浏览器的变量变化

组件

  • Fragment 用于组合多个组件的空标签
  • Profiler 测量组件渲染性能
  • StrictMode 严格模式
  • Suspense 在组件加载完成之前提供替代渲染内容

API

  • createContext 创建上下文
  • forwardRef 创建可以将内容暴露给父组件的组件
  • memo 创建根据props/state/context进行缓存的组件(而非强制根据父组件的重新渲染而重新渲染)
  • lazy 组件懒加载
  • startTransition 不阻塞UI的情况下更新state

lazy

用于实现组件懒加载
可以配合Suspense实现组件灵敏加载

综合练习

Hook综合练习

一个聊天软件界面:

  • 鼠标移入时,光标变为圆点
    • useEffect
    • 自定义Hook封装
  • 点击人,输入邮件内容,发送邮件
  • 聊天内容模块,提供跳转到最早聊天记录/查看最新聊天记录按钮
    • 父组件调用子组件方法 forwardRef useImperativeHandle
  • 2个主要组件:
    • 联系人列表
      • prop1 联系人列表
      • context1 当前发送信息的联系人
        • context1 在此处被修改
    • 聊天界面
      • context1 当前发送信息的联系人
  • 全局存储:
    • useContext
      • 当前发送信息的联系人
      • 界面主题颜色
  • 监听网络:
    • useSyncExternalStore监听浏览器网络连接
组件纯粹性

React提倡组件要保持纯粹,
意思就是组件函数无论被调用创建多少次,渲染结果应当都是一样的,
就和纯函数一样,只要输入值不变,输出值永远相同,
这就意味着不应当引入全局变量等可能出现mutation(突变)的内容来污染组件

这也是为什么在开发环境、严格模式下,
React会将每个组件内容执行2次的原因,
就是为了检查组件的纯粹性