基本使用
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
-
使用
this.setState()
更新state
,禁止 直接更改state
,传入全新state
, 完全替换 -
class组件
方法内的this
需要特别注意!
方式1:显示bind(this)
- 在
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
- 也可以在绑定事件时再
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
- 函数组件的
state
需要借助钩子useState
的帮助 - 使用
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使用注意点
-
无论是哪种组件都不要直接修改
state
的值,class组件
使用this.set()
方法修改state
,Function组件
使用useState()
返回的set
方法修改state
为什么?
组件
state
被设计为不可直接修改的,这是为了保持React
的单向数据流 和组件隔离的原则。直接修改
state
会导致什么问题?-
不触发更新 :如果直接修改状态的值,
React
无法检测到状态的变化,因此不会触发组件的重新渲染(rerender
)。结果是组件的输出不会更新,导致显示的内容不符合预期。 -
性能问题 :
React
通过比较前后状态的差异来判断是否需要重新渲染组件。当直接修改状态值时,React
无法确定状态是否发生了变化,因此可能会导致频繁的重新渲染,降低性能。 -
可追踪性问题 :
React
通过setState()
方法记录状态的变化历史,这样可以追踪状态的修改,帮助进行调试和排查问题。直接修改状态会失去这种历史记录,使得错误的定位和修复更加困难。
-
-
set
是异步的,我们要获取改变之后的state
,class组件
需要通过向setState()
传入callback
去获取,callback
会在rerender
之后触发,而Function组件
需要借助useEffect()
的帮助 -
set
时如果传入的是一个值,不是函数,操作会被合并 如图,每次执行addCount
时,都执行5次set
,分别点击一次add按钮,结果如下: 可以明显看到,class组件
的回调函数被执行了5次,因为每个回调函数的内存地址都不一样 -
set
时如果传入的是一个返回值的函数并且使用该函数提供的prevState
操作不会被合并为什么?
使用函数,会将函数传入异步队列,利用
eventLoop
的机制顺序执行这些函数 -
如果一个变量不用于
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. 传递普通数据,数据变化时,不会引起子组件
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-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-1. 传递父组件的方法给子组件使用
jsconst [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组件生命周期
-
挂载阶段(Mounting Phase):组件被实例化并插入到DOM中。
constructor(props)
在组件被创建时调用,用于初始化组件的状态(state
)和绑定事件处理方法。render()
必需方法,在该方法中返回JSX
片段。componentDidMount()
在组件首次渲染之后调用,可以执行一些副作用操作,如访问DOM
、发送网络请求等。
-
更新阶段(Updating Phase) :组件的
props
或state
发生变化,导致组件重新渲染或更新。shouldComponentUpdate(nextProps, nextState)
在组件更新之前调用,用于决定是否需要进行重渲染,默认情况下总是返回true
。render()
componentDidUpdate(prevProps, prevState, snapshot)
在组件更新之后调用,可以执行一些副作用操作。
-
卸载阶段(Unmounting Phase):组件从DOM中被移除。
componentWillUnmount()
在组件被卸载和销毁之前调用,可以进行一些清理操作,如取消订阅、清除计时器等。
-
错误处理阶段(Error Handling Phase):组件在渲染期间、生命周期方法中发生错误。
componentDidCatch(error, info)
捕获在后代组件中生成的异常。未处理的异常将导致整个组件树卸载。
函数组件'生命周期'
严格意义来说,函数组件本身就只是个纯函数
没有生命周期,但我们可以借助useEffect
useEffect: 参数1-effect:useEffect
钩子触发时的执行函数,该函数可以返回一个函数,如果返回函数,则该返回函数会在组件时调用(模拟componentWillUnmount
)
参数2-deps: 依赖项(非必传)
- 当该参数为
[]
(空数组)时,模拟componentDidMount
,在组件挂载完成时执行参数1 - 当该参数为有依赖项的数组时,模拟
componentDidMount
,在组件挂载完成时及数组内的依赖项发生变化时(模拟特定于这些依赖项的componentDidUpdate
)执行参数1 - 当该参数未传为
undefined
、null
时,模拟componentDidMount
,模拟componentDidUpdate
,组件挂载完成时及任何状态发生改变的时候都会执行参数1
那么哪些东西可以作为依赖项呢?
-
当前组件的
state
-
props
, 前提得是父组件传递过来的state
, 具体例子可以看上面父组件通过props
传递普通数据和状态的区别,普通数据不能作为依赖项,它不会引起rerender
-
在组件中声明的会因为重新渲染而改变的变量都可以作为依赖项,可以视为具有了状态
例子1:i变化的时候并不会执行
effect
函数,因为i改变,不会引起重新渲染,不能作为依赖项,不会产生副作用,所以不会执行jslet i = 0 useEffect(() => { console.log(i) }, [i]) setInterval(() => { i++ }, 1000)
例子2:
url
的query
可以作为依赖项
, 因为query
改变能够引起rerender
jslet 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
安装: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
安装: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种方案小结
-
普通引入CSS: 无法有效解决css命名冲突的问题,这就是层叠样式,与框架无关
-
CSS-Module:
CSS-Module
是一种利用构建工具(如webpack
)在编译阶段将CSS样式文件模块化的方法。它使用了类似于局部作用域的方式,确保每个组件的样式在运行时是唯一且隔离的。在使用CSS-Module
时,每个CSS文件被视为独立的模块,样式文件中定义的类名被自动重命名为一个独特的类名,并使用该类名来应用样式。这样可以避免全局样式冲突,并提高代码的可维护性。 -
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
异步组件
-
按需加载(Code-Splitting) :当应用程序变得庞大时,将所有组件都打包在一起可能导致初始加载时间过长。使用
异步组件
,可以在需要时按需加载组件,减小初始加载的文件大小,提升应用程序初次渲染的速度。 -
减少打包体积:通过将某些组件延迟加载,可以将与这些组件相关的代码从初始打包中剥离。这样,用户访问应用程序时只加载必要的代码,避免不必要的资源浪费,减小了打包生成的文件体积。
-
优化用户体验 :通过在需要时异步加载组件,可以提高页面的响应速度。当用户与应用程序交互时,
异步组件
可以在后台加载,确保视觉上的延迟最小化,提升用户体验。 -
降低页面的加载时间 :通过将页面划分为多个
异步组件
,可以并行下载组件的代码和资源,从而降低整体页面加载时间。这尤其有助于在低带宽或高延迟环境中提供更好的用户体验。
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
主要用于以下几个方面:
-
代码复用:通过将通用的逻辑和功能提取到高阶组件中,可以在多个组件之间共享和重用代码。
-
渲染劫持:
HOC
可以修改被包装组件的渲染行为,例如添加额外的DOM元素
、修改props
或state
等。 -
条件渲染:
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
刚好相反
在比较函数中,你可以根据具体情况自定义比较逻辑:
- 深比较(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);
- 浅比较(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);
- 特定字段比较:只比较某些特定的
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
会调用计算函数,计算新的结果并返回
-
针对耗时的计算:如果组件需要进行一些耗时 的计算操作,且这些计算结果在依赖项 不发生变化时保持不变,可以使用
useMemo
来缓存这些计算结果。 -
避免不必要的渲染:如果在父组件进行重渲染时,某些子组件的渲染是不必要的,可以使用
useMemo
来缓存子组件的内容,只有在依赖项发生变化时才更新子组件。 -
避免重复请求数据:在组件中进行数据请求时,可以使用
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
:jsimport 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
的原理jsimport 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组件
用作显示jsimport 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组件
用作更改操作jsimport 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
这进行了模块化,状态管理更大化
- 配置
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,
},
})
- 创建切片(模块),输出
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
- 使用
<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()
- 通过
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的工作分为三个简单步骤。
-
只要任何底层数据发生变化,整个UI就会以Virtual DOM表示形式重新渲染。
-
然后计算以前的DOM表示和新的DOM表示之间的差异。
-
计算完成后,真正的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
合成事件机制
合成事件
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
时发生了什么?
- 将
newState
存入pending
微任务队列中 - 判断当前是否处于
batchUpdate
阶段 - 如果当前处于
batchUpdate
阶段,存放在dirtyComponents
中,等待下一次更新时进行处理 - 如果当前没有处于
batchUpdate
阶段,遍历所有待处理的dirtyComponents
,执行updateComponent
更新pending
队列,state
,props
setState
是异步任务还是同步任务?
setState
无所谓异步还是同步- 看是否能命中
batchUpdate
机制 - 判断
isBatchingUpdates
哪些能命中batchUpdate
机制?
- 生命周期(和它调用的函数)
React
中注册的事件(和它调用的函数)React
可以"管理"的入口
哪些无法命中batchUpdate
机制?
setTimeout
,setInterval
等(和它调用的函数)- 自定义的
DOM事件
(和它调用的函数) react
"管不到"的入口
transaction
事务机制
- 无论执行什么方法,先执行
initialize
,然后再执行该方法,最后执行close
initialize
对应isBatchingUpdate = true
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
的方式来开发新的组件
如果你喜欢,帮助到你了,点赞是对我最大帮助,如果你觉得这篇文章以后还能帮助到你,请收藏下它,如果文中有不对的,或者漏了的知识点,请私信我,感激不尽