官方文档:React 官方中文文档
虚拟DOM以及React挂载DOM的方式
- 利用JavaScript脚本操作DOM的行为是性能损耗的大头,故产生虚拟DOM的概念。初始化在内存中得到相应的虚拟DOM树,然后将该结果一次性作用于真实DOM,逻辑层的变动导致部分视图层的改变这渲染逻辑也是经过虚拟DOM加上diff得到一次性结果然后作用于真实DOM,在这里Vue和React都是一样的。
- 可以简单理解:react模块旨在完成虚拟dom的相关逻辑,react-dom模块旨在完成真实DOM和diff的处理
// mount container
rootReactDOM.createRoot(document.querySelector('#root'))
// create VDOM
const data = React.createElement('div', {}, 'data')
// VDOM -> true DOM
root.render(data)
JSX
上述ReactDOM生成虚拟DOM的方式,实际编写代码不方便,于是出现了类似JSX来简化视图层的代码编写(Vue中利用Template模板来完成相同视图层的逻辑构建)
- JSX的识别要么在jsx后缀文件书写相关代码逻辑,要么利用babel工具来进行代码转义..
const data = (
<div>
content<span>something...</span>
</div>
)
root.render(data)
- 模板语法:
{}
,Vue是是{{}}
- 注释:
{/* */}
- 内容可以变量、函数、JSX嵌套 ······
- JSX注意事项
- 严格单标签(
<Tab />
) - 标签名小写
- 唯一根节点 ->
<></>
或React.Fragment
- 部分HTML属性名变动:
class -> className
;for -> htmlFor
······ - 多单词属性用驼峰方式书写,dataset除外
理解类组件
- 组件的概念也就是模块的拆分、抽象和复用,和Vue组件一致,React的组件编写有两种,类组件和函数组件,实际编写代码,React18函数式组件编写更为推崇,但是理解上以类组件会更好。
- 具体类组件的编写,其实就是在面向对象编程,给你一个已知类React.Component,写具体逻辑时,遵照所继承的类的一些规则来扩展业务代码。
简单的父子通信
// A component
class A extends React.Component {
constructor(props) {
super(props)
this.props = props
}
render {
this.props.getData('data')
return (
<div>A component {this.props.info} </div>
)
}
}
// B component
const getData = (data) => {
// result -> data
}
const data = <div>content <A info='something' getData={getData} /> </div>
props相关事项
- 传入组件的参数可以用扩展运算符来传入
const data = {
info: 'something,
getData: (data) => {},
}
const data = <div><A {...data} /></div>
- 单独属性值为true
- 类组件的默认值
// A component
class A extends React.Component {
static defaultProps = {
info: 'default info',
}
constructor(props) {
super(props)
this.props = props
}
render {
this.props.getData('data')
return (
<div>A component {this.props.info} </div>
)
}
}
响应式变量
前端框架必须具体的一个能力就是响应式变量的构造,以及内部实现的动态渲染,react的响应式变量就是构造类的一个私有变量state,再利用其规定指定的方法setState方法来实现响应式
class A extends React.Component {
state {
a: 1,
}
static defaultProps = {
info: 'default info',
}
constructor(props) {
super(props)
this.props = props
}
render {
this.props.getData('data')
return (
<div>A component {this.props.info} </div>
)
}
handleMethod() {
this.setState({ a: 2 })
}
}
如果响应式的值为数组呢?
- react里面的响应式的值为不可变数据集合,比较简单的情况可以利用剩余参数来解决该问题。
class A extends React.Component {
state ={
a: [1, 2, 3]
}
···
handleMethod() {
this.setState({ a: [...a, 4] })
}
}
- 深拷贝
(TODO: 应该有更好的解决方式)
批处理
响应式处理,内部会一次性收集当前变化的量,进入一个处理队列,最终得到一个最终态来进行渲染,避免状态改变多次渲染的情况。
- React18之前,在一些函数执行时机下,不会进行批处理,React18之后解决了这个问题。具体有:Promise、setTimeout、原生事件下
- React提供了防止批处理的函数
class A extends React.Component {
state ={
a: 1
}
···
handleMethod() {
ReactDOM.flushSync(() => {
this.setState({ a: this.state.a + 1 })
this.setState({ a: this.state.a + 1 })
})
// result: run twice render method...
}
render {
console.info('react render.')
return (
<div>A component {this.props.info} </div>
)
}
}
- setState是一个一个异步函数,其第二个参数可以传入回调函数,以便写状态改变之后的相关业务逻辑
- 如果传入setState的值是对象,那么会在响应式内部所收集变化的量里面进行覆盖,如果需要实时根据最新变化的量做逻辑处理,则需要传入一个参数为state的回调函数
class A extends React.Component {
state ={
a: 1
}
···
handleMethod() {
// 1. cover state
this.setState({ a: this.state.a + 1 })
this.setState({ a: this.state.a + 1 })
// result: a -> 2
// 2. real time state
this.setState((state) => { a: this.state.a + 1 })
this.setState((state) => { a: this.state.a + 1 })
// result: a -> 3
}
}
PureComponent shouldComponentUpdate
下面代码,因响应式变量变化,所以会执行一次render渲染
class A extends React.Component {
state ={
a: 1
}
···
handleMethod() {
this.setState({ a: 1 })
}
render {
console.info('react render.')
return (
<div>A component {this.props.info} </div>
)
}
}
上面情况其实响应式的结果都是a变为1,所以从渲染结果上看其实可以优化掉该种情况的渲染,react提供的渲染优化方式有两种:
- PureComponent优化组件
class A extends React.PureComponent {
···
}
- shouldComponentUpdate生命周期函数
class A extends React.Component {
state ={
a: 1
}
···
shouldComponentUpdate = (nextProps, nextState) => {
if (nextState.a === this.state.a)
return false // not render
return true // run render
}
render {
console.info('react render.')
return (
<div>A component {this.props.info} </div>
)
}
}
Refs
框架只是对渲染dom的前置操作做了一层封装,比如先构造虚拟dom或是渲染时机的优化等,但有些时候需要在业务代码里面直接获取dom进行操作,比如input标签的自动聚焦(focus)
- 变量
// A component
class A extends React.Component {
inputRef = React.createRef()
// this.inputRef.current // -> Input DOM
render {
return (
<div>
A component
<input ref={inputRef} />
</div>
)
}
}
- 回调函数
// A component
class A extends React.Component {
inputRef = (dom) => {
dom.focus()
}
// this.inputRef.current // -> Input DOM
render {
return (
<div>
A component
<input ref={inputRef} />
</div>
)
}
}
- 除了可以得到DOM,也可以得到类组件的实例对象
// A component
class A extends React.Component {
···
}
// B component
class B extends React.Component {
refData = React.createRef()
render() {
return <div>content <A ref={refData} /> </div>
}
}
生命周期
组件模式:RenderProps
class A extends React.Component {
state = {
x: 0,
y: 0,
}
constructor(props) {
super(props)
this.props = props
}
componentDidMount = () => {
document.addEventListener('mousemove', this.move)
}
componentWillUnMount = () => {
document.removeEventListener('mousemove', this.move)
}
move = (e) => {
this.setState({
x: e.pageX,
y: e.pageY,
})
}
render() {
return (
<React.Fragment>
{ this.props.render(this.state.x, this.state.y)}
</React.Fragment>
)
}
}
class B extends React.Component {
render() {
return <A render={(x, y) => {
<div>{x}, {y}</div>
}} />
}
}
组件模式:HOC
参数为组件,返回新组件
function hocFun(WithComponent) {
return class extends React.Component {
state = {
x: 0,
y: 0,
}
componentDidMount = () => {
document.addEventListener('mousemove', this.move)
}
componentWillUnMount = () => {
document.removeEventListener('mousemove', this.move)
}
move = (e) => {
this.setState({
x: e.pageX,
y: e.pageY,
})
}
render() {
return <WithComponent {...this.state} />
}
}
}
class A extends React.Component {
render() {
return (
<div>
{this.props.x}, {this.props.y}
</div>
)
}
}
const C = hocFun(A)
class B extends React.Component {
render() {
return <C />
}
}
Context通信
const ContextData = React.createContext()
class A extends React.Component {
state = {
info: ‘something’
}
render() {
return (
A
<ContextData.Provider value={this.state.info}>
<B />
</ContextData.Provider>
)
}
}
class B extends React.Component {
render() {
return (
B<C />
)
}
}
class C extends React.Component {
static contextType = ContextData
componentDidMount = () => {
console.lof(this.context)
}
render() {
return (
C
<ContextData.Consumer>{ value => value }</ContextData.Consumer>
)
}
}
// render -> ABCsomething
函数组件
useState
响应式变量的声明和改变
import { useState } from 'React'
const A = () => {
const [a, setA] = useState('a')
return <div>a value: {a}</div>
}
和类组件的差异
- 改变值的时候不会merge,而是覆盖
- 初始值可以值回调函数,但不能是函数否则每次改变都会重新执行一遍该函数
- 同样存在批处理,也有防止批处理的函数
import { useState } from 'React'
import { flushSync } from 'ReactDDOM'
const A = () => {
const [a, setA] = useState('a')
const [b, setB] = useState('b')
flushSync(() => {
setA('aa')
})
flushSync(() => {
setB('bb')
})
return <div>a value: {a}</div>
}
- 传入的是值,渲染前会存在覆盖情况,如果传入的是函数,则会保留上一次的状态值计算
useEffect
副作用Hook
- 模拟各个生命周期的执行时机
import { useState, useEffect } from 'React'
const A = () => {
useEffect(() => {
// mount || update
return () => {
// beforeUpdate || unMount
// clear sider function...
}
})
return <div>a value: {a}</div>
}
- 关于第二个数组参数
- 如果不设置,那么在update时期都会执行
- 如果设置空数组(且回调函数中没有响应式变量),则只在mount时期执行一次
- 如果useEffect回调函数中存在响应式变量,那么第二个参数数组中应当有该响应式变量
- useLayoutEffect
- useEffect:渲染并绘制到屏幕之后执行,异步
- useLayoutEffect:渲染之后,但绘制到屏幕之前执行,同步
- 一般如果回调函数中有DOM的相关操作并且会改变样式,用后者,避免DOM渲染闪屏/白屏,但前者性能方面更好
useRef
- 作用在DOM上的回调函数,则为DOM实例
const A = () => {
const elementFun = (dom) => {
// dom -> input DOM
}
return <div><input type="text" ref={elementFun} /></div>
}
- 作用在DOM上且为useRef函数,则为带有键为current值为DOM本身的对象
import { useRef } from 'React'
const A = () => {
const obj = useRef()
// obj => { current: inputDOM }
return <div><input type="text" ref={obj} /></div>
}
- 不可以作用在组件上,但是子组件有React.forwardRef(组件转发)可以
import { useRef } from 'React'
const Son = React.forwardRef((props, ref) => {
return <div><input type="text" ref={ref} /></div>
})
const A = () => {
const obj = useRef()
// obj => { current: inputDOM }
return <Son ref={obj} />
}
- 利用useRef创建的普通变量,可以具备“记忆”功能,类似于类的实例属性。利用这一点可以设置一个flag来实现组件仅更新时触发的代码执行时机
import { useRef, useEffect } from 'React'
const A = () => {
const obj = useRef(false)
const inputOnClick = () => {
obj.current = true
}
useEffect(() => {
if (obj.current) {
// only update -> run...
}
})
return <div><input type="text" onClick={inputOnClick} /></div>
}
useContext
跨组件通信
import { useContext, createContext } from 'React'
// not Provider value -> render default value
const C = createContext(‘default value…’)
const GSon = () => {
const value = useContext(C)
// value -> something…
return <div>GSon template…</div>
}
const Son = () => {
return <GSon />
}
const A = () => {
return (
<C.Provider value=’something…’>
<Son ref={obj} />
</C.Provider>
)
}
memo
类似类组件中的PureComponent
性能优化组件
- 函数组件中当响应变量的值没有发生改变,不会重新渲染,和类组件不一样
- 当组件的值发生改变才进行render,反之不进行render
import { useState, memo } from 'React'
const Son = memo(() => {
return <div>son</div>
})
// continue click -> continue run ↓
// const Son = () => {
// return <div>son</div>
// }
const A = () => {
const [count, setCount] = useState(0)
const clickEvent = () => {
setCount(1)
}
return (
<div>
<button onClick={clickEvent} >click</button>
<Son count={count} />
</div>
)
}
useMemo和useCallback
React函数式组件在重新渲染时候,代码执行会将函数体重新执行,即便结果不是重新更新DOM。如果函数体的代码逻辑复杂会带来不小的性能损耗,如果能够对这过程中的部分“量”进行“记忆”,则会较大提升性能。
响应式变量内部会自动进行记忆,但是如果是非响应式变量,比如一个对象或是一个函数传入组件,这其实也是改变了的,原因是对象的引用改变,依然会引起DOM的重新渲染,而利用useMemo或是useCallback创建的对象(对象、数组、函数)则会保留记忆功能。
- useCallback:传入回调函数
- useMemo:传入一个必须带有返回值的函数
- 两者的第二个参数都是依赖数组,同useEffect
import { useCallback, useMemo } from 'React'
const A = () => {
// 1. every render -> run
const fun = () => {}
// 2. useCallback (ignore const error...)
const fun = useCallback(() => {}, [])
// 3. useMemo (ignore const error...)
const fun = useMemo(() => () => {}, [])
const a = useMemo(() => [1, 2, 3], [])
return <div onClick={fun} a={a}></div>
}
useReducer
管理多个有关联的响应式变量
import { useReducer } from 'React
const loginState = { isLogin: true, isLogout: false }
const loginReducer = (state, action) => {
switch(action.type) {
case ‘login’:
return { isLogin: true, isLogout: false }
case ‘logout’:
return { isLogin: false, isLogout: true }
default:
return new Error()
}
}
const A = () => {
const [state, LoginDispatch] = useReducer(loginReducer, loginState)
const clickEvent = () => {
loginDispatch({ type: state.isLogin ? ‘logout’ : ‘login’ })
}
return (
<button onClick={clickEvent}>{state.isLogin ? ‘login’, ‘logout’}</button>
)
}
并发模式
- React18之前,渲染是一个单一的、不间断的、同步的事务,一旦渲染开始,就不能被中断
- React18引入并发模式,它允许你将更新作为一个transitions,这会告诉React他们可以被中断执行。这样可以把紧急的任务先更新你,不紧急的任务后更新
startTransition
import { startTransition } from 'React'
const A = () => {
...
const fun = () => {
// 紧急任务
setA('')
// 不紧急任务(将内部的任何非紧急状态更新标记为 Transition)
startTransition(() => setB(''))
}
return <div></div>
}
useTransition和useDeferredValue
- useTransition返回一个状态值表示过渡任务的等待状态,以及一个启动该过渡任务的函数
import { useTransition } from 'React'
const A = () => {
...
const [pending, startTransition] = useTransition()
const fun = () => {
// 紧急任务
setA('')
// 不紧急任务(将内部的任何非紧急状态更新标记为 Transition)
startTransition(() => setB(''))
}
return <div></div>
}
- useDeferredValue接受一个值,并返回该值的新副本,该副本将推迟到更紧急地更新之后
import { useDeferredValue, useState } from 'React'
const A = () => {
const [a, setA] = useState('')
// aD => 不紧急时候的值(同a),也就是延迟之后的值
const aD = useDeferredValue(a)
...
return <div></div>
}
自定义hook
- 命名通常使用use开头
import { useState, useEffect } from 'React'
const useMouseXY = () => {
const [x, setX] = usestate(0)
const [y, setY] = usestate(0)
useEffect(() => {
function move(e) {
setX(e.pageX)
setY(e.pageY)
}
document.addEventListener(‘mousemove’, move)
return () => document.removeEventListener(‘mousemove’, move)
}, [])
return { x, y }
}
// use
const { x, y } = useMouseXY()
lazy
当模块化引入(import)组件,但在实际代码中未使用到,代码内部逻辑仍会执行,这个时候可以利用lazy使组件异步化加载,从而达到性能优化效果
import { lazy } from 'react';
const AuthRoute = lazy(() => import(‘./AuthRoute’));
const ErrorPage = lazy(() => import(‘@/components/ErrorBoundary’));
const NotFound = lazy(() => import(‘@/pages/404’));
Suspense
组件加载中或是切换过程中的“中间态”
import { Suspense, useState } from 'react';
import { RouterProvider } from 'react-router-dom';
import router from '@/router/index';
import Loading from '@/components/Loading';
function MyApp() {
const [show, setShow] = useState(true)
const clickEvent = () => setShow(!show)
return (
<>
<button onClick={clickEvent}>btn</button>
<Suspense fallback={<Loading />}>
{show ? <RouterProvider router={router} /> : <>other template</>}
</Suspense>
</>
);
}
export default MyApp;
结合startTransition使用:
如果需要切换过程中不展示loading的加载态,则将上述代码中的切换逻辑转为并发异步即可
const clickEvent = () => startTransition(() => setShow(!show))
错误边界捕获
如果编写的组件内部报错,那么react渲染会清除根节点DOM,React官方文档暂时只有类组件编写的示例代码。其核心逻辑在类组件上支持关键的生命周期方法getDerivedStateFromError()和componentDidCatch(),无法将错误边界编写为函数式组件,同时以下几种错误情况,也捕获不到:
- 异步代码
- 事件处理函数
- 服务器组件
- Error Boundary自身
具体的完整封装 TODO:
ReactDOM
createPortal
- 可以指定节点挂载到指定目标DOM节点
// template
const A = () => ReactDOM.createPortal(<div>info</div>, document.body)
- 对于一些全局组件(message),DOM的挂在可能不在root根节点内,自定义挂在指定DOM节点,除了上述createPortal方式,还有react的createRoot方式
评论区