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,对标vuetemplate,但是JSXFragment可以作为根节点,vuetemplate不能作为根节点

属性

  • jsx的属性就是html的属性,只有些微区别
  • class要写为className,为了防止和类(class)关键字冲突
  • style要使用Object,且必须是驼峰写法
  • labelfor要写为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. constructbind(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是异步的,我们要获取改变之后的stateclass组件需要通过向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) :组件的propsstate发生变化,导致组件重新渲染或更新。

    • 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. 当该参数未传为undefinednull时,模拟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:urlquery可以作为依赖项, 因为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.logconsole.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这个函数时,会生成gettersetter,返回一个函数,与一个状态,而这个函数与状态都是被保存在useState这个闭包函数 里,不受外界干扰,alert(count)时的count来源于当时执行rerender时调用的函数组件本身,而这个组件函数里面又调用了useState获取到了当时的count。

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

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

表单

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

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

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

再次重申:labelfor要写为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,仅需要默认赋值,因此使用defaultValuedefaultChecked

  • 输入元素使用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增加后缀名,不让它们重复,发挥一种类似于vuescoped的作用
  • 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元素、修改propsstate等。

  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
  • 必须保证每次的调用顺序一致,不能放在iffor的内部
  • 必须放在组件顶层
  • 不能再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

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

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

class组件渲染优化

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

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):使用某个库(如 LodashisEqual)或自己实现递归比较,比较所有的 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

小结

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

Redux

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

Context + useReducer 代替 Redux?

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

Redux单向数据流

实践

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

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

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

  1. 配置store,将不同模块的statereducer,进行整合,统一出口,供外界访问
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. 创建切片(模块),输出reduceraction
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管理statereducer
  • 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不相同,则直接删掉重建,不再深度比较
  • tagkey,两者都相同,则认为是相同节点,不再深度比较
  • Vue2.X(Snabbdom),Vue3,React三者实现vdom细节都不同,核心概念和实现思路都一样

JSX的本质是什么

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

JSX的本质

  • JSXReact.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之前,有时异步,有时同步(setTimeoutDOM事件)
  • 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,现在已经更改为了另一套基于requestAnimationFramepolyfill

面试题

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

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

你使用React遇到过哪些坑?

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

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

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

输出什么?

初始值都为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的方式来开发新的组件

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

相关推荐
学习使我快乐012 小时前
JS进阶 3——深入面向对象、原型
开发语言·前端·javascript
bobostudio19952 小时前
TypeScript 设计模式之【策略模式】
前端·javascript·设计模式·typescript·策略模式
黄尚圈圈3 小时前
Vue 中引入 ECharts 的详细步骤与示例
前端·vue.js·echarts
浮华似水4 小时前
简洁之道 - React Hook Form
前端
正小安6 小时前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch8 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光8 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   8 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   8 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web8 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery