4W7千字React笔记涵盖方方面面希望大家能喜欢

基本使用

React就是JS,外加一点模板语言JSX

创建React项目

  • vite创建: npm create vite@latest react-demo-vite --template react-ts
  • create-react-app创建: npx create-react-app test-demo --template typescript

JSX语法

  • js的加强版,卸载JS里面,组件的UI结构
  • 语法与HTML相似
  • 已成为ES规范,可用于Vue3

标签

  • 首字母大写表示组件,小写是html原生标签
  • 每段JSX片段只能有一个根节点,即单根节点(vue3可以是多根节点)
  • JSX中空标签表示Fragment,对标vue的template,但是JSX中Fragment可以作为根节点,vue中template不能作为根节点

属性

  • jsx的属性就是html的属性,只有些微区别
  • class要写为className,为了防止和类(class)关键字冲突
  • style要使用Object,且必须是驼峰写法
  • label的for要写为htmlFor,避免与for循环冲突

事件

  • 写法使用onXxx形式
  • 必须传入一个函数(是fn不能是fn的执行结果fn())
  • 注意使用Typescript类型, 如:import type { MouseEvent } from 'react'

react事件和DOM事件的区别

  • react中的event不是原生的event, __proto__.construct 指向 SyntheticEvent ,是一个react定义后的事件对象,译为组合事件 ,模拟DOM事件的所有能力
  • DOM原生事件对象的__proto__.construct一般指向诸如MouseEvent之类的事件对象
  • 如果要获取react事件对应的原生DOM事件,可以通过event.nativeEvent获取
  • react16及之前事件绑定到document上,react17及以后事件绑定到root组件上,有利于多个react版本并存,例如微前端

class组件事件为何bind this

  • 如果没有bind this,会报错误:TypeError:Cannot read property 'setState' of undefined,也就是说this不会正确地指向该class组件
  • 严格模式下this默认指向undefined,因此我们需要bind this让它指向组件
  • 为何不推荐在jsx中执行bind this,因为这样做会多次执行bind事件(一个小的性能优化知识点)
  • 使用箭头函数定义的方法,无需bind this,因为箭头函数地this指向它的上层作用域的this

插值表达式

  • 我们可以使用{ XXX }插入JS变量,函数,表达式
  • 可以插入普通文本,属性
  • 可以用于注释

JSX中使用条件判断

  • 使用&&,适用隐藏显示单个元素
  • 使用三元表达式,适用两个元素的来回切换
  • 使用函数 ,适用多个元素的来回切换,如swith case
js 复制代码
const show = true
let no = 0
no = 2

// 首字母大小,返回一个JSX片段,可以看作是一个自定义组件或自定义标签
const WhoShow = () => {
    switch (no) {
        case 1:
            return <p>1</p>
        case 2:
            return <p>2</p>
        default:
            return <p>0</p>
    }
}

<div>{show && <p>hello</p>}</div>
<div>{show ? <p>nothing</p> : <p>hello</p>}</div>
<WhoShow></WhoShow>

JSX中使用循环

  • 使用数组map
  • 每个item元素都需要一个key ,同vue
  • key同级别唯一
  • 不要使用index 作为key
js 复制代码
const list = [
        { username: 'zhangsan', name: '张三' },
        { username: 'lisi', name: '李四' },
        { username: 'wangmazi', name: '王麻子' },
]

<ol>
    {list.map(user => {
        const { username, name } = user
        return <li key={username}>{name}</li>
    })}
</ol>

组件

  • 组件就是一个UI片段
  • 拥有独立的逻辑和显示
  • 组件可大可小,可嵌套
  • 组件拆分,利于维护,和多人协作开发
  • 可封装公共组件(或第三方组件)复用代码,提高开发效率
  • class组件、函数组件两种定义方式
  • React16 以后,推崇函数组件+hooks,从此,组件就是一个函数

定义class组件

render函数定义我们要渲染到网页上的内容

16.6版本之前:

js 复制代码
import React, { Component, ReactNode } from 'react'

class ClassComponentDemo extends Component<unknown> {
	constructor(props: unknown) {
		super(props)
	}

	render(): ReactNode {
		// 插值表达式
		return (
			<>
				<div>FunctionComponent</div>
			</>
		)
	}
}

export default ClassComponentDemo

从 React 16.6 版本开始,可以不再为 class 组件显式定义 constructor 构造函数

js 复制代码
import React, { Component, ReactNode } from 'react'

class ClassComponentDemo extends Component {
	render(): ReactNode {
		return <div>ClassComponent</div>
	}
}

export default ClassComponentDemo

定义一个函数组件

函数的返回值,视作class组件的render方法

js 复制代码
import React from 'react'
import type { FC } from 'react'

const FunctionComponentDemo: FC = () => {
	return <div>FunctionComponentDemo</div>
}

export default FunctionComponentDemo

state

  • state是组件内部的状态信息(组件的独家记忆),不对外
  • 普通变量变化,组件不更新,state变化,触发组件更新,重新执行render方法,渲染页面(rerender)
  • 不可变 永远都不要去修改state的值,如果需要改变,而是应该完整替换,使内存地址发生变更

class组件的state

  1. 使用this.setState()更新state,禁止 直接更改state,传入全新state, 完全替换

  2. class组件方法内的this需要特别注意!

方式1:显示bind(this)

  1. 在construct内bind(this)
js 复制代码
import React, { Component, ReactNode } from 'react'

type StateType = {
	count: number
}

class ClassComponentDemo extends Component<unknown, StateType> {
	constructor(props: unknown) {
		super(props)
		this.state = {
			count: 0,
		}
		// bind(this)
		this.addCount = this.addCount.bind(this)
	}

	render(): ReactNode {
		// 插值表达式
		return (
			<>
				<div>{this.state.count}</div>
				<div>
					<button onClick={this.addCount}>add</button>
				</div>
			</>
		)
	}

	addCount() {
		console.log(this) // 如果没有 bind(this) 指向undefined 后续会报错找不到state
		this.setState({count: this.state.count + 1})
	}
}
export default ClassComponentDemo
  1. 也可以在绑定事件时再bind(this),但不推荐,为什么?
  • 因为每次更新都会重新执行render方法,每次更新都会执行bind(this)
  • construct只有初始化时执行一次bind(this),所以性能更好
js 复制代码
render(): ReactNode {
    return (
            <>
                    <div>{this.state.count}</div>
                    <div>
                            <button onClick={this.addCount.bind(this)}>add</button>
                    </div>
            </>
    )
}

方式2: 定义方法的时候使用箭头函数, 箭头函数内的this为函数的上级作用域的this

js 复制代码
addCount = () => {
        console.log(this) // 打印组件本身
        this.setState({count: this.count + 1})
}

推荐: 使用箭头函数,简洁

函数组件的state

  1. 函数组件的state需要借助钩子useState的帮助
  2. 使用useState返回的set方法修改state, 禁止 直接更改state,传入全新state,完全替换
js 复制代码
import React, { useState, useEffect } from 'react'
import type { FC } from 'react'

const FunctionComponentDemo: FC = () => {
	const [count, setCount] = useState(0)

	useEffect(() => {
		console.log(count) // 获取最新的count
	}, [count])

	const addCount = () => {
		setCount(count + 1)
		console.log(count) // 这里的count永远是setCount之前的count
	}

	// 插值表达式
	return (
		<>
			<div>{count}</div>
			<div onClick={addCount}>
				<button>add</button>
			</div>
		</>
	)
}

export default FunctionComponentDemo

使用immer释放不可变值的心理负担

  • state是不可变数据
  • 操作成本比较高,有很大的不稳定性,你可能会忘了解构,忘了变更地址
  • 使用immer可避免这一问题

安装:npm install immer --save

js 复制代码
import React, { FC, useState } from 'react'
import produce from 'immer'

const Demo: FC = () => {
	const [userInfo, setUserInfo] = useState<{
		name: string
		age: number
		a?: number
	}>({ name: 'ljx', age: 18 })
	const changeAge = () => {
		// userInfo.age = 21 修改失败,这里是直接对state的值进行修改
		// 传入一个新的值,修改成功
		// userInfo.age = 21
		// setUserInfo({ ...userInfo })

		setUserInfo(
			produce(draft => {
				draft.age = 21
				draft.a = 10
			})
		)
	}
	const [list, setList] = useState(['x', 'y'])
	const changeList = () => {
		// list[2] = 'z' 修改失败,与上面同理
		// const newList = [...list]
		// newList[1] = 'z'
		// setList(newList)

		setList(
			produce(draft => {
				draft[1] = 'z'
			})
		)
	}
	return (
		<div>
			<h2>state 不可变数据</h2>
			<div>{JSON.stringify(userInfo)}</div>
			<button onClick={changeAge}>修改年龄</button>
			<div>{JSON.stringify(list)}</div>
			<button onClick={changeList}>修改数组</button>
		</div>
	)
}

export default Demo

state使用注意点

  1. 无论是哪种组件都不要直接修改state的值,class组件使用this.set()方法修改state,Function组件使用useState()返回的set方法修改state

    为什么?

    组件state被设计为不可直接修改的,这是为了保持React的单向数据流 和组件隔离的原则。

    直接修改state会导致什么问题?

    1. 不触发更新 :如果直接修改状态的值,React无法检测到状态的变化,因此不会触发组件的重新渲染(rerender)。结果是组件的输出不会更新,导致显示的内容不符合预期。

    2. 性能问题 :React通过比较前后状态的差异来判断是否需要重新渲染组件。当直接修改状态值时,React无法确定状态是否发生了变化,因此可能会导致频繁的重新渲染,降低性能。

    3. 可追踪性问题 :React通过setState()方法记录状态的变化历史,这样可以追踪状态的修改,帮助进行调试和排查问题。直接修改状态会失去这种历史记录,使得错误的定位和修复更加困难。

  2. set是异步的,我们要获取改变之后的state,class组件需要通过向setState()传入callback去获取,callback会在rerender之后触发,而Function组件需要借助useEffect()的帮助

  3. set时如果传入的是一个值,不是函数,操作会被合并 如图,每次执行addCount时,都执行5次set,分别点击一次add按钮,结果如下: 可以明显看到,class组件的回调函数被执行了5次,因为每个回调函数的内存地址都不一样

  4. set时如果传入的是一个返回值的函数并且使用该函数提供的prevState操作不会被合并

    为什么?

    使用函数,会将函数传入异步队列,利用eventLoop的机制顺序执行这些函数

  5. 如果一个变量不用于JSX片段请不要使用state来管理它

react 18中的setState的变化

React <= 17时的 setState

  • React组件事件:异步更新 + 合并 state
  • DOM事件,setTimeout: 同步更新,不合并state

React 18 时的 setState

  • React组件事件:异步更新 + 合并state
  • DOM事件,setTimeout: 异步更新 + 合并state
  • Automatic Batching 自动批处理

总结

  • react <=17: 只有react组件事件才批处理
  • react18: 所有事件都自动批处理
  • react18:操作一致,更加简单,降低心智负担

props

完成父子通信

  • 组件是可嵌套的,有层级关系
  • 父组件可以给子组件传递数据
  • 子组件接收数据,并显示数据

父组件:

js 复制代码
import React, { useState } from 'react'
import type { FC } from 'react'
import FunctionComponentDemo from './FunctionComponentDemo'
import ClassComponentDemo from './ClassComponentDemo'
import styles from './index.module.scss'
import classnames from 'classnames'

const BaseUseDemo: FC = () => {
	const [count, setCount] = useState(0)

	const [isRedBordedr, setIsRedBordedr] = useState(false)
	const [isYellowBackground, setIsYellowBackground] = useState(false)
	const [isBlueColor, setIsBlueColor] = useState(false)

	const dynamicClassName = classnames({
		[styles['container']]: true,
		[styles['red-border']]: isRedBordedr,
		[styles['yellow-background']]: isYellowBackground,
		[styles['blue-color']]: isBlueColor,
	})

	const toggleBorderStyle = () => {
		setIsRedBordedr(!isRedBordedr)
		setIsYellowBackground(!isYellowBackground)
		setIsBlueColor(!isBlueColor)
	}

	const countAdd = () => {
		setCount(count + 1)
	}

	// 1. 传递普通数据,不会rerender
	let message = '0'
	const sendMessage = () => {
		message = Math.random() * 998 + ''
	}

	// 传递state数据,可以触发rerender
	// const [message, setMessage] = useState('0')
	// const sendMessage = () => {
	// 	setMessage(Math.random() * 998 + '')
	// }

	// 2. 传递组件给子组件渲染
	const comp = (
		<ul>
			<li>我是父组件排下来巡查的</li>
		</ul>
	)

	// 3. 传递普通函数给子组件执行
	const [classCompMsg, setClassCompMsg] = useState('无msg')
	const [funcCompMsg, setFuncCompMsg] = useState('无msg')

	// 传递函数给子组件,接收子组件传递的函数
	let addFuncCompCount: () => void
	const getFuncCompAddCount = (callback: () => void) => {
		addFuncCompCount = callback
	}

	let addClassCompCount: () => void
	const getClassCompAddCount = (callback: () => void) => {
		addClassCompCount = callback
	}

	return (
		<div className={dynamicClassName}>
			<div className={styles.item}>
				<h3>function 组件</h3>
				<div>
					<FunctionComponentDemo
						msg={message}
						addParentCount={countAdd}
						setParentMsg={setFuncCompMsg}
						comp={comp}
						transCallback={getFuncCompAddCount}
					/>
				</div>
			</div>
			<div className={styles.item}>
				<div>class组件消息:{classCompMsg}</div>
				<div>function组件消息:{funcCompMsg}</div>
				<div>count: {count}</div>
				<button onClick={sendMessage}>sendMessage</button>
				<button
					onClick={() => {
						addClassCompCount()
					}}
				>
					addClassCompCount
				</button>
				<button
					onClick={() => {
						addFuncCompCount()
					}}
				>
					addFuncCompCount
				</button>
				{/* <button onClick={toggleBorderStyle}>显示/隐藏红边框</button> */}
			</div>
			<div className={styles.item}>
				<h3>class 组件</h3>
				<div>
					<ClassComponentDemo
						msg={message}
						addParentCount={countAdd}
						setParentMsg={setClassCompMsg}
						comp={comp}
						transCallback={getClassCompAddCount}
					/>
				</div>
			</div>
		</div>
	)
}

export default BaseUseDemo

Class子组件:

js 复制代码
import React, { Component, ReactNode } from 'react'

type StateType = {
	count: number
}

type PropsType = {
	msg: string
	addParentCount: () => void
	setParentMsg: (msg: string) => void
	comp: JSX.Element
	transCallback: (callback: () => void) => void
}

class ClassComponentDemo extends Component<PropsType, StateType> {
	constructor(props: PropsType) {
		super(props)
		this.state = {
			count: 0,
		}
		console.log('111')
		// bind(this)
		this.addCount = this.addCount.bind(this)

		// 传递 方法 给父组件
		props.transCallback(this.addCount)
	}

	render(): ReactNode {
		// 插值表达式
		return (
			<>
				<div>{this.state.count}</div>
				<div>
					<button onClick={this.addCount}>add</button>
				</div>

				{/* 使用父组件传递的数据 */}
				<div>parentMsg:{this.props.msg}</div>

				<div>
					{/* 调用父组件传递的方法 */}
					<button onClick={this.props.addParentCount}>addParentCount</button>

					{/* 传递数据给父组件 */}
					<button
						onClick={() => {
							this.props.setParentMsg('这是Class组件设置的信息')
						}}
					>
						setParentMsg
					</button>

					{/* 渲染父组件传递的组件 */}
					<>渲染父组件传递的组件:{this.props.comp}</>
				</div>
			</>
		)
	}

	addCount() {
		console.info(this) // 如果没有 bind(this) 指向undefined 后续会报错找不到state
		this.setState(
			prevState => ({ count: prevState.count + 1 }),
			() => {
				console.log(this.state.count) // 最新的state
			}
		)
		console.info(this.state.count) // 这里的state永远是setState之前的state
	}
        
        componentDidUpdate(
		prevProps: Readonly<PropsType>,
		prevState: Readonly<StateType>,
		snapshot?: any
	): void {
		// 更新后传递 方法 给父组件
		this.props.transCallback(this.addCount)
	}
}
export default ClassComponentDemo

函数子组件:

js 复制代码
import React, { useState, useEffect } from 'react'
import type { FC } from 'react'

type PropsType = {
	msg: string
	addParentCount: () => void
	setParentMsg: (msg: string) => void
	comp: JSX.Element
	transCallback: (callback: () => void) => void
}

const FunctionComponentDemo: FC<PropsType> = props => {
	const [count, setCount] = useState(0)

	useEffect(() => {
		console.log(count) // 获取最新的count
	}, [count])
	const addCount = () => {
		setCount(prevCount => prevCount + 1)
	}

	// 传递 方法 给父组件
	props.transCallback(addCount)

	// 插值表达式
	return (
		<>
			<div>{count}</div>
			<div>
				<button onClick={addCount}>add</button>
			</div>

			{/* 使用父组件传递的数据 */}
			<div>parentMsg:{props.msg}</div>

			<div>
				{/* 调用父组件传递的方法 */}
				<button onClick={props.addParentCount}>addParentCount</button>

				{/* 传递数据给父组件 */}
				<button
					onClick={() => {
						props.setParentMsg('这是Function组件设置的信息')
					}}
				>
					setParentMsg
				</button>

				{/* 渲染父组件传递的组件 */}
				<>渲染父组件传递的组件:{props.comp}</>
			</div>
		</>
	)
}

export default FunctionComponentDemo

props的3种应用:

  1. 传递数据

    1-1. 传递普通数据,数据变化时,不会引起子组件rerender

    js 复制代码
    // 1. 传递普通数据,不会rerender
    let message = '0'
    const sendMessage = () => {
            message = Math.random() * 998 + ''
    }
    <FunctionComponentDemo  msg={message}  />
    <ClassComponentDemo  msg={message}  />

    1-2. 传递状态,状态变化时,引起子组件rerender

    js 复制代码
    // 传递state数据,可以触发rerender
    const [message, setMessage] = useState('0')
    const sendMessage = () => {
            setMessage(Math.random() * 998 + '')
    }
    <FunctionComponentDemo  msg={message}  />
    <ClassComponentDemo  msg={message}  />
  2. 传递组件给子组件,子组件渲染该组件

    2-1. 自定义属性传递,组件内通过该属性获取而后渲染

    js 复制代码
    // 2. 传递组件给子组件渲染
    const comp = (
            <ul>
                    <li>我是父组件排下来巡查的</li>
            </ul>
    )
    <FunctionComponentDemo  comp={comp}  />
    <ClassComponentDemo comp={comp}  />
    
    // 子组件
    <>渲染父组件传递的组件:{this.props.comp}</>

    2-2. 也可以直接插入,组件内部通过props.children获取而后渲染

    js 复制代码
    <FunctionComponentDemo  comp={comp}>
            <div>123123</div>
    </FunctionComponentDemo>
    
    type PropsType = {
            msg: string
            addParentCount: () => void
            setParentMsg: (msg: string) => void
            comp: JSX.Element
            transCallback: (callback: () => void) => void
            children: JSX.Element
    }
    const FunctionComponentDemo: FC<PropsType> = props => {
            return(
                    <div>
                            {/* 渲染父组件传递的组件 */}
                            <>渲染父组件传递的组件:{props.comp}</>
                            <>{props.children}</>
                    </div>
            )
    }
  3. 传递函数给子组件

    3-1. 传递父组件的方法给子组件使用

    js 复制代码
    const [count, setCount] = useState(0)
    const countAdd = () => {
            setCount(count + 1)
    }
    
    // 3. 传递普通函数给子组件执行
    const [classCompMsg, setClassCompMsg] = useState('无msg')
    const [funcCompMsg, setFuncCompMsg] = useState('无msg')
    
    <FunctionComponentDemo  setParentMsg={setFuncCompMsg} addParentCount={countAdd}  />
    <ClassComponentDemo  setParentMsg={setClassCompMsg} addParentCount={countAdd}  />

    3-2. 传递函数给子组件,子组件调用该函数,传递自身的方法给父组件使用, 同理,也能传数据给父组件

    js 复制代码
    // 传递函数给子组件,接收子组件传递的函数
    let addFuncCompCount: () => void
    const getFuncCompAddCount = (callback: () => void) => {
            addFuncCompCount = callback
    }
    
    let addClassCompCount: () => void
    const getClassCompAddCount = (callback: () => void) => {
            addClassCompCount = callback
    }
    
    <FunctionComponentDemo  transCallback={getFuncCompAddCount}  />
    <ClassComponentDemo  transCallback={getClassCompAddCount}  />

    class组件:首次执行时在construct内传递方法给父组件,更新时还需要在componentDidUpdate传递方法给父组件

建议:如果传递值非常多,可以使用解构的方式,使代码更简洁

组件生命周期

在React中,组件具有一组生命周期方法,它们在组件的不同阶段会被自动调用。下面分别论述Class组件及Function组件的生命周期方法和它们的执行顺序

class组件生命周期

  1. 挂载阶段(Mounting Phase):组件被实例化并插入到DOM中。

    • constructor(props) 在组件被创建时调用,用于初始化组件的状态(state)和绑定事件处理方法。
    • render() 必需方法,在该方法中返回JSX片段。
    • componentDidMount() 在组件首次渲染之后调用,可以执行一些副作用操作,如访问DOM、发送网络请求等。
  2. 更新阶段(Updating Phase) :组件的props或state发生变化,导致组件重新渲染或更新。

    • shouldComponentUpdate(nextProps, nextState) 在组件更新之前调用,用于决定是否需要进行重渲染,默认情况下总是返回true。
    • render()
    • componentDidUpdate(prevProps, prevState, snapshot) 在组件更新之后调用,可以执行一些副作用操作。
  3. 卸载阶段(Unmounting Phase):组件从DOM中被移除。

    • componentWillUnmount() 在组件被卸载和销毁之前调用,可以进行一些清理操作,如取消订阅、清除计时器等。
  4. 错误处理阶段(Error Handling Phase):组件在渲染期间、生命周期方法中发生错误。

    • componentDidCatch(error, info) 捕获在后代组件中生成的异常。未处理的异常将导致整个组件树卸载。

函数组件'生命周期'

严格意义来说,函数组件本身就只是个纯函数没有生命周期,但我们可以借助useEffect

useEffect: 参数1-effect:useEffect钩子触发时的执行函数,该函数可以返回一个函数,如果返回函数,则该返回函数会在组件时调用(模拟componentWillUnmount)

参数2-deps: 依赖项(非必传)

  1. 当该参数为[](空数组)时,模拟componentDidMount,在组件挂载完成时执行参数1
  2. 当该参数为有依赖项的数组时,模拟componentDidMount,在组件挂载完成时及数组内的依赖项发生变化时(模拟特定于这些依赖项的componentDidUpdate)执行参数1
  3. 当该参数未传为undefined、null时,模拟componentDidMount,模拟componentDidUpdate,组件挂载完成时及任何状态发生改变的时候都会执行参数1

那么哪些东西可以作为依赖项呢?

  1. 当前组件的state

  2. props, 前提得是父组件传递过来的state, 具体例子可以看上面父组件通过props传递普通数据和状态的区别,普通数据不能作为依赖项,它不会引起rerender

  3. 在组件中声明的会因为重新渲染而改变的变量都可以作为依赖项,可以视为具有了状态

    例子1:i变化的时候并不会执行effect函数,因为i改变,不会引起重新渲染,不能作为依赖项,不会产生副作用,所以不会执行

    js 复制代码
    let i = 0
    
    useEffect(() => {
            console.log(i)
    }, [i])
    
    setInterval(() => {
            i++
    }, 1000)

    例子2:url的query可以作为依赖项, 因为query改变能够引起rerender

    js 复制代码
    let query = window.location.search
    
    useEffect(() => {
            console.log('qeury:', query)
    }, [query])
    
    setInterval(() => {
            query += '1'
            // query能否成为依赖项取决于下面这行代码能否启用!
            // window.location.search = query
    }, 1000)

重点:能够引起重新渲染 的变量才能作为依赖项 ,state也是因为这个原因

tip:如果写的过程中,不清楚能否作为依赖项,那就去尝试改变这个变量是否能引起重新渲染,能够引起那就可以

如何获取组件实例、DOM元素? ------ ref

在React中,通过使用 ref 来获取组件实例。 ref 是一个可以引用React组件、DOM元素或其他对象的特殊属性。

  • ref用于操作DOM,而不触发rerender,避免性能的浪费
  • 也可传入普通JS变量,但更新不会触发rerender
  • class组件使用createRef()
  • 函数组件使用useRef()钩子

class组件

js 复制代码
import React, { Component, createRef } from 'react'
import type { ReactNode, RefObject } from 'react'

class ClassComponentDemo extends Component<PropsType, StateType> {
	private btnRef: RefObject<HTMLButtonElement> = createRef()

	render(): ReactNode {
		console.info('render')
		// 插值表达式
		return (
			<>
                                <button ref={this.btnRef} onClick={this.addCount}>
                                        add
                                </button>
			</>
		)
	}

	componentDidMount(): void {
		console.info('componentDidMount')
		console.dir(this.btnRef.current)
	}
}
export default ClassComponentDemo

小技巧:可以使用console.dir代替console.log,console.info,打印DOM更友好

函数组件

js 复制代码
import React, { useEffect, useRef, useState } from 'react'
import type { FC } from 'react'
import ClassComponentDemo from './ClassComponentDemo'

const BaseUseDemo: FC = () => {
	const ClassComponentDemoRef = useRef<ClassComponentDemo>(null)

	useEffect(() => {
		console.dir(ClassComponentDemoRef.current)
	}, [ClassComponentDemoRef])
        
        return (<ClassComponentDemo
                        ref={ClassComponentDemoRef}
                        msg={message}
                        addParentCount={countAdd}
                        setParentMsg={setClassCompMsg}
                        comp={comp}
                        transCallback={getClassCompAddCount}
                />)
}

export default BaseUseDemo

使用useRef解决闭包陷阱

  • 当异步函数获取state时,可能不是当前最新的state
  • 可使用useRef来解决

当只使用useState时:

使用useRef后:

js 复制代码
import React, { useEffect, useState, useRef, FC } from 'react'

const Demo: FC = () => {
	const [count, setCount] = useState(0)
	const countRef = useRef(0)

	useEffect(() => {
		countRef.current = count
	}, [count])

	const add = () => {
		setCount(prev => prev + 1)
	}

	const alertFn = () => {
		setTimeout(() => {
			// alert(count)
			alert(countRef.current)
		}, 3000)
	}

	return (
		<>
			<p>闭包陷阱</p>
			<span>{count}</span>
			<button onClick={add}>add</button>
			<button onClick={alertFn}>alert</button>
		</>
	)
}

export default Demo

思考:为什么会产生这样的问题?

  • 使用useState这个函数时,会生成getter,setter,返回一个函数,与一个状态,而这个函数与状态都是被保存在useState这个闭包函数 里,不受外界干扰,alert(count)时的count来源于当时执行rerender时调用的函数组件本身,而这个组件函数里面又调用了useState获取到了当时的count。

  • 所以当我们点击 alert时useState吐出的count是多少,alert(count)几秒后弹出的也是多少,count指向点击时保存它的那个useState函数作用域内。

  • 使用useRef能够获取到最新的值是因为useRef并不是一个闭包函数,无论组件函数被重新rerender重新执行多少次,都没有生成新的作用域,始终都指向同一个作用域。

表单

React中处理表单有两种两种不同方式受控组件 和非受控组件

  • 受控组件 : 在受控组件中,组件的状态(例如输入框的值)由 React组件的state管理。当用户输入数据时,React组件会更新state,并通过属性将新的值传递给输入框。这样,state始终由 React控制。

  • 非受控组件 :在非受控组件中,组件的状态并不由React组件管理。相反,DOM元素本身维护其自己的状态。通常,我们会使用 ref 来获取DOM元素的值。

再次重申:label的for要写为htmlFor

受控组件

受控组件 将表单元素交由react进行状态控制,通过value绑定状态,当发生交互事件时,更新状态,因此它是value + 事件处理,与vue的自定义v-model原理相似

class组件

js 复制代码
import React, { Component } from 'react'
import type { ReactNode, ChangeEventHandler } from 'react'
import { produce } from 'immer'

type StateType = {
	inputVal: string
	textareaVal: string
	selecteadList: Array<string>
	gender: string
	lang: string
}

class ClassComponentDemo extends Component<unknown, StateType> {
	constructor(props: unknown) {
		super(props)
		this.state = {
			inputVal: '',
			textareaVal: '',
			selecteadList: [],
			gender: 'male',
			lang: '',
		}
	}

	render(): ReactNode {
		return (
			<form aria-disabled>
				{/* input受控 */}
				<div>
					输入框:
					<input type="text" value={this.state.inputVal} onChange={this.inputValChange} />
				</div>
				<div>
					文本域:
					<textarea
						value={this.state.textareaVal}
						onChange={this.textareaValChange}
						cols={30}
						rows={10}
					></textarea>
				</div>

				{/* 多选受控 */}
				<div>
					<label htmlFor="checkbox1">北京</label>
					<input
						type="checkbox"
						id="checkbox1"
						value="beijing"
						checked={this.state.selecteadList.includes('beijing')}
						onChange={this.multipleChoiceChange}
					/>
					<label htmlFor="checkbox2">上海</label>
					<input
						type="checkbox"
						id="checkbox2"
						value="shanghai"
						checked={this.state.selecteadList.includes('shanghai')}
						onChange={this.multipleChoiceChange}
					/>
					<label htmlFor="checkbox3">广州</label>
					<input
						type="checkbox"
						id="checkbox3"
						value="guangzhou"
						checked={this.state.selecteadList.includes('guangzhou')}
						onChange={this.multipleChoiceChange}
					/>
					<label htmlFor="checkbox4">深圳</label>
					<input
						type="checkbox"
						id="checkbox4"
						value="shenzhen"
						checked={this.state.selecteadList.includes('shenzhen')}
						onChange={this.multipleChoiceChange}
					/>
					<div>{this.state.selecteadList.toString()}</div>
				</div>

				{/* 单选受控 */}
				<div>
					<label htmlFor="radio1">男</label>
					<input
						type="radio"
						id="radio1"
						name="gender"
						value="male"
						checked={this.state.gender === 'male'}
						onChange={this.singleChoiceChange}
					/>
					<label htmlFor="radio2">女</label>
					<input
						type="radio"
						id="radio2"
						name="gender"
						value="female"
						checked={this.state.gender === 'female'}
						onChange={this.singleChoiceChange}
					/>
				</div>

				{/* select受控 */}
				<div>
					<select value={this.state.lang} onChange={this.selectorChange}>
						<option value="java">JAVA</option>
						<option value="js">JS</option>
						<option value="css">CSS</option>
						<option value="html">HTML</option>
					</select>
				</div>

				<button
					type="button"
					onClick={() => {
						console.log(this.state)
					}}
				>
					提交
				</button>
			</form>
		)
	}

	private inputValChange: ChangeEventHandler<HTMLInputElement> = e => {
		console.log(e.target.value)
		this.setState(
			produce<StateType>(draft => {
				draft.inputVal = e.target.value
			})
		)
	}

	private textareaValChange: ChangeEventHandler<HTMLTextAreaElement> = e => {
		console.log(e.target.value)
		this.setState(
			produce<StateType>(draft => {
				draft.textareaVal = e.target.value
			})
		)
	}

	private multipleChoiceChange: ChangeEventHandler<HTMLInputElement> = e => {
		console.log(e.target.value)
		this.setState(
			produce<StateType>(draft => {
				if (!this.state.selecteadList.includes(e.target.value)) {
					draft.selecteadList.push(e.target.value)
				} else {
					draft.selecteadList = this.state.selecteadList.filter(c => c !== e.target.value)
				}
			}),
			() => {
				console.log(this.state.selecteadList)
			}
		)
	}

	private singleChoiceChange: ChangeEventHandler<HTMLInputElement> = e => {
		console.log(e.target.value)
		this.setState(
			produce<StateType>(draft => {
				draft.gender = e.target.value
			})
		)
	}

	private selectorChange: ChangeEventHandler<HTMLSelectElement> = e => {
		console.log(e.target.value)
		this.setState(
			produce<StateType>(draft => {
				draft.lang = e.target.value
			})
		)
	}
}

export default ClassComponentDemo

函数组件:

js 复制代码
import React, { useState } from 'react'
import type { FC, ChangeEventHandler } from 'react'
import { produce } from 'immer'

type StateType = {
	inputVal: string
	textareaVal: string
	selecteadList: Array<string>
	gender: string
	lang: string
}

const FunctionComponentDemo: FC<unknown> = () => {
	const [state, setState] = useState<StateType>({
		inputVal: '',
		textareaVal: '',
		selecteadList: [],
		gender: 'male',
		lang: '',
	})

	const inputValChange: ChangeEventHandler<HTMLInputElement> = e => {
		console.log(e.target.value)
		setState(
			produce<StateType>(draft => {
				draft.inputVal = e.target.value
			})
		)
	}

	const textareaValChange: ChangeEventHandler<HTMLTextAreaElement> = e => {
		console.log(e.target.value)
		setState(
			produce<StateType>(draft => {
				draft.textareaVal = e.target.value
			})
		)
	}

	const multipleChoiceChange: ChangeEventHandler<HTMLInputElement> = e => {
		console.log(e.target.value)
		setState(
			produce<StateType>(draft => {
				if (!state.selecteadList.includes(e.target.value)) {
					draft.selecteadList.push(e.target.value)
				} else {
					draft.selecteadList = state.selecteadList.filter(c => c !== e.target.value)
				}
			})
		)
	}

	const singleChoiceChange: ChangeEventHandler<HTMLInputElement> = e => {
		console.log(e.target.value)
		setState(
			produce<StateType>(draft => {
				draft.gender = e.target.value
			})
		)
	}

	const selectorChange: ChangeEventHandler<HTMLSelectElement> = e => {
		console.log(e.target.value)
		setState(
			produce<StateType>(draft => {
				draft.lang = e.target.value
			})
		)
	}

	// 插值表达式
	return (
		<form aria-disabled>
			{/* input受控 */}
			<div>
				输入框:
				<input type="text" value={state.inputVal} onChange={inputValChange} />
			</div>
			<div>
				文本域:
				<textarea
					value={state.textareaVal}
					onChange={textareaValChange}
					cols={30}
					rows={10}
				></textarea>
			</div>

			{/* 多选受控 */}
			<div>
				<label htmlFor="checkbox1">北京</label>
				<input
					type="checkbox"
					id="checkbox1"
					value="beijing"
					checked={state.selecteadList.includes('beijing')}
					onChange={multipleChoiceChange}
				/>
				<label htmlFor="checkbox2">上海</label>
				<input
					type="checkbox"
					id="checkbox2"
					value="shanghai"
					checked={state.selecteadList.includes('shanghai')}
					onChange={multipleChoiceChange}
				/>
				<label htmlFor="checkbox3">广州</label>
				<input
					type="checkbox"
					id="checkbox3"
					value="guangzhou"
					checked={state.selecteadList.includes('guangzhou')}
					onChange={multipleChoiceChange}
				/>
				<label htmlFor="checkbox4">深圳</label>
				<input
					type="checkbox"
					id="checkbox4"
					value="shenzhen"
					checked={state.selecteadList.includes('shenzhen')}
					onChange={multipleChoiceChange}
				/>
				<div>{state.selecteadList.toString()}</div>
			</div>

			{/* 单选受控 */}
			<div>
				<label htmlFor="radio1">男</label>
				<input
					type="radio"
					id="radio1"
					name="gender"
					value="male"
					checked={state.gender === 'male'}
					onChange={singleChoiceChange}
				/>
				<label htmlFor="radio2">女</label>
				<input
					type="radio"
					id="radio2"
					name="gender"
					value="female"
					checked={state.gender === 'female'}
					onChange={singleChoiceChange}
				/>
			</div>

			{/* select受控 */}
			<div>
				<select value={state.lang} onChange={selectorChange}>
					<option value="java">JAVA</option>
					<option value="js">JS</option>
					<option value="css">CSS</option>
					<option value="html">HTML</option>
				</select>
			</div>

			<button
				type="button"
				onClick={() => {
					console.log(state)
				}}
			>
				提交
			</button>
		</form>
	)
}

export default FunctionComponentDemo

非受控组件

非受控组件 讲究的是不由react进行状态控制,原生的DOM已经自己记录下了表单元素的状态,我们通过传统的操作DOM去解决问题就好,不涉及事件,不涉及value,仅需要默认赋值,因此使用defaultValue、defaultChecked

  • 输入元素使用defaultValue进行默认赋值
  • 选择元素使用defaultChecked进行默认勾选

class组件:

js 复制代码
import React, { Component, createRef } from 'react'
import type { ReactNode, RefObject } from 'react'

class ClassComponentDemo extends Component<unknown> {
	private inputRef: RefObject<HTMLInputElement> = createRef()
	private textareaRef: RefObject<HTMLTextAreaElement> = createRef()
	private mutilpleRef: RefObject<HTMLDivElement> = createRef()
	private radioRef: RefObject<HTMLDivElement> = createRef()
	private selectRef: RefObject<HTMLSelectElement> = createRef()

	render(): ReactNode {
		return (
			<form>
				{/* input非受控 */}
				<div>
					输入框:
					<input type="text" ref={this.inputRef} />
				</div>
				<div>
					文本域:
					<textarea
						cols={30}
						ref={this.textareaRef}
						rows={10}
						defaultValue="默认文本作用域"
					></textarea>
				</div>

				{/* 多选非受控 */}
				<div ref={this.mutilpleRef}>
					<label htmlFor="ClassComponentDemo-checkbox1">北京</label>
					<input type="checkbox" id="ClassComponentDemo-checkbox1" value="beijing" />
					<label htmlFor="ClassComponentDemo-checkbox2">上海</label>
					<input type="checkbox" id="ClassComponentDemo-checkbox2" value="shanghai" />
					<label htmlFor="ClassComponentDemo-checkbox3">广州</label>
					<input
						type="checkbox"
						id="ClassComponentDemo-checkbox3"
						value="guangzhou"
						defaultChecked
					/>
					<label htmlFor="ClassComponentDemo-checkbox4">深圳</label>
					<input
						type="checkbox"
						id="ClassComponentDemo-checkbox4"
						value="shenzhen"
						defaultChecked
					/>
				</div>

				{/* 单选非受控 */}
				<div ref={this.radioRef}>
					<label htmlFor="ClassComponentDemo-radio1">男</label>
					<input type="radio" id="ClassComponentDemo-radio1" name="gender" value="male" />
					<label htmlFor="ClassComponentDemo-radio2">女</label>
					<input
						type="radio"
						id="ClassComponentDemo-radio2"
						name="gender"
						value="female"
						defaultChecked
					/>
				</div>

				{/* select非受控 */}
				<div>
					<select ref={this.selectRef} defaultValue="html">
						<option value="java">JAVA</option>
						<option value="js">JS</option>
						<option value="css">CSS</option>
						<option value="html">HTML</option>
					</select>
				</div>

				<button type="button" onClick={this.handleSubmit}>
					提交
				</button>
			</form>
		)
	}

	private handleSubmit = () => {
		const submitObj: Record<string, any> = {}
		if (this.mutilpleRef.current) {
			submitObj.selecteadList = Array.from(this.mutilpleRef.current.children)
				.filter((el: any) => {
					if (el.tagName !== 'INPUT') return false
					if (el.checked) return true
					return false
				})
				.map((el: any) => el.value)
		}

		if (this.radioRef.current) {
			const choice: any = Array.from(this.radioRef.current.children).find((el: any) => {
				if (el.tagName === 'INPUT' && el.checked === true) return el
			})
			if (choice) submitObj.gender = choice.value
		}
		submitObj.inputVal = this.inputRef.current?.value
		submitObj.textareaVal = this.textareaRef.current?.value
		submitObj.lang = this.selectRef.current?.value

		console.dir(submitObj)
	}
}

export default ClassComponentDemo

函数组件:

js 复制代码
import React, { useRef } from 'react'
import type { FC, RefObject } from 'react'

const FunctionComponentDemo: FC<unknown> = () => {
	const inputRef: RefObject<HTMLInputElement> = useRef(null)
	const textareaRef: RefObject<HTMLTextAreaElement> = useRef(null)
	const mutilpleRef: RefObject<HTMLDivElement> = useRef(null)
	const radioRef: RefObject<HTMLDivElement> = useRef(null)
	const selectRef: RefObject<HTMLSelectElement> = useRef(null)

	const handleSubmit = () => {
		const submitObj: Record<string, any> = {}
		if (mutilpleRef.current) {
			submitObj.selecteadList = Array.from(mutilpleRef.current.children)
				.filter((el: any) => {
					if (el.tagName !== 'INPUT') return false
					if (el.checked) return true
					return false
				})
				.map((el: any) => el.value)
		}

		if (radioRef.current) {
			const choice: any = Array.from(radioRef.current.children).find((el: any) => {
				if (el.tagName === 'INPUT' && el.checked === true) return el
			})
			if (choice) submitObj.gender = choice.value
		}
		submitObj.inputVal = inputRef.current?.value
		submitObj.textareaVal = textareaRef.current?.value
		submitObj.lang = selectRef.current?.value

		console.dir(submitObj)
	}

	// 插值表达式
	return (
		<form>
			{/* input非受控 */}
			<div>
				输入框:
				<input ref={inputRef} type="text" defaultValue="默认值" />
			</div>
			<div>
				文本域:
				<textarea ref={textareaRef} cols={30} rows={10}></textarea>
			</div>

			{/* 多选非受控 */}
			<div ref={mutilpleRef}>
				<label htmlFor="FunctionComponentDemo-checkbox1">北京</label>
				<input type="checkbox" id="FunctionComponentDemo-checkbox1" value="beijing" />
				<label htmlFor="FunctionComponentDemo-checkbox2">上海</label>
				<input
					type="checkbox"
					id="FunctionComponentDemo-checkbox2"
					defaultChecked
					value="shanghai"
				/>
				<label htmlFor="FunctionComponentDemo-checkbox3">广州</label>
				<input type="checkbox" id="FunctionComponentDemo-checkbox3" value="guangzhou" />
				<label htmlFor="FunctionComponentDemo-checkbox4">深圳</label>
				<input type="checkbox" id="FunctionComponentDemo-checkbox4" value="shenzhen" />
			</div>

			{/* 单选非受控 */}
			<div ref={radioRef}>
				<label htmlFor="FunctionComponentDemo-radio1">男</label>
				<input
					type="radio"
					id="FunctionComponentDemo-radio1"
					name="gender"
					value="male"
					defaultChecked
				/>
				<label htmlFor="FunctionComponentDemo-radio2">女</label>
				<input type="radio" id="FunctionComponentDemo-radio2" name="gender" value="female" />
			</div>

			{/* select非受控 */}
			<div>
				<select ref={selectRef} defaultValue="js">
					<option value="java">JAVA</option>
					<option value="js">JS</option>
					<option value="css">CSS</option>
					<option value="html">HTML</option>
				</select>
			</div>

			<button type="button" onClick={handleSubmit}>
				提交
			</button>
		</form>
	)
}

export default FunctionComponentDemo

小结

分别使用受控组件 和非受控组件 以class组件、function组件的方式实现了同一个表单,相互对比下,可以看出来非受控组件 的代码更简洁,代码量几乎只有受控组件 的60%左右,实现上也显得不是那么的react

非受控组件 可以解决一些受控组件 无法解决的场景,比如:一些必须手动操作DOM场景,文件选择,所以真实开发中是会出现混合使用的情况的

  • 工作中优先使用受控组件 ,更符合react设计原则
  • 必须操作DOM时再使用非受控组件

样式方案

元素内联

  • 和HTML元素的style类似
  • 但必须是JS对象的写法,不能是字符串
  • 样式名要用驼峰式写法,如fontSize
  • 尽量不要用内联style,代码多,性能差,扩展性不好

举例:

js 复制代码
<div
    style={{
            height: '100vh',
            margin: 0,
            padding: '2px',
            boxSizing: 'border-box',
    }}
>
        <BaseUseDemo />
</div>

普通引入方案

index.tsx:

jsx 复制代码
import React from 'react'
import type { FC } from 'react'
import FunctionComponentDemo from './FunctionComponentDemo'
import ClassComponentDemo from './ClassComponentDemo'
import './index.scss'

const BaseUseDemo: FC = () => {
	return (
		<div className="container red-border">
			<div className="item">
				<h3>function 组件</h3>
				<div>
					<FunctionComponentDemo />
				</div>
			</div>
			<div className="item">
				<h3>class 组件</h3>
				<div>
					<ClassComponentDemo />
				</div>
			</div>
		</div>
	)
}

export default BaseUseDemo

index.scss

scss 复制代码
.container {
  display: flex;
  box-sizing: border-box;
  border: 1px solid #eee;
  height: 100%;
  .item {
    width: 50%;
    padding: 8px;
    box-sizing: border-box;
    border: 1px solid #eee;
  }
}

.red-border {
  border-color: red;
}

萌新常见错误

下面这种写法看似正确,实则无法改变样式,为什么?

因为虽然改变了dynamicClassName这个变量,但它本身并不具备响应式,不会产生任何副作用,react不知道需要重新渲染,所以不生效:

js 复制代码
import React from 'react'
import type { FC } from 'react'
import FunctionComponentDemo from './FunctionComponentDemo'
import ClassComponentDemo from './ClassComponentDemo'
import './index.scss'

const BaseUseDemo: FC = () => {
	let dynamicClassName = 'container'

	const toggleBorderStyle = () => {
		if (dynamicClassName === 'container') {
			dynamicClassName = 'container red-border'
		} else {
			dynamicClassName = 'container'
		}
	}

	return (
		<div className={dynamicClassName}>
			<div className="item">
				<h3>function 组件</h3>
				<div>
					<FunctionComponentDemo />
				</div>
			</div>
			<div className="item">
				<button onClick={toggleBorderStyle}>显示/隐藏红边框</button>
			</div>
			<div className="item">
				<h3>class 组件</h3>
				<div>
					<ClassComponentDemo />
				</div>
			</div>
		</div>
	)
}

export default BaseUseDemo

我们需要借助响应式状态改变,触发rerender:

js 复制代码
import React, { useState } from 'react'
import type { FC } from 'react'
import FunctionComponentDemo from './FunctionComponentDemo'
import ClassComponentDemo from './ClassComponentDemo'
import './index.scss'

const BaseUseDemo: FC = () => {
	const [dynamicClassName, setDaynamicClassName] = useState('container')

	const toggleBorderStyle = () => {
		if (dynamicClassName === 'container') {
			setDaynamicClassName('container red-border')
		} else {
			setDaynamicClassName('container')
		}
	}

	return (
		<div className={dynamicClassName}>
			<div className="item">
				<h3>function 组件</h3>
				<div>
					<FunctionComponentDemo />
				</div>
			</div>
			<div className="item">
				<button onClick={toggleBorderStyle}>显示/隐藏红边框</button>
			</div>
			<div className="item">
				<h3>class 组件</h3>
				<div>
					<ClassComponentDemo />
				</div>
			</div>
		</div>
	)
}

export default BaseUseDemo

缺点: 现在只是涉及到2个class进行组合,还可以很好y应对动态拼接,如果需要对多个class进行同时控制呢?

实现多class组合的2个lib

  1. classnames

安装:npm install classnames

函数组件使用:

js 复制代码
import React, { useState } from 'react'
import type { FC } from 'react'
import FunctionComponentDemo from './FunctionComponentDemo'
import ClassComponentDemo from './ClassComponentDemo'
import './index.scss'
import classnames from 'classnames'

const BaseUseDemo: FC = () => {
	const [isRedBordedr, setIsRedBordedr] = useState(false)
	const [isYellowBackground, setIsYellowBackground] = useState(false)
	const [isBlueColor, setIsBlueColor] = useState(false)

	const dynamicClassName = classnames('container', {
		'red-border': isRedBordedr,
		'yellow-background': isYellowBackground,
		'blue-color': isBlueColor,
	})

	const toggleBorderStyle = () => {
		setIsRedBordedr(!isRedBordedr)
		setIsYellowBackground(!isYellowBackground)
		setIsBlueColor(!isBlueColor)
	}

	return (
		<div className={dynamicClassName}>
			<div className="item">
				<h3>function 组件</h3>
				<div>
					<FunctionComponentDemo />
				</div>
			</div>
			<div className="item">
				<button onClick={toggleBorderStyle}>显示/隐藏红边框</button>
			</div>
			<div className="item">
				<h3>class 组件</h3>
				<div>
					<ClassComponentDemo />
				</div>
			</div>
		</div>
	)
}

export default BaseUseDemo

class组件使用:

js 复制代码
import React, { Component } from 'react'

import classnames from 'classnames'
import { produce } from 'immer'
import styles from './index.scss'

type PropsType = unknown

type StateType = {
	dynamicClassName: string
}

class FormUseDemo extends Component<PropsType, StateType> {
	private isRedBordedr = false
	private isYellowBackground = false
	private isBlueColor = false
	constructor(props: unknown) {
		super(props)
		this.state = {
			dynamicClassName: classnames({
				[styles['container']]: true,
				[styles['red-border']]: this.isRedBordedr,
				[styles['yellow-background']]: this.isYellowBackground,
				[styles['blue-color']]: this.isBlueColor,
			}),
		}
	}
	render(): React.ReactNode {
		return (
			<div className={this.state.dynamicClassName}>
				<div className={styles.item}>
					<h3>function 组件 ------ 受控表单</h3>
					<div></div>
				</div>
				<div className={styles.item}>
					<h3>父组件</h3>
					<button onClick={this.toggleBorderStyle}>显示/隐藏样式</button>
				</div>
				<div className={styles.item}>
					<h3>class 组件 ------ 受控表单</h3>
					<div></div>
				</div>
			</div>
		)
	}

	private toggleBorderStyle = () => {
		this.isRedBordedr = !this.isRedBordedr
		this.isYellowBackground = !this.isYellowBackground
		this.isBlueColor = !this.isBlueColor
		this.setState(
			produce<StateType>(draft => {
				draft.dynamicClassName = classnames({
					[styles['container']]: true,
					[styles['red-border']]: this.isRedBordedr,
					[styles['yellow-background']]: this.isYellowBackground,
					[styles['blue-color']]: this.isBlueColor,
				})
			})
		)
	}
}

export default FormUseDemo
  1. clsx

安装:npm install clsx

使用方式与classnames非常接近

使用CSS-Module方案

普通引入样式的问题

  • React使用组件化开发
  • 多个组件,就需要多个样式文件
  • 多个样式文件很容易造成className重复,不好管理

所以我们需要CSS-Module

CSS-Module规则

  • 每个CSS文件都当作单独的模块,命名xxx.module.css
  • 为每个className增加后缀名,不让它们重复,发挥一种类似于vue的scoped的作用
  • Create-React-App原生支持CSS-Module

不同的引入方式,现在我们的css直接被处理成了一个模块,引入的是一个Object,因此使用时是取该对象的属性值:

普通引入样式文件生成的DOM:

使用CSS-Module方案生成的DOM:

使用:

js 复制代码
// import styles from './index.scss'

// 仅需修改为XXX.module.scss即可
import styles from './index.module.scss'

CSS in JS方案

  • 一种解决方案,有好几个工具
  • 在JS中写CSS,带来极大的灵活性
  • 它和内联style完全不一样,也不会有内联style的问题

styled-components工具

安装:npm install --save styled-components

js 复制代码
import React, { FC } from 'react'
import styled, { css } from 'styled-components'

type ButtonPropsType = {
	primary?: boolean
}

const Layout = styled.div`
	display: flex;
	height: 100%;
	width: 100%;
	flex-direction: column;
	justify-content: center;
	align-items: center;
`

// 注意:首字母大写,因为我们写的是一个组件
const Button = styled.button`
	background: transparent;
	border-radius: 3px;
	border: 2px solid palevioletred;
	color: palevioletred;
	margin: 0 1em;
	padding: 0.25em 1em;

	${(props: ButtonPropsType) =>
		props.primary &&
		css`
			background: palevioletred;
			color: white;
		`}
`

const Container = styled.div`
	margin-top: 50px;
	position: relative;
	width: 220px;
	height: 300px;
	display: flex;
	justify-content: center;
	align-items: center;
	transition: 0.5s;
	z-index: 1;

	::before {
		content: ' ';
		position: absolute;
		top: 0;
		left: 50px;
		width: 50%;
		height: 100%;
		text-decoration: none;
		background: #fff;
		border-radius: 8px;
		transform: skewX(15deg);
		transition: 0.5s;
	}

	::after {
		content: '';
		position: absolute;
		top: 0;
		left: 50;
		width: 50%;
		height: 100%;
		background: #fff;
		border-radius: 8px;
		transform: skewX(15deg);
		transition: 0.5s;
		filter: blur(30px);
	}

	&:hover:before,
	&:hover::after {
		transform: skewX(0deg) scaleX(1.3);
	}

	&:before,
	&:after {
		background: linear-gradient(315deg, #ffbc00, #ff0058);
	}

	span {
		display: block;
		position: absolute;
		top: 0;
		left: 0;
		right: 0;
		bottom: 0;
		z-index: 5;
		pointer-events: none;
	}

	span::before {
		content: '';
		position: absolute;
		top: 0;
		left: 0;
		width: 0;
		height: 0;
		border-radius: 8px;
		background: rgba(255, 255, 255, 0.1);
		backdrop-filter: blur(10px);
		opacity: 0;
		transition: 0.1s;
		animation: animate 2s ease-in-out infinite;
		box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
	}

	span::before {
		top: -40px;
		left: 40px;
		width: 50px;
		height: 50px;
		opacity: 1;
	}

	span::after {
		content: '';
		position: absolute;
		bottom: 0;
		right: 0;
		width: 100%;
		height: 100%;
		border-radius: 8px;
		background: rgba(255, 255, 255, 0.1);
		backdrop-filter: blur(10px);
		opacity: 0;
		transition: 0.5s;
		box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
		animation-delay: -1s;
	}

	span:after {
		bottom: -40px;
		right: 40px;
		width: 50px;
		height: 50px;
		opacity: 1;
	}
`

const Content = styled.div`
	position: relative;
	width: 190px;
	height: 254px;
	padding: 20px 40px;
	background: rgba(255, 255, 255, 0.05);
	backdrop-filter: blur(10px);
	box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
	border-radius: 8px;
	z-index: 1;
	transform: 0.5s;
	color: #fff;
	display: flex;
	justify-content: center;
	align-items: center;

	h2 {
		font-size: 20px;
		color: #fff;
		margin-bottom: 10px;
	}
`

const Demo: FC = () => {
	return (
		<Layout>
			<Button>按钮</Button>
			<Button primary={true}>BUTTON</Button>
			<Container>
				<span></span>
				<Content>
					<h2>hover !</h2>
				</Content>
			</Container>
		</Layout>
	)
}

export default Demo

Styled-jsx 工具

ts环境下,使用起来很麻烦,略过

Emotion 工具

ts环境下,使用起来很麻烦,略过

3种方案小结

  1. 普通引入CSS: 无法有效解决css命名冲突的问题,这就是层叠样式,与框架无关

  2. CSS-Module: CSS-Module是一种利用构建工具(如webpack)在编译阶段将CSS样式文件模块化的方法。它使用了类似于局部作用域的方式,确保每个组件的样式在运行时是唯一且隔离的。在使用CSS-Module时,每个CSS文件被视为独立的模块,样式文件中定义的类名被自动重命名为一个独特的类名,并使用该类名来应用样式。这样可以避免全局样式冲突,并提高代码的可维护性。

  3. CSS in JS: CSS in JS是一种将CSS样式直接写在JavaScript代码中的方法。它允许在JavaScript代码中直接定义样式对象,并通过将这些样式对象绑定到组件的props或类名来应用样式。CSS in JS在运行时动态生成和应用样式,使得样式的定义和组件的逻辑更加紧密,并能够更灵活地根据组件的状态或属性来动态调整样式。

区别:

  • 语法:CSS-Module使用普通的CSS语法,而CSS in JS使用JavaScript对象的语法。
  • 构建:CSS-Module需要依靠构建工具(如webpack)在编译阶段将CSS样式文件进行处理,而CSS in JS是在运行时动态生成和应用样式。
  • 唯一性:CSS-Module通过自动重命名类名确保样式的唯一性,而CSS in JS则通过样式对象的引用来确保样式的唯一性。
  • 运行环境:CSS-Module可以运行在任何支持CSS的环境中,而CSS in JS则依赖于JavaScript运行环境。

高级特性

Portals

  • 组件默认会按照既定层次嵌套渲染
  • 如何让组件渲染到父组件以外?

使用场景:创建诸如模态框、弹出菜单、通知提示等需要在DOM层次结构之外渲染的UI组件非常有用。

渲染到root上并保证原有功能:

js 复制代码
import React, { Component } from 'react'
import ReactDOM from 'react-dom'
import type { ReactNode } from 'react'

import classnames from 'classnames'
import { produce } from 'immer'
import styles from './index.module.scss'
import ClassControlledComponentDemo from './ClassControlledComponentDemo'
import FunctionControlledComponentDemo from './FunctionControlledComponentDemo'
import ClassUnControlledComponentDemo from './ClassUnControlledComponentDemo'
import FunctionUnControlledCOmponentDemo from './FunctionUnControlledComponentDemo'

type StateType = {
	dynamicClassName: string
}

class FormUseDemo extends Component<unknown, StateType> {
	private isRedBordedr = false
	private isYellowBackground = false
	private isBlueColor = false
	private tempComponent = () => {
		return ReactDOM.createPortal(
			<div className={styles.item}>
				<h3>父组件</h3>
				<button onClick={this.toggleBorderStyle}>显示/隐藏样式</button>
			</div>,
			document.getElementById('root') as HTMLDivElement
		)
	}
	constructor(props: unknown) {
		super(props)
		this.state = {
			dynamicClassName: classnames({
				[styles['container']]: true,
				[styles['red-border']]: this.isRedBordedr,
				[styles['yellow-background']]: this.isYellowBackground,
				[styles['blue-color']]: this.isBlueColor,
			}),
		}
	}
	render(): ReactNode {
		return (
			<div className={this.state.dynamicClassName}>
				<div className={styles.item}>
					<h3>function 组件 ------ 受控表单</h3>
					<div>
						<FunctionControlledComponentDemo />
					</div>
				</div>
				<div className={styles.item}>
					<h3>function 组件 ------ 非受控表单</h3>
					<div>
						<FunctionUnControlledCOmponentDemo />
					</div>
				</div>
                                
                                {/* 这一部分抽成了一个组件,挂载到了root上 */}
				<this.tempComponent />
                                
				<div className={styles.item}>
					<h3>class 组件 ------ 受控表单</h3>
					<div>
						<ClassControlledComponentDemo />
					</div>
				</div>
				<div className={styles.item}>
					<h3>class 组件 ------ 非受控表单</h3>
					<div>
						<ClassUnControlledComponentDemo />
					</div>
				</div>
			</div>
		)
	}

	private toggleBorderStyle = () => {
		this.isRedBordedr = !this.isRedBordedr
		this.isYellowBackground = !this.isYellowBackground
		this.isBlueColor = !this.isBlueColor
		this.setState(
			produce<StateType>(draft => {
				draft.dynamicClassName = classnames({
					[styles['container']]: true,
					[styles['red-border']]: this.isRedBordedr,
					[styles['yellow-background']]: this.isYellowBackground,
					[styles['blue-color']]: this.isBlueColor,
				})
			})
		)
	}
}

export default FormUseDemo

context

  • 公共信息(语言,主题)如何传递给每个组件?
  • 用props多层传递太繁琐,用redux小题大做
  • 通过React.createContext创建一个上下文组件
  • class组件与函数组件,提供方写法相同,消费写法不同
    • class组件仅能通过Consumer组件消费
    • 函数组件不仅可以通过Consumer组件消费,也可以使用useContext钩子消费

缺点:Context 并不能直接从下游组件向上游组件传递数据。下游组件只能访问直接提供给它们的 Context 数据,而无法使用 Context 直接向上游组件传递数据。

创建context,并制作一个Provider:

js 复制代码
import React, { createContext, useState } from 'react'
import type { FC } from 'react'
import ReactDOM from 'react-dom'
import styles from './index.module.scss'

// 引入一个语言配置作为被消费的数据
import { languageConfig } from './language'

type PropsType = {
	children: JSX.Element
}

// 创建一个context并导出
export const LanguageContext = createContext(languageConfig.CN)

const LanguageProvide: FC<PropsType> = props => {
	const [lang, setLang] = useState(languageConfig.CN)

	const toggleLanguage = () => {
		if (lang === languageConfig.CN) {
			setLang(languageConfig.EN)
		} else {
			setLang(languageConfig.CN)
		}
	}

	const ToolBar: FC = () => {
		return ReactDOM.createPortal(
			<div className={styles['toolbar']}>
				<button onClick={toggleLanguage}>切换语言</button>
			</div>,
			document.getElementById('root') as Element
		)
	}

	return (
		<LanguageContext.Provider value={lang}>
			<ToolBar />
			<div style={{ marginTop: '25px' }}>{props.children}</div>
		</LanguageContext.Provider>
	)
}

export default LanguageProvide

LanguageProvide的下游组件可以消费LanguageProvide提供的数据:

js 复制代码
import React from 'react'
import type { FC } from 'react'
import styles from './index.module.scss'
import OneLevelComponent from './OneLevelComponent'
import LanguageProvide from './LanguageProvide'

const BaseUseDemo: FC = () => {
	return (
		<LanguageProvide>
			<div className={styles.container}>
				<OneLevelComponent />
			</div>
		</LanguageProvide>
	)
}

export default BaseUseDemo

class组件仅可以通过Consumer组件消费:

js 复制代码
import React, { Component } from 'react'
import { LanguageContext } from './LanguageProvide'
import styles from './index.module.scss'

class FourLevelComponent extends Component {
	render(): React.ReactNode {
		return (
			<LanguageContext.Consumer>
				{lang => <div className={styles['gray-boreder']}>{lang.four}</div>}
			</LanguageContext.Consumer>
		)
	}
}

export default FourLevelComponent

函数组件可以使用Consumer组件消费:

js 复制代码
import React from 'react'
import type { FC } from 'react'
import { LanguageContext } from './LanguageProvide'
import styles from './index.module.scss'
import FourLevelComponent from './FourLevelComponent'

const ThreeLevelComponent: FC = () => {
	return (
		<LanguageContext.Consumer>
			{lang => (
				<div className={styles['gray-boreder']}>
					{lang.three}
					<FourLevelComponent />
				</div>
			)}
		</LanguageContext.Consumer>
	)
}

export default ThreeLevelComponent

函数组件也可以使用useContext钩子消费:

js 复制代码
import React, { useContext } from 'react'
import type { FC } from 'react'
import styles from './index.module.scss'
import TwoLevelComponent from './TwoLevelComponent'
import { LanguageContext } from './LanguageProvide'

const OneLevelComponent: FC = () => {
	const lang = useContext(LanguageContext)

	return (
		<div className={styles['gray-boreder']}>
			{lang.one}
			<TwoLevelComponent />
		</div>
	)
}

export default OneLevelComponent

异步组件

  1. 按需加载(Code-Splitting) :当应用程序变得庞大时,将所有组件都打包在一起可能导致初始加载时间过长。使用异步组件,可以在需要时按需加载组件,减小初始加载的文件大小,提升应用程序初次渲染的速度。

  2. 减少打包体积:通过将某些组件延迟加载,可以将与这些组件相关的代码从初始打包中剥离。这样,用户访问应用程序时只加载必要的代码,避免不必要的资源浪费,减小了打包生成的文件体积。

  3. 优化用户体验 :通过在需要时异步加载组件,可以提高页面的响应速度。当用户与应用程序交互时,异步组件可以在后台加载,确保视觉上的延迟最小化,提升用户体验。

  4. 降低页面的加载时间 :通过将页面划分为多个异步组件,可以并行下载组件的代码和资源,从而降低整体页面加载时间。这尤其有助于在低带宽或高延迟环境中提供更好的用户体验。

class组件:

js 复制代码
import React, { lazy, Suspense } from 'react'
import styles from './index.module.scss'

class LazyDemo extends React.Component {
	render() {
		const AsyncComponent = lazy(() => import('../../form/index'))

		return (
			<div className={styles.container}>
				<Suspense fallback={<div>Loading...</div>}>
					<AsyncComponent />
				</Suspense>
			</div>
		)
	}
}

export default LazyDemo

函数组件:

js 复制代码
import React, { lazy, Suspense } from 'react'
import type { FC } from 'react'
import styles from './index.module.scss'

const LazyDemo: FC = () => {
	const AsyncComponent = lazy(() => import('../../form/index'))

	return (
		<div className={styles.container}>
			<Suspense fallback={<div>Loading...</div>}>
				<AsyncComponent />
			</Suspense>
		</div>
	)
}

export default LazyDemo

代码复用

HOC

HOC(Higher-Order Component)是React中的一种高阶组件的模式,它实际上是一个函数,接受一个组件作为参数,并返回一个增强过的新组件。

HOC可以在不修改原始组件的情况下,通过封装和增强组件的功能来实现代码的重用。它通过将通用的逻辑和功能提取到一个高阶组件中,然后将这些逻辑和功能通过props传递给被包装的组件。

HOC主要用于以下几个方面:

  1. 代码复用:通过将通用的逻辑和功能提取到高阶组件中,可以在多个组件之间共享和重用代码。

  2. 渲染劫持:HOC可以修改被包装组件的渲染行为,例如添加额外的DOM元素、修改props或state等。

  3. 条件渲染:HOC可以根据一定的条件来渲染不同的组件。例如,根据用户的登录状态来渲染不同的导航栏。

当无权限访问时,显示提示语:

当有权限访问时,正常渲染组件:

class组件:

js 复制代码
import React, { Component } from 'react'

const withAuthorization = (WrappedComponent: any, requiredRole: string): any => {
	class roleCheck extends Component {
		// 假设当前的用户有以下角色
		private roleList = ['role1', 'role2', 'role3']

		// 角色校验是否通过
		private isPass = this.roleList.includes(requiredRole)

		render() {
			return <>{this.isPass ? <WrappedComponent {...this.props} /> : <div>暂无权限访问</div>}</>
		}
	}

	return roleCheck
}

export default withAuthorization

函数组件:

js 复制代码
import React from 'react'
import type { FC } from 'react'

const withAuthorization = (WrappedComponent: any, requiredRole: string) => {
	const RoleCheck: FC = (props: any) => {
		const roleList = ['role1', 'role2', 'role3']
                
                // 角色校验是否通过
		const isPass = roleList.includes(requiredRole)

		return <>{isPass ? <WrappedComponent {...props} /> : <div>暂无权限访问</div>}</>
	}

	return RoleCheck
}

export default withAuthorization

使用:

js 复制代码
import MouseTracker from './view/advancedUse/render-props/index'
import withAuthorization from './view/advancedUse/HOC/index'

// 传入需要添加权限校验的组件,必须拥有的角色
const HOC = withAuthorization(MouseTracker, 'role4')

// 渲染
<HOC />

通过使用HOC,我们可以在不修改原始组件的情况下,为其添加额外的功能,实现代码的重用和组件的增强。

Render Props

Render Props是一种在React中共享代码逻辑的技术。

Render Props通过使用一个值为函数 的prop,将一个组件需要的代码逻辑封装到函数中,并将这个函数作为prop传递给子组件。

  • 父组件负责定义和传递函数作为prop,规定如何进行渲染。(发布任务)
  • 子组件则负责调用这个函数并使用其返回的值, 完成父组件渲染所需要的功能逻辑。(完成任务)

父组件:

js 复制代码
import React from 'react'
import Mouse from './Mouse'

class MouseTracker extends React.Component {
	render() {
		return (
			<div>
				<h1>移动鼠标</h1>
				<Mouse
					position={(x, y) => (
						<h2>
							鼠标位置: {x}, {y}
						</h2>
					)}
				/>
			</div>
		)
	}
}

export default MouseTracker

子组件:

js 复制代码
import React from 'react'
import type { MouseEvent } from 'react'

type PropsType = {
	position: (x: number, y: number) => JSX.Element
}

class Mouse extends React.Component<PropsType> {
	state = { x: 0, y: 0 }

	handleMouseMove = (event: MouseEvent) => {
		this.setState({ x: event.clientX, y: event.clientY })
	}

	render() {
		return (
			<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
				{this.props.position(this.state.x, this.state.y)}
			</div>
		)
	}
}

export default Mouse

自定义Hook

  • 必须用useXxx格式来命名
  • 只能在两个地方调用Hook:组件内,或其他Hook内
  • 必须保证每次的调用顺序一致,不能放在if、for的内部
  • 必须放在组件顶层
  • 不能再hook前面加return
js 复制代码
import { useEffect } from 'react'

export default () => {
	useEffect(() => {
		document.title = 'React学习'
	}, [])
}
js 复制代码
import { useCallback, useEffect, useState } from 'react'

// 获取鼠标位置
const useMouse = () => {
	const [x, setX] = useState(0)
	const [y, setY] = useState(0)

	const mouseMoveHandler = useCallback((event: MouseEvent) => {
		setX(event.clientX)
		setY(event.clientY)
	}, [])

	// 引用该hook的组件,执行的useEffect
	useEffect(() => {
		// 监听鼠标事件
		window.addEventListener('mousemove', mouseMoveHandler)
		// 组件销毁时,解绑DOM事件
		return () => {
			window.removeEventListener('mousemove', mouseMoveHandler)
		}
	}, [])

	return { x, y }
}

export default useMouse
js 复制代码
import { useState, useEffect } from 'react'

const getInfo = (): Promise<string> => {
	return new Promise(resolve => {
		setTimeout(() => {
			resolve(Date.now().toString())
		}, 1500)
	})
}

const useGetInfo = () => {
	const [loading, setLoading] = useState(true)
	const [info, setInfo] = useState('')

	useEffect(() => {
		setLoading(true)
		getInfo()
			.then(res => {
				setInfo(res)
				setLoading(false)
			})
			.catch(() => {
				setLoading(false)
			})
	}, [])

	return {
		info,
		loading,
	}
}

export default useGetInfo

性能优化

Vue是自动挡,React是手动挡,除了路由懒加载外我们还需要注意很多渲染和计算的优化点

SCU与React.memo

为什么需要SCU与React.memo? 当父组件state改变时,子组件state无变化,子组件却发生了渲染,这是无意义的,我们可以优化掉这种没有必要的渲染,因此就有了SCU和React.memo

React的更新机制是什么样的? 当父组件发生更新,class组件会执行自身的render,函数组件会重新执行该函数,所以会有很多次渲染

class组件渲染优化

SCU是指class组件的生命周期shouldComponentUpdate,这是一个接收两个参数nextProps 和 nextState返回boolean的生命周期函数,默认返回true,也就是,默认允许更新组件,从而引发了渲染,我们可以通过比对新老props、state,决定是否更新,避免无意义的渲染

js 复制代码
import React from 'react';

class MyComponent extends React.Component {
  shouldComponentUpdate(nextProps, nextState) {
    // 比较 props 和 state 是否发生变化
    if (
      nextProps.name !== this.props.name ||
      nextState.count !== this.state.count
    ) {
      return true; // 需要重新渲染
    }
    return false; // 不需要重新渲染
  }

  render() {
    return <div>{this.props.name}</div>;
  }
}

初次之外,class组件还可以选择通过继承PureComponent避免无效渲染,它会浅比较当前和下一个props以及state的变化,如果没有变化,则阻止不必要的重新渲染, 并不是所有的组件都适合使用PureComponent,特别是在组件内部有复杂的数据结构或属性的组件。

js 复制代码
class Component extends React.PureComponent

函数组件渲染优化

函数组件是没有生命周期的,但我们可以通过React.memo来进行优化,React.memo的默认比较行为是对每个 prop进行浅比较, 只有prop第一层级有内存地址变动才会发生更新,第二参数非必传,如有需要,传入一个比较函数,可以自定义如何比较新老props从而决定是否'记住'组件,不重新渲染,返回true不渲染,返回false渲染,这与SCU刚好相反

在比较函数中,你可以根据具体情况自定义比较逻辑:

  1. 深比较(Deep Comparison):使用某个库(如 Lodash 的 isEqual)或自己实现递归比较,比较所有的 props。这样会比较完整,但也可能带来一定的性能开销。
js 复制代码
import React from 'react';
import _isEqual from 'lodash/isEqual';

function areEqual(prevProps, nextProps) {
  return _isEqual(prevProps, nextProps);
}

const MyComponent = React.memo(Component, areEqual);
  1. 浅比较(Shallow Comparison):只比较一级 props 的值,可以使用浅相等操作符(===)进行比较。这种方式快速而简单,但可能会漏掉一些细微的变化。
js 复制代码
import React from 'react';

function areEqual(prevProps, nextProps) {
  return (
    prevProps.name === nextProps.name &&
    prevProps.age === nextProps.age
    // 其他需要比较的 props
  );
}

const MyComponent = React.memo(Component, areEqual);
  1. 特定字段比较:只比较某些特定的 props 字段。这适用于只关心部分 props 字段变化的情况。
js 复制代码
import React from 'react';

function areEqual(prevProps, nextProps) {
  return prevProps.name === nextProps.name;
  // 只比较 name 字段
}

const MyComponent = React.memo(Component, areEqual);

使用第二个参数的比较函数可以根据具体需求,灵活地决定组件是否需要重新渲染,从而提高性能。

使用useCallback和useMemo减少无效定义、无效计算

由于函数组件触发rerender时,会重新执行该函数的原因,进而多了许多没有必要的计算以及内部函数的重复定义

useCallback用于优化函数的性能。它可以用来缓存函数的引用,并在依赖项不发生变化的情况下,避免函数的重复创建。

js 复制代码
import React, { useCallback } from 'react';

function MyComponent() {
  const handleClick = useCallback(() => {
    // 处理点击事件的逻辑
  }, []); // 依赖项为空数组,这意味着回调函数不依赖任何值

  return (
    <button onClick={handleClick}>Click me</button>
  );
}

useMemo用于优化组件的计算性能。它可以在组件渲染过程中缓存计算结果,避免重复计算,当依赖项不发生变化时,useMemo会返回前一次计算的结果。而当依赖项 发生变化时,useMemo会调用计算函数,计算新的结果并返回

  1. 针对耗时的计算:如果组件需要进行一些耗时 的计算操作,且这些计算结果在依赖项 不发生变化时保持不变,可以使用useMemo来缓存这些计算结果。

  2. 避免不必要的渲染:如果在父组件进行重渲染时,某些子组件的渲染是不必要的,可以使用useMemo来缓存子组件的内容,只有在依赖项发生变化时才更新子组件。

  3. 避免重复请求数据:在组件中进行数据请求时,可以使用useMemo来缓存请求结果,只有当依赖项发生变化时才重新请求数据。

js 复制代码
import React, { useMemo } from 'react';

const Component = ({ data }) => {
  const average = useMemo(() => {
    let sum = 0;
    for (let i = 0; i < data.length; i++) {
      sum += data[i];
    }
    return sum / data.length;
  }, [data]);

  return (
    <div>
      Average: {average}
    </div>
  );
};

与useMemo类似的还有useState传入函数

  • useState传入普通变量,每次组件更新都会执行
  • useState传入函数,只在组件渲染时执行一次,适合数据结构复杂、计算成本高,但依赖项不经常变更的场景

class组件可使用类似于lodash中的memoize函数将不变的属性或计算结果缓存起来,避免重复计算

使用immer或者immutable

不可变值 每一次进行赋值都需要深拷贝,性能很差,使用这两个库,可以基于'共享数据'进行赋值,不是深拷贝,速度好

优化注意事项

  • 不要提前优化
  • 不要为了优化而优化
  • 在需要优化的时候再进行优化

状态管理

什么是状态管理?

  • 页面足够复杂:组件很多,嵌套层级很深
  • 通过props层层传递不合适
  • 需要状态管理,即集中、统一管理页面数据

什么是状态提升?

  • 页面拆分组件,组件嵌套
  • 数据存储在父组件
  • 通过props传递给子组件

useReducer

  • useState的代替方案,阉割版的redux
  • 数据结构简单时用useState,复杂时用useReducer
  • 简化版的redux
  • 函数组件可以使用useReducer
  • class组件无法使用useReducer,但是可以模拟实现

简单使用

js 复制代码
import React, { useReducer } from 'react'
import type { FC } from 'react'

type StateType = {
	count: number
}

type ActionType = {
	type: string
}

const initialState: StateType = { count: 100 }

/**
 * @description: 根据传入的 action 返回新的 state (不可变数据)
 * @param {StateType} state
 * @param {ActionType} action
 */
const reducer = (state: StateType, action: ActionType) => {
	switch (action.type) {
		case 'increment':
			return { count: state.count + 1 }
		case 'decrement':
			return { count: state.count - 1 }
		default:
			throw new Error('请务必传入action type')
	}
}

const CountReducer: FC = () => {
	const [state, dispatch] = useReducer(reducer, initialState)

	return (
		<>
			<span>count: {state.count}</span>
			<button
				onClick={() => {
					dispatch({ type: 'increment' })
				}}
			>
				+
			</button>
			<button
				onClick={() => {
					dispatch({ type: 'decrement' })
				}}
			>
				-
			</button>
		</>
	)
}

export default CountReducer

结合Context跨组件使用reducer

现在我们将state单独管理,方便接入各层级组件,按以下目录结构定义:

js 复制代码
- store
    - reducer 减速器,控制状态改动
    - state 状态,也就是数据
    - interface ts类型管理
    - index store入口,整合资源提供外界访问

store/index.ts:

js 复制代码
export * from './interface'
export * from './reducer'
export * from './state'

store/interface.ts:

js 复制代码
import type { Dispatch } from 'react'

export type Item = {
	name: string
	age: number
	gender: string
}

export type StateType = {
	list: Array<Item>
}
export type ActionType = {
	type: 'ADD' | 'DELETE' | 'CHANGE'
	payload: Partial<{
		data: Item
		index: number
	}>
}

export type ListContextType = {
	state: StateType
	dispatch: Dispatch<ActionType>
}

store/state.ts:

js 复制代码
import { StateType } from './interface'

export const initialState: StateType = {
	list: [{ name: 'ljx', age: 28, gender: 'male' }],
}

使用函数组件或者class组件结合上下文,供下游访问

  • 函数组件方式:可以使用useReducer钩子快捷沟通store:

    js 复制代码
    import React, { useReducer, createContext } from 'react'
    import type { FC } from 'react'
    import { initialState, reducer } from './store/index'
    import type { ListContextType } from './store/index'
    import List from './List'
    
    // 创建context,供多组件访问
    export const ListContext = createContext<ListContextType>({
            state: initialState,
            dispatch: () => {
                    /* 空 */
            },
    })
    
    const ReducerDemo: FC = () => {
            const [state, dispatch] = useReducer(reducer, initialState)
    
            return (
                    <ListContext.Provider value={{ state, dispatch }}>
                            <List />
                    </ListContext.Provider>
            )
    }
    
    export default ReducerDemo
  • class组件方式:无法使用钩子,但是我们可以自定义,借此可以理解useReducer的原理

    js 复制代码
    import React, { createContext, Component } from 'react'
    import type { Dispatch } from 'react'
    import { initialState, reducer } from './store/index'
    import type { ListContextType, StateType, ActionType } from './store/index'
    import List from './List'
    
    // 创建context,供多组件访问
    export const ListContext = createContext<ListContextType>({
            state: initialState,
            dispatch: () => {
                    /* 空 */
            },
    })
    
    class ReducerDemo extends Component {
            state: StateType = initialState
    
            render(): React.ReactNode {
                    return (
                            <ListContext.Provider value={{ state: this.state, dispatch: this.dispatch }}>
                                    <List />
                            </ListContext.Provider>
                    )
            }
    
            dispatch: Dispatch<ActionType> = (action: ActionType) => {
                    const newState = reducer(this.state, action)
                    this.setState(newState)
            }
    }
    
    export default ReducerDemo

内部访问

  • List组件用作显示

    js 复制代码
    import React, { useContext } from 'react'
    import type { FC } from 'react'
    import { ListContext } from './index'
    
    const List: FC = () => {
            const { state } = useContext(ListContext)
    
            return (
                    <div>
                            {state.list.map(({ name, gender, age }) => (
                                    <div key={name}>
                                            <span>姓名:{name}</span>
                                            <span>年龄:{age}</span>
                                            <span>性别:{gender}</span>
                                    </div>
                            ))}
                    </div>
            )
    }
    
    export default List
  • ToolBar组件用作更改操作

    js 复制代码
    import React, { Component } from 'react'
    import { nanoid } from 'nanoid'
    import { ListContext } from './index'
    import type { ListContextType } from './store/index'
    
    class ToolBar extends Component {
            dispatch?: ListContextType['dispatch']
            reducerState?: ListContextType['state']
            render(): React.ReactNode {
                    return (
                            <ListContext.Consumer>
                                    {context => {
                                            this.dispatch = context.dispatch
                                            this.reducerState = context.state
                                            return (
                                                    <div>
                                                            <button onClick={this.handleAdd}>add</button>
                                                            <button onClick={this.handleChange}>change</button>
                                                            <button onClick={this.handleDelete}>delete</button>
                                                    </div>
                                            )
                                    }}
                            </ListContext.Consumer>
                    )
            }
    
            handleAdd = () => {
                    if (!this.reducerState || !this.dispatch) return
                    const data = {
                            name: 'dys' + nanoid(2),
                            age: Math.trunc(100 * Math.random()),
                            gender: 'male',
                    }
                    this.dispatch({
                            type: 'ADD',
                            payload: { data },
                    })
            }
    
            handleChange = () => {
                    if (!this.reducerState || !this.dispatch) return
                    const data = {
                            name: 'hzc' + nanoid(2),
                            age: Math.trunc(100 * Math.random()),
                            gender: 'male',
                    }
                    this.dispatch({
                            type: 'CHANGE',
                            payload: { data, index: Math.trunc(this.reducerState.list.length * Math.random()) },
                    })
            }
    
            handleDelete = () => {
                    if (!this.reducerState || !this.dispatch) return
                    this.dispatch({
                            type: 'DELETE',
                            payload: { index: Math.trunc(this.reducerState.list.length * Math.random()) },
                    })
            }
    }
    
    export default ToolBar

小结

  • state或store数据
  • dispatch派发action,谁要更新state,谁进行派发
  • action动作,指令,指明调用何种reducer
  • reducer如何处理state,而后返回一个新的state代替原有的state,注意不可变数据
  • 类似于redux的流程和API
  • 结合Context解决跨组件问题
  • useReducer与useState一样,返回的值是响应式的,可以触发rerender
  • class组件模拟实现是将store中存储的状态转换为了class组件自身的state后才具备触发rerender能力得到

Redux

  • React 最流行的状态管理工具
  • Redux和useReducer概念一致
  • dispatch派发action,谁要更新state,谁进行派发
  • reducer对state进行处理

Context + useReducer 代替 Redux?

  • 简单场景可以,节省代码体积,更简单
  • 复杂场景仍然建议使用Redux

Redux单向数据流

实践

chrome安装redux Devtools方便跟踪和调试

安装:npm install @reduxjs/toolkit react-redux --save

原理同上面useReducer结合context跨组件,区别在于,redux这进行了模块化,状态管理更大化

  1. 配置store,将不同模块的state、reducer,进行整合,统一出口,供外界访问
js 复制代码
import { configureStore } from '@reduxjs/toolkit'
import countReducer from './count'
import todoListReducer from './todoList'
import type { TodoItemType } from './todoList'

export type StateType = {
	count: number
	todoList: Array<TodoItemType>
}

// 配置store
export default configureStore({
	reducer: {
		count: countReducer,
		todoList: todoListReducer,
	},
})
  1. 创建切片(模块),输出reducer,action
js 复制代码
import { createSlice } from '@reduxjs/toolkit'
import type { PayloadAction } from '@reduxjs/toolkit'
import { nanoid } from 'nanoid'

export type TodoItemType = {
	id: string
	title: string
	completed: boolean // 是否完成
}

const INIT_STATE: Array<TodoItemType> = [
	{
		id: nanoid(5),
		title: '吃螺蛳粉',
		completed: true,
	},
	{
		id: nanoid(5),
		title: '煲电话粥',
		completed: true,
	},
	{
		id: nanoid(5),
		title: '学习react',
		completed: false,
	},
]

const todoListSlice = createSlice({
	name: 'todoList',
	initialState: INIT_STATE,
	reducers: {
		addTodo(state, action: PayloadAction<TodoItemType>) {
			return state.concat(action.payload)
		},
		removeTodo(state, action: PayloadAction<TodoItemType>) {
			return state.filter(item => item.id !== action.payload.id)
		},
		toggleCompleted(state, action: PayloadAction<TodoItemType>) {
			return state.map(item => {
				if (item.id !== action.payload.id) return item
				const newItem = {
					...item,
				}
				newItem.completed = !newItem.completed
				return newItem
			})
		},
	},
})

export const { addTodo, removeTodo, toggleCompleted } = todoListSlice.actions

export default todoListSlice.reducer
  1. 使用<Provider>包裹顶层组件,提供store供下游使用
js 复制代码
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App'
import reportWebVitals from './reportWebVitals'
import { Provider } from 'react-redux'
import store from './store'

const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement)
root.render(
	<React.StrictMode>
		<Provider store={store}>
			<App />
		</Provider>
	</React.StrictMode>
)

reportWebVitals()
  1. 通过useSelector获取指定模块的state,通过useDispatch修改派发action
js 复制代码
import React from 'react'
import type { FC } from 'react'
import { nanoid } from 'nanoid'
import { useSelector, useDispatch } from 'react-redux'
import { addTodo, removeTodo, toggleCompleted } from '../store/todoList'
import type { StateType } from '../store/index'
import type { TodoItemType } from '../store/todoList'

const TodoList: FC = () => {
	const todoList = useSelector<StateType, Array<TodoItemType>>(state => state.todoList)

	const dispath = useDispatch()

	const del = (todo: TodoItemType) => {
		dispath(removeTodo(todo))
	}

	const toggle = (todo: TodoItemType) => {
		dispath(toggleCompleted(todo))
	}

	const add = () => {
		const newtodo: TodoItemType = {
			id: nanoid(5),
			title: `${Date.now()}`,
			completed: false,
		}
		dispath(addTodo(newtodo))
	}

	return (
		<div>
			<p>TodoList demo</p>
			<ul>
				{todoList.map(todo => {
					const { id, title, completed } = todo
					return (
						<li key={id} style={{ textDecoration: completed ? 'line-through' : '' }}>
							<span>{title}</span>
							<button onClick={() => del(todo)}>删除</button>
							<button onClick={() => toggle(todo)}>{completed ? '未完成' : '已完成'}</button>
						</li>
					)
				})}
			</ul>

			<button onClick={add}>+</button>
		</div>
	)
}

export default TodoList

小结

  • <Provider>包裹顶层
  • store管理state与reducer
  • reducer 更迭state
  • action 指明调用何种reducer
  • dispatch 需要修改状态时,派发action
  • 配合immer

React原理

数据驱动视图的核心

  • h函数
  • VNode数据结构
  • patch函数

VDOM

什么是虚拟DOM? 虚拟DOM (VDOM)是真实DOM的内存表示。UI的表示保存在内存中,并与"真正的"DOM同步。这是一个发生在被调用的渲染函数和在屏幕上显示元素之间的步骤。整个过程被称为'协调'

虚拟DOM的工作原理 Virtual DOM的工作分为三个简单步骤。

  1. 只要任何底层数据发生变化,整个UI就会以Virtual DOM表示形式重新渲染。

  2. 然后计算以前的DOM表示和新的DOM表示之间的差异。

  3. 计算完成后,真正的DOM将仅使用实际更改的内容进行更新

diff

  • 只比较同一层级,不跨级比较
  • tag不相同,则直接删掉重建,不再深度比较
  • tag和key,两者都相同,则认为是相同节点,不再深度比较
  • Vue2.X(Snabbdom),Vue3,React三者实现vdom细节都不同,核心概念和实现思路都一样

JSX的本质是什么

  • JSX等同于Vue模板
  • Vue模板不是html
  • JSX也不是JS

JSX的本质

  • JSX即React.createElement,即h函数,返回VNode,react 17使用jsx转换器代替React.createElement同样是返回VNode,所以现在它的本质就是一个转换器。
  • 第一个参数,可能是组件,也可能是html tag

jsx在线编译

合成事件机制

合成事件

  • event不是原生的,是SyntheticEvent合成事件对象
  • 可以通过event.nativeEvent获取原生事件对象
  • 和Vue事件不同,和DOM事件也不同
  • react 16及以前,所有事件挂载到document上,react 17后绑定到root组件,有利于多个React版本并存,例如微前端

react16及之前:

为何需要事件机制?

  • 更好的兼容性和跨平台
  • 统一挂载到root组件,减少内存消耗,避免频繁解邦
  • 方便事件的统一管理(如:transaction事务机制)

setState 和 batchUpdate(批量更新)

核心要点

  • setState主流程
  • batchUpdate(批量更新机制)
  • transaction(事物机制)

setState原理

  • react 18之前,有时异步,有时同步(setTimeout,DOM事件)
  • react 18之后,都是异步
  • 有时合并(对象形式),有时不合并(函数形式)

程序执行setState时发生了什么?

  1. 将newState存入pending微任务队列中
  2. 判断当前是否处于batchUpdate阶段
  3. 如果当前处于batchUpdate阶段,存放在dirtyComponents中,等待下一次更新时进行处理
  4. 如果当前没有处于batchUpdate阶段,遍历所有待处理的dirtyComponents,执行updateComponent更新pending队列,state,props

setState是异步任务还是同步任务?

  • setState无所谓异步还是同步
  • 看是否能命中batchUpdate机制
  • 判断isBatchingUpdates

哪些能命中batchUpdate机制?

  • 生命周期(和它调用的函数)
  • React中注册的事件(和它调用的函数)
  • React可以"管理"的入口

哪些无法命中batchUpdate机制?

  • setTimeout,setInterval等(和它调用的函数)
  • 自定义的DOM事件(和它调用的函数)
  • react"管不到"的入口

transaction事务机制

  1. 无论执行什么方法,先执行initialize,然后再执行该方法,最后执行close
  2. initialize对应isBatchingUpdate = true
  3. close对应isBatchingUpdate = false

react-fiber如何优化性能?

更新的两个阶段

  • reconciliation协调阶段------执行diff算法,纯JS计算
  • commit提交阶段------将diff结果渲染到DOM

为什么分为两个阶段?

  • JS是单线程 ,且和DOM渲染共用一个线程
  • 当组件足够复杂,组建更新时计算和渲染都压力很大
  • 同时再有DOM操作需求(动画,鼠标拖拽等),将卡顿

解决方案react-fiber

  • 将reconciliation阶段进行任务拆分(commit无法拆分)
  • DOM需要渲染时暂停,空闲时恢复
  • 原本fiber机制的本质是利用window.requestIdleCallback,现在已经更改为了另一套基于requestAnimationFrame的polyfill

面试题

实际工作中,做过哪些React优化?

  • 使用css模拟v-show
  • 组件层级多时,使用Fragment减少层级
  • 不在jsx中定义函数,因为jsx会被频繁执行
  • 使用class组件时,在constructor中bind this或者使用箭头函数,避免bind this
  • scu、PureComponent、React.memo、useCallback、useMemo、useState传入函数
  • 异步组件、路由懒加载
  • 使用immer

你使用React遇到过哪些坑?

setState因为版本的不同,导致执行时机不同,React18以前在setTimeout、DOM事件中(非React上下文中时候),可以在更新完成后接着就获取到值,表现为同步执行,其他时候都是异步执行,React18全都是异步执行

React如何统一监听组件报错?

  • ErrorBoundary组件
    • 监听所有下级组件报错
    • 可降级展示UI
    • 只监听组件渲染时报错
    • 不监听DOM事件
  • Window.onerror监听DOM事件报错、异步报错
  • window.onunhandledrejection监听Promise未catch的报错

输出什么?

初始值都为0 18版本:生产环境0 0 0 0 开发环境0 0 0 0 1 1 1 1 18之前: 0 0 2 3

18版本: 0 0 0 0 20 20 20 20 18版本以前: 0 0 21 22

17版本:

setState是宏任务还是微任务?

  • setState是 同步 任务,state,都是同步更新,只不过让React做成了异步得到样子
  • 因为要考虑性能,多次state修改。只进行一次DOM渲染
  • 在微任务Promise.then开始之前,state已经计算完了
  • 日常说的异步是不严谨的,但沟通成本低

小技巧:如果一个作用域内有一个setState,你可以理解为它永远放在作用域最后一行执行,如果是传入函数的就把它按顺序放在作用域最后顺序执行

class组件和function组件有什么区别?如何选择?

  • Function组件相比Class组件具有更好的性能,因为它没有额外的实例化和内存开销。Function组件也更易于测试和理解
  • 官方更推荐使用Function组件和Hooks的方式来开发新的组件

如果你喜欢,帮助到你了,点赞是对我最大帮助,如果你觉得这篇文章以后还能帮助到你,请收藏下它,如果文中有不对的,或者漏了的知识点,请私信我,感激不尽

相关推荐
code2cat2 分钟前
HTML基础篇之第一张网页:从index.html到浏览器里的任务清单
前端·html
miss1 小时前
AI 改完不敢上线?OpenSpec 规约驱动开发实战
前端·代码规范·前端工程化
AiMan1 小时前
腾讯ima还是不可以导出左侧所有对话,只能单条导出,方法不对吗
前端
去伪存真1 小时前
并发量一高就装死?Agent 全链路“防猝死”手册
前端·agent
Moment1 小时前
AI 编程时代,聊聊我的 Mac mini 双屏桌面
前端·后端
AiMan2 小时前
先确认能不能一次把Microsofat Copilot所有内容打包一个文件导出
前端
总要学点什么2 小时前
第97篇:三维数据加载-城市建筑白模自定义着色器——让一条光带沿高度扫上去,快慢却由帧率说了算
前端·gis·webgl·cesium·着色器·空间分析·白模
上海懒图科技2 小时前
工厂厂区电子地图定位导航:厂区人车精准定位,安全合规双保障
前端·arcgis·vue
西安小哥3 小时前
端侧跑大模型,真正的门槛不在参数量:一次 596 秒到 10 秒的压缩路径复盘
前端·人工智能
陈随易3 小时前
Cloudflare 出钱,Vite 团队挑人,44 个维护者分 30 万美元
前端·后端·程序员