1、初识
类组件class
使用ES6面向对象的语法,有生命周期函数、this、state、ref、context
缺点:性能比函数式组件差一点,已经不是主流,但仍需掌握
函数组件
自React就有,没有...,配合hook达到类组件的功能
import { Component } from 'react'
class DemoA extends Component {
render(){
return (<h1>类组件class</h1>)
}
}
function DemoB() {
return (<h1>函数式组件</h1>)
}
export default DemoB
2、jsx
给组件提供视图模板,是可选的
写法
jsx
React.createElement:浏览器不能识别jsx ,使用babel翻译成React.createElement的形式
本质:React.createElement()的返回值,即一个不可变对象 ---- 因为jsx元素最终要被渲染成真实的DOM元素,所以不能对这个对象中的属性直接进行操作,只能使用
JSX对象可以叫做”fiber单元",很多很多的嵌套的”fiber单元"就会构成”fiber树"
嵌套表达式必须用 {} 包起来 任何有值的内容都是表达式
可以做函数入参、返回值,还可用在if、for中
支持点语法
三个变化的属性
class ===> className
for ===> htmlFor
tabindex ===> tabIndex
新增三属性
key --- 列表渲染
ref --- 便于dom操作
dangerouslySetInnerHTML --- 渲染一片HTML字符串
只能有一个根标签, 根标签还可以这样写: <> </>
组件和任何HTML标签都可以使用单标签
双标签之间内容是通过props.children来接收,可以是任何类型的数据
所有组件名必须以大写字母开头!如果小写,就是普通函数
行内样式必须是对象形式,而对象是js代码,js代码需要写在 {} 中
不管是类组件,还是函数式组件,都有props(父给子传递数据)
对组件来说,props---自定义属性,props只能使用,不能修改;对HTML标签来说,props---标签属性
在JSX中是不能直接渲染一个对象,但是可以渲染一个数组,而如果数组中有Boolean值、null、undefined,会直接被忽略,不会生成对应的文本节点
使用ref可以获取DOM元素
3、状态
类组件
必须有render
constructor中 this.state={}定义状态
this.state.xxx 直接修改状态,但render不会重新执行,不推荐
this.setState 修改状态,有两种写法:
this.setState({}, callback)
新值与旧值无关时,推荐使用这种写法,callback表示当状态修改后,自动执行,当状态修完后,有一些业务逻辑放到callback中。
this.setState((state, props)=>({}), callback)
新值与旧值有关时,新值由旧值计算而来,形参state永远表示旧值,建议使用这种写法。callback同上。
为了性能优化,在V18中,this.setState设计成异步,这种特性叫”并发模式“
在同一个函数作用域中,为了减少没必要的diff运算,多次this.setState会自动合并成一次,叫协调运算
修改状态的同步异步问题
react18中:异步 (并发模式)
react18前:合成事件 --- 异步, 宏任务&Promise.then --- 同步
避免至二级修改(只使用老状态,不修改老状态)
函数组件
需要配合hook(useState)定义状态
参数1---状态 参数2---修改状态
形参变量名可以使用 _ 代替 ,表示老状态
let [num, setNum] = useState(1)
let add = () => {
// 写法1
// setNum(num + 1)
// 写法2
setNum(_=>_+1)
}
setNum(修改状态)中,没有callback
同步异步问题
V18之前:合成事件---异步,宏任务&promise.then---同步
V18:异步
初识事件绑定
onXxx----合成事件
<button onClick={this.add}>+1</button>
上面的写法不完美,因为在监听器中不能获取到this,是und。
解决方法:
利用ES5中的bind中手动绑定this
<button onClick={this.add.bind(this)}>+1</button>
利用ES6中的箭头函数自动绑定this,推荐
<button onClick={()=>this.add()}>+1</button>
在constructor中进行this绑定
constructor(props){
super(props)
this.state={
num:1
}
this.add=this.add.bind(this)
}
4、条件渲染
在React中,没有指令,使用原生JS来实现
import { Component } from "react";
class DemoA extends Component {
constructor(props) {
super(props)
this.state = {
flag: true,
num: 0
}
}
// 多个元素---封装自定义渲染函数
renderHn() {
let { num } = this.state
let res = null
if (num === 1) res = <h1>im h1</h1>
if (num === 2) res = <h2>im h2</h2>
if (num === 3) res = <h3>im h3</h3>
return res
}
render() {
let { flag } = this.state
return (
<div>
<h1 style={{ display: (flag ? 'block' : 'none') }}>类组件</h1>
{/* 单一元素 */}
{flag && <h1>h1</h1>}
{/* 多个标签 */}
{flag &&
(
<div>
<h1>hola</h1>
<h1>hola</h1>
</div>
)}
{/* 两个元素 */}
{flag ? <h2>h2</h2> : <h3>h3</h3>}
{/* 多个元素 */}
{this.renderHn()}
{/* 实现显示隐藏 */}
<button onClick={() => this.setState(_ => ({ flag: !_.flag }))}>show/hidden</button>
<button onClick={() => this.setState(_ => ({ num: (_.num + 1) % 7 }))}>change num</button>
</div>
)
}
}
export default DemoA
5、表单绑定、列表渲染
受控表单:一个表单的value或checked由state来决定,通过控制state就可以修改表单数据,这样的表单叫受控表单。
受控组件:一个组件的自定义属性由状态来控制,只有这个状态发生变化,组件才能更新。后面讲。
列表渲染:通过map方法实现。因为map方法可以对数据进行加工,返回新的数据(jsx)
6、类组件的生命周期
生命周期函数很多,我们需要掌握6个:
装载阶段(3个):constructor, render, componentDidMount
更新阶段(2个):render,componentDidUpdate
卸载阶段(1个):componentWillUnmount
consructor
调用父类的构造函数
super必须在第一行
props:接收父组件传递的任何数据,其数据流与state的数据流必须独立
一切和业务相关的代码都不能写在constructor钩子函数中
不能调用setState方法(组件未挂载完毕)
不能调用接口
不能进行DOM操作
不能开定时器...
可以:
定义状态
绑定方法的this
render
类组件必须要有这个钩子函数
用于返回组件的视图结构,jsx
调用render函数,会异步生成棵Fiber树(双向链表结构),然后进行协调运算,类似于Vue中的Diff运算,也就是老Fiber树和新Fiber树对比运算。然后,进入到commmit提交阶段,一次性提交Fiber更新DOM。
不能调用setState
页面调用setState后,会re-render重新渲染
componentDidMount
页面第一次渲染完成
可以:
调用接口
开定时器
DOM操作
编写业务逻辑
componentDidUpdate
页面再次渲染成功
模拟vue中的监听器,监听数据是否变化(推荐)
不使用componentDidUpdate也能实现类似vue中的监听器:this.setState({}/fn, callback)
利用callback也可以感知到数据变化,但推荐使用componentDidUpdate。因为多个setState会合并,合并后,callback容易出问题。
可以调用setState,但必须给一个出口(终止条件),否则会陷入死循环,循环到一定次数就会报错。
componentWillUnmount
组件即将销毁
可以:
清缓存
清除定时器
关闭长连接
销毁DOM元素...
shouldComponentUpdate
控制是否更新,返回true正常更新,返回false不更新。
在项目中用的不多,是官方提供的一种性能优化方案。
当执行forceUpdate时,会绕过shouldComponentUpdate方法,一定会进入到更新阶段。
可以使用PureCompoentf替代
只有参与页面渲染的状态变化了才会渲染,可以提升性能,尽可能减少生成Fiber树
为什么要使用这个开关呢?
组件中有很多状态,有些状态会参与到界面刷新,但是还有一些状态是不参与到界面更新,也就是状态变了,不需要更新页面的,此时就体现出开关的重要性了。参与页面更新的状态,状态变化了,在showCompoentUpdate中返回true,正常更新。如果没有参与页面刷新的状态变化了,在shouldCompoentUpdate中返回false,就需要再次调用render。这样,就会少生成一次Fiber树。这个钩子函数是用来性能调优的,可以阻塞掉那些不参与视图渲染的状态更新导致的Fiber生成。
React组件渲染(更新)流程
两个阶段组成
render阶段:
目标是生成Fiber树,这个过程是异步的,是可中断,并且不会执行任何副作用。到底中断与否,看的是浏览器主线程的忙不忙。
commit阶段:
目的是把协调运算的结果,一次性提交渲染或更新真实DOM。这个过程在V18之前是不可中断的,在V18中是可以人为中断的
7、状态提升
子组件间通过把状态定义到父组件实现通信
在react中没有自定义属性或自定义事件,都是props
8、封装组件
UI组件库,官网:https://ant.design/index-cn/
组合是React组件化的设计模式。也就是研究如何封装一个组件,步骤如下:
第一步:根据UI设计图拆解组件。
第二步:把这个独立的组件单独进行封装。
第三步:利用props把组件串联起来。
在封装组件时,我们需要给组件传递非常多的数据,此时,需要对数据进行校验,需要使用一个第三方包,prop-types
"render props":就是可以参与组件的视图渲染的props,在封装组件时,用的非常多。
9、上下文
组件间的通信,类似于Vue中的proveder/inject。
特点:
在组件树中,上下文中一种单向数据流通信,不能颠倒。
通信是可以跨级的,祖先提供数据,后代消费数据。
这个通过方法,不具有响应式
只有类组件有
上下文的使用场景:
路由
状态管理
在一些组件库,如切换主题,切换组件大小...
国际化
使用上下文的步骤:
创建const ThemeContent = React.createContext()创建上下文
使用Provider提供数据,是给后代提供数据
消费上下文中的数据有2种方案
import React, { PureComponent, useState } from 'react'
const ThemeContext = React.createContext()
const { Provider, Consumer } = ThemeContext
// 方法1
class Child extends PureComponent {
render(){
return(
<div>
<h1>子组件</h1>
</div>
)
}
}
Child.contextType=ThemeContext
// 方法2
class Child extends PureComponent {
render(){
return(
<Consumer>
{
(ctx)=>{
return(
<div>
<h1>子组件:{ctx}</h1>
</div>
)
}
}
)
function Parent(props) {
return (
<div>
<h3>父组件</h3>
<Child />
</div>
)
}
function PageA() {
return (
<Provider value={'100'}>
<div>
<h2>page</h2>
<hr />
<Parent />
</div>
</Provider>
)
}
export default PageA
React中的组件通信:
状态提升(父传子,子传父),核心靠props
上下文,是祖先与后代之间的通信,父子关系不需要明确
props穿透,需要搞清楚父子关系,缺点:会让后代的props变得臃肿
10、hooks
官方提供的API,V16.8新增
在函数式组件中模拟类组件的功能,如state,ref,context,生命周期...
useState, useEffect, useLayoutEffect,useContext, userReducer, useRef, useMemo, useCallback..
开源hook: react-use, ahooks...
useState状态
定义状态
const [num,setNum]=useState(1)
为什么使用const
避免直接修改状态,要修改状态,就要使用专属方法(参数2)
为什么是数组解构,不是对象解构
如果是对象解构,useState中返回的对象的键是固定的,如果是数组,可以随便起名
setNum(参数2)的2种写法
setNum(num+1)
setNum(arg=>arg+1) arg是旧num,没有类似类组件中setState的callback
调用setNum,状态改变,组件的更新流程是什么?
调用setNum时,会触发整个函数组件的执行,生成Fiber树,进一步执行协调运算,最后commit提交更新DOM
如何实现+1
调用setNum,整个函数重新执行,按理说useState也会重新执行,num每次都是1;state定义状态时,定义出来的1储存在react最底层,当setNum执行时,并不会重新setState,而是之前底层定义的1
同步异步问题
V18中,异步
useEffect效果
模拟生命周期:componentDidMount/componentDidUpdate/componentWillUnmount
类似于vue中的watchEffect,vue中的watchEffect会自动依赖依赖,React中的useEffect,需要手动指定依赖
一个函数式组件中,可以写多个useEffect,且彼此互不影响
建议一个useEffect只执行一个效果,不要同时执行多个效果
不要把效果直接暴露在函数体内,一定要用useEffect进行控制
useEffect(()=>{
fn1() // 效果 --- 相当于componentDidMount()
return fn2() //清除效果 --- 相当于componentWillUnmount()
},[依赖数组]) // 相当于componentDidUpdate
工作流程
没有''依赖数组"这个参数时
初始化只执行fn1
当re-render时,先执行fn2,再执行fn1
路由切换时,只执行fn2
有''依赖数组"这个参数,但是一个空数组时
初始化只执行fn1
当re-render时,什么也不执行
路由切换时,只执行fn2
有''依赖数组"这个参数(依赖可以有多个)
初始化只执行fn1
只有当依赖数组中的变量发生变化而导致re-render时,先执行fn2,再执行fn1
路由切换时,只执行fn2
useLayoutEffect布局效果
运行机制和useEffect相同,区别在于useLayoutEffect执行更早
一般项目中很少用,在一些第三方库中用的比较多
在这个hook中,不能进行ref或dom操作。
useMemo备忘录
用于性能优化,缓存一些比较消耗性能的计算,类似vue的计算属性
const memoizedValue = useMemo(() => computeExpensiveValue(a,b),[a,b]);
参数2:依赖数组,也有3种写法,和useEffect一样
仅当依赖数组中的状态改变时,useMemo才会重新执行耗能的计算
let total = useMemo(() => {
return 6 * num
},[num])
useCallback回调
用于缓存函数声明,进行性能优化,可以用useMemo替代
const fn = useCallback(() => {}, [依赖数组])
useContext上下文
提供访问上下文的入口
const ctx=useContext(上下文对象)
useRef参考
ref与ref转化:
类组件中,如果ref写在DOM元素上,目的是为了获取DOM元素,进而操作DOM元素。
如果ref写在类组件标签上,目的是为了获取组件实例, 进而实现组件通信
如果ref写在函数组件标签上,会报错。 需要使用ref转发,转发到了函数式组件中的JSX的DOM标签上, 进而获取函数式组件中的JSX的DOM元素。
useReducer减速机
函数式组件中模块redux的数据流,一个useReducer可以替代多个useState,
即一次性定义多个状态,项目中用的不多
const [state,dispath/foreUpdate/setState]=useReducer(reducer,{
初始值
})
import { useState, useEffect, useReducer } from "react"
// reducer --- 管理员,要修改状态必须通过reducer
// state --- 状态
// action --- 信号,根据不同的信号,就可以针对性地修改状态
// 管理员根据信号进行状态的修改
// 修改状态的流程:1)对于state进行深copy 2)修改更新state 3)返回修改后的state
const reducer = (state, action) => {
let newState = JSON.parse(JSON.stringify(state)); // 1)对state进行深copy
// 根据信号更新state
switch (action.type) { // action是一个对象,对象中有一个type,不同的type表示不同的信号
case "NUM_ADD":
newState.num += 1
break;
case "NUM_SUB":
newState.num -= 1
break;
}
return newState;
}
// 定义初始值
const initState = {
num: 1,
list: ["a", "b"],
falg: true
}
const A = props => {
// dispatch 派发一个action,管理员就可以收到action
const [state, dispatch] = useReducer(reducer, initState);
return (
<div>
<h2 >函数组件</h2>
<h3>{state.num}</h3>
<h3>{state.list}</h3>
<button onClick={() => dispatch({ type: "NUM_ADD" })}>+1</button>
<button onClick={() => dispatch({ type: "NUM_SUB" })}>-1</button>
</div>
)
}
export default A
useId
V18新增,返回一个唯一的标识,在函数式组件的整个运行过程中都是唯一的
let id=useId()
return (
<div>
<h1>{id}</id>
</div>
)
useDeferredValue递延值
和防抖类似。和真正的防抖区别在于,这个hook所延迟的时间是不确定,由浏览器自己决定。
上面发送ajax,对服务器造成很多的压力,需要防抖,有了useDeferredValue这个hook,我们就不需要实现防抖了,如下: