React 高级用法

课程目标

  1. 会⽤React写项⽬,较熟练使⽤React配套技术栈,有⼀定实际开发经验;
  2. 能够针对复杂的业务场景制定出较为规范的逻辑架构;
  3. 基于实际开发场景,搭建配套脚⼿架,能够基于当前实际开发场景优化架构设计,制定团队规范;
  4. 对前沿技术有⾜够的敏感度,保证项⽬的可扩展性与健壮性;
  5. 精通⼀个框架的底层设计,熟悉多个框架的实际设计及对⽐;其他⽬标:
  6. 深⼊了解React技术栈相关的知识点,知道React⽣态中发展现状,能够对⾯试所提的问题举⼀反三;

课程大纲

  1. ⾼阶组件的⽤法及封装
    
    1. Hooks详解
      
      1. 异步组件
        
        1. React 18 新特性
          

知识要点

⾼阶组件⽤法及封装

⾼阶组件(HOC)是 React 中⽤于复⽤组件逻辑的⼀种⾼级技巧。HOC ⾃身不是 React API 的⼀部分,它是⼀种基于 React 的组合特性⽽形成的设计模式。
简单点说,就是组件作为参数,返回值也是组件的函数,它是纯函数,不会修改传⼊的组件,也不会使⽤继承来复制其⾏为。相反,HOC 通过将组件包装在容器组件中来组成新组件。HOC 是纯函数,没有副作⽤。

使⽤HOC的原因

  1. 抽取重复代码,实现组件复⽤:相同功能组件复⽤
    
    1. 条件渲染,控制组件的渲染逻辑(渲染劫持):权限控制。
      
      1. 捕获/劫持被处理组件的⽣命周期,常⻅场景:组件渲染性能追踪、⽇志打点。
        

HOC实现⽅式

属性代理

使⽤组合的⽅式,将组件包装在容器上,依赖⽗⼦组件的⽣命周期关系来;

  1. 返回stateless的函数组件
  2. 返回class组件
  • 操作 props
    可以通过属性代理,拦截父组件传递过来的 props 并进行处理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 返回一个无状态的函数组件
    function HOC (WrappedComponent) {
    const newProps = { type: 'HOC' }
    return <WrappedComponent {...props} {...newProps} />
    }

    // 返回一个有状态的 class 组件
    function HOC (WrappedComponent) {
    return class extends React.Component {
    render () {
    const newProps = { type: 'HOC' }
    return <WrappedComponent {...this.props} {...newProps} />
    }
    }
    }
  • 抽象 state
    通过属性代理无法直接操作原组件的 state,可以通过 props 和 callback 抽象 state

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function HOC (WrappedComponent) {
return class extends React.Component {
constructor (props) {
super(props)
this.state = {
name: ''
}
this.onChange = this.onChange.bind(this)
}

onChange = (event) => {
this.setState({
name: event.target.value
})
}

render() {
const newProps = {
name: {
value: this.state.name,
onChange: this.onChange
}
}
return <WrappedComponent {...this.props} {...newProps} />
}
}
}

@HOC
class Example extends Component {
render () {
return <input name="name" {...this.props.name} />
}
}
  • 通过 props 实现条件渲染
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import * as React from 'react'

export default function HOC (WrappedComponent) {
return (props) => (
<div>
{
props.isShow ? (
<WrappedComponent {...props} />
) : (
<div>暂无数据</div>
)
}
</div>
)
}
  • 其他元素 wrapper 传入的组件
1
2
3
4
5
6
7
8
9
10
11
function withBackgroundColor (WrappedComponent) {
return class extends React.Component {
render () {
return (
<div style={{backgroudColor: '#ccc'}}>
<WrappedComponent {...this.props} {...newProps} />
</div>
)
}
}
}
反向继承

使⽤⼀个函数接受⼀个组件作为参数传⼊,并返回⼀个继承了该传⼊组件的类组件,且在返回组件的 render() ⽅法中返回 super.render() ⽅法

1
2
3
4
5
6
7
function HOC (WrappedComponent) {
return class extends WrappedComponent {
render () {
return super.render()
}
}
}
  1. 允许HOC通过this访问到原组件,可以直接读取和操作原组件的state/ref等;
    
    1. 可以通过super.render()获取传⼊组件的render,可以有选择的渲染劫持;
      
      1. 劫持原组件⽣命周期⽅法
        
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function HOC (WrappedComponent) {
const didMount = WrappedComponent.prototype.componentDidMount
// 继承传入组件
return class extends WrappedComponent {
async componentDidMount() {
// 劫持 WrappedComponent 组件的生命周期
if (didMount) {
await didMount.apply(this)
}
}
render () {
// 使用 super 调用传入组件的 render 方法
return super.render()
}
}
}
  • 读取/操作原组件的 state
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function HOC (WrappedComponent) {
const didMount = WrappedComponent.prototype.componentDidMount
// 继承传入组件
return class extends WrappedComponent {
async componentDidMount() {
if (didMount) {
await didMount.apply(this)
}
// 将 state 中的 number 值修改为 2
this.setState({number:2})
}
render () {
// 使用 super 调用传入组件的 render 方法
return super.render()
}
}
}
  • 条件渲染
1
2
3
4
5
6
7
8
9
const HOC = (WrappedComponent) => class extends WrappedComponent {
render() {
if (this.props.isRender) {
return super.render()
} else {
return <div>暂无数据</div>
}
}
}
  • 修改 react 数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function HOC (WrappedComponent) {
return class extends WrappedComponent {
render () {
const tree = super.render()
const newProps = {}
if (tree && tree.type === 'input') {
newProps.value = 'something here'
}
const props = {
...tree.props,
...newProps
}
const newTree = React.cloneElement(tree, props, tree.props.childern)
return newTree
}
}
}

属性代理和反向继承对⽐

  1. 属性代理:从“组合”⻆度出发,有利于从外部操作wrappedComp,可以操作props,或者在 wrappedComp 外加⼀些拦截器(如条件渲染等);
    
    1. 反向继承:从“继承”⻆度出发,从内部操作wrappedComp,可以操作组件内部的state,⽣命周期和render等,功能能加强⼤;
      

举个例子

  • 页面复用(属性代理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// views/PageA.js
import React from 'react'
import fetchMovieListByType from '../lib/utils'
import MovieList from '../components/MoviesList'

export default class PageA extends React.Component {
state = {
movieList: []
}
/* ... */
async componentDidMount () {
const moveList = await fetchMovieListByType('comedy')
this.setState({
moveList
})
}
render () {
return <MovieList data={this.state.movieList} emptyTips="暂无喜剧" />
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// views/PageB.js
import React from 'react'
import fetchMovieListByType from '../lib/utils'
import MovieList from '../components/MoviesList'

export default class PageB extends React.Component {
state = {
movieList: []
}
/* ... */
async componentDidMount () {
const moveList = await fetchMovieListByType('action')
this.setState({
moveList
})
}
render () {
return <MovieList data={this.state.movieList} emptyTips="暂无动作片" />
}
}

以上冗余代码过多,使用HOC 代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// HOC
import React from 'react'
const withFetchingHOC = (WrappedComponent, fetchingMethod, defaultProps) => {
return class extends React.Component {
async componentDidMount () {
const data = await fetchingMethod()
this.setState({data})
}
render () {
return (
<WrappedComponent data={this.state.data} {...defaultProps} {...this.props} />
)
}
}
}
1
2
3
4
5
6
7
8
// views/PageA.js
import React from 'react'
import withFetchingHOC from '../hoc/withFetchingHOC'
import fetchMovieListByType from '../lib/utils'
import MovieList from '../components/MoviesList'

const defaultProps = {emptyTips: '暂无喜剧'}
export default withFetchingHOC(MovieList, fetchMovieListByType('comedy'), defaultProps)
1
2
3
4
5
6
7
8
// views/PageOthers.js
import React from 'react'
import withFetchingHOC from '../hoc/withFetchingHOC'
import fetchMovieListByType from '../lib/utils'
import MovieList from '../components/MoviesList'

const defaultProps = {...}
export default withFetchingHOC(MovieList, fetchMovieListByType('some-other-type'), defaultProps)

更符合 ⾥⽒代换原则(Liskov Substitution Principle LSP),任何基类可以出现的地⽅,⼦类⼀定可以出现。LSP是继承复⽤的基⽯,只有当衍⽣类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复⽤,⽽衍⽣类也能够在基类的基础上增加新的⾏为

  • 权限控制(属性代理)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React from 'react'
import { whiteListAuth } from '../lib/utils' // 鉴权方法

function AuthWrapper (WrappedComponent) {
return class AuthWrappedComponent extends React.Component {
constructor (props) {
super(props)
this.state = {
premissionDeied: -1
}
}

async componentDidMount () {
try {
await whiteListAuth()
this.setState({
premissionDeied: 0
})
} catch (e) {
this.setState({
premissionDeied: -1
})
}
}

render () {
if (this.state.premissionDeied === -1) {
return null
}
return <WrappedComponent {...this.props} />
}
}
}
  • 组件渲染性能(反向继承)
    如何计算一个组件 render 期间的渲染耗时
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import React from 'react'

// Home 组件
class Home extends React.Component {
render () {
return <h1>Hello World.</h1>
}
}

function withTiming (WrappedComponent) {
let start, end

return class extends WrappedComponent {
constructor (props) {
super(props)
start = 0
end = 0
}
componentWillMount () {
if (super.componentWillMount) {
super.componentWillMount()
}
start = +Date.now()
}
componentDidMount () {
if (super.componentDidMount) {
super.componentDidMount()
}
end = +Date.now()
console.log(`${WrappedComponent.name} 组件渲染时间为${end - start}ms`)
}
render () {
return super.render()
}
}
}

export default withTiming(Home)

Hooks 详解

Hooks 是 react 16.8 以后新增的钩子 API
目的:增加代码的可复⽤性,逻辑性,弥补函数式组件没有⽣命周期,没有数据管理状态 state 的缺陷。

为什么要使⽤ Hooks?
1. 开发友好,可扩展性强,抽离公共的⽅法或组件,Hook 使你在⽆需修改组件结构的情况下复⽤状态逻辑;
2. 函数式编程,将组件中相互关联的部分根据业务逻辑拆分成更⼩的函数;
3. class 更多作为语法糖,没有稳定的提案,且在开发过程中会出现不必要的优化点,Hooks ⽆需学习复杂的函数式或响应式编程技术;

常见 Hooks

useState
6
1
const [state, setState] = useState(initialState);
  1. setState ⽀持函数式组件有⾃⼰的 state;
  2. ⼊参:具体值或⼀个函数;
  3. 返回值:数组,第⼀项是 state 值,第⼆项负责派发数据更新,组件渲染;

注意:setState 会让组件重新执⾏,所以⼀般需要配合 useMemo 或 useCallback;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const DemoState = (props) => {
/* number 为此时 state 读取值,setNumber 为派发更新的函数 */
const [number, setNumber] = useState(0) /* 0 为初始值 */
return (
<div>
<span>{number}</span>
<button onClick={() => {
setNumber(number + 1)
console.log(number) // 0
}} />
</div>
)
}

const a = 1
const DemoState = (props) => {
/* useState 第一个参数如果是函数 则处理复杂逻辑,返回值为初始值 */
const [number, setNumber] = useState(() => {
return a === 1 ? 1 : 2
}) /* 1 为初始值 */
return (
<div>
<span>{number}</span>
<button onClick={() => {
setNumber(number + 1)
}} />
</div>
)
}
useEffect
  1. 使⽤条件:当组件init、dom render完成、操纵dom、请求数据(如componentDidMount)等;
    
    1. 不限制条件,组件每次更新都会触发 useEffect --> componentDidUpdate 与 componentwillreceiveprops;
      
      1. useEffect 第⼀个参数为处理事件,第⼆个参数接收数组,为限定条件,当数组变化时触发事件,为[]只在组件初始化时触发;
        
        1. useEffect 第⼀个参数有返回时,⼀般⽤来消除副作⽤(如去除定时器、事件绑定等);
          

注意:useEffect⽆法直接使⽤async await

1
2
3
4
5
6
7
8
9
useEffect(() => {
const fetchData = async () => {
const response = await fetch('https://xxx.com')
const json = response.json()
setData(json)
}

fetchData().catch(console.error)
}, [])
useLayoutEffect

渲染更新之前的 useEffect
useEffect: 组件更新挂载完成 -> 浏览器dom 绘制完成 -> 执⾏useEffect回调 ;
useLayoutEffect : 组件更新挂载完成 -> 执⾏useLayoutEffect回调-> 浏览器dom 绘制完成;

渲染组件
1. useEffect:闪动;
2. useLayoutEffect:卡顿;

1
2
3
4
5
6
7
8
9
10
11
12
13
const DemoUseLayoutEffect = () => {
const target = useRef()
useLayoutEffect(() => {
// 需要在 DOM 绘制之前,移动 DOM 到指定位置
const { x, y } = getPosition() // 获取要移动的x,y 坐标
animate(target.current, {x, y})
}, [])
return (
<div>
<span ref={target} className="animate"></span>
</div>
)
}
useRef

⽤来获取元素、缓存数据;
⼊参可以作为初始值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 获取元素
const DemoUseRef = () => {
const dom = useRef(null)
const handerSubmit = () => {
console.log(dom.current) // <div>表单组件</div> dom 节点
}
return (
<div>
<div ref={dom}>表单组件</div>
<button onClick={() => handerSubmit()}>提交</button>
</div>
)
}

// 缓存数据
// 不同于 useState,useRef 改变值不会使组件重新渲染
const currentRef = useRef(InitialData)
currentRef.current = newValue
useContext

⽤来获取⽗级组件传递过来的 context 值,这个当前值就是最近的⽗级组件 Provider 的 value;
从 parent comp 获取 ctx ⽅式;
1. useContext(Context);
2. Context.Consumer;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Context.Consumer
const DemoContext = () => {
return (
<Context.Consumer>
{(value) => <div>my name is {value.name}</div>}
</Context.Consumer>
)
}
// useContext(Context)
const DemoUseContext = () => {
const value = useContext(Context)
return <div>my name is {value.name}</div>
}

export default () => {
return (
<div>
<Context.Provider value={{name: 'aaa'}}>
<DemoContext />
<DemoUseContext />
</Context.Provider>
</div>
)
}
useReducer

⼊参:

  1. 第⼀个为函数,可以视为 reducer,包括 state 和 action,返回值为根据 action 的不同⽽改变后的 state;
  2. 第⼆个为 state 的初始值;

出参:
4. 第⼀个更新后的 state 值;
5. 第⼆个是派发更新的 dispatch 函数;执⾏ dispatch 会导致组件 re-render;(另⼀个是useState)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const DemoUseReducer = () => {
/* number 为更新后的 state 值,dispatchNumber 为当前的派发函数 */
const [number, dispatchNumber] = useReducer((state, action) => {
const { payload, name } = action
// return 的值为新的 state
switch (name) {
case 'a':
return state + 1
case 'b':
return state - 1
case 'c':
return payload
}
return state
}, 0)

return (
<div>
{/* 派发更新 */}
<button onClick={() => dispatchNumber({name: 'a'})}>增加</button>
<button onClick={() => dispatchNumber({name: 'b'})}>减少</button>
<button onClick={() => dispatchNumber({name: 'c', payload: 666})}>赋值</button>
{/* 把 dispatch 和 state 传递给子组件 */}
<MyChildren dispatch={dispatchNumber} state={{number}} />
</div>
)
}

业务中经常将 useReducer + useContext 代替 Redux

useMemo

⽤来根据 useMemo 的第⼆个参数 deps(数组)判定是否满⾜当前的限定条件来决定是否执⾏第⼀个 cb;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// selectList 不更新时,不会重新渲染,减少不必要的循环渲染
useMemo(() => (
<div>
{
selectList.map((i, v) => (
<span key={v} className={style.listSpan}>{i.patenName}</span>
))
}
</div>
), [selectList])

// 减少组件更新导致函数重新声明
const DemoUserMemo = () => {
/* 用 useMemo 包裹之后的 log 函数可以避免了每次组件更新再重新声明,可以限制上下文的执行 */
const newLog = useMemo(() => {
const log = () => {
console.log(123)
}
return log
}, [])
return <div onClick={() => newLog()}></div>
}
useCallback

useMemo 返回 cb 的运⾏结果;
useCallback 返回 cb 的函数;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { useState, useCallback } from 'react'

function Button (props) {
const { handleClick, children } = props
console.log('Button -> render')
return <button onClick={handleClick}>{children}</button>
}

const MemoizedButton = React.memo(Button)

export default function Index() {
const [clickCount, increaseCount] = useState(0)
const handleClick = useCallback(() => {
console.log('handleClick')
increaseCount(clickCount + 1)
}, [])

return (
<div>
<p>{clickCount}</p>
<MemoizedButton handleClick={handleClick()}>click</MemoizedButton>
</div>
)
}

Hooks 实战

所有依赖都必须放在依赖数组中么?

useEffect 中,默认有个共识: useEffect 中使⽤到外部变量,都应该放到第⼆个数组参数中。

Solution:
1. 不要使⽤ eslint-plugin-react-hooks 插件,或者可以选择性忽略该插件的警告;
2. 只有⼀种情况,需要把变量放到 deps 数组中,那就是当该变量变化时,需要触发 useEffect 函数执⾏。⽽不是因为 useEffect 中⽤到了这个变量!

尽量不要用 useCallback
  1. useCallback ⼤部分场景没有提升性能
    必须用 React.memo wrapper 子组件,才能避免在参数不变的情况下,不重复渲染
  2. useCallback 依赖层层传递导致代码可读性变差
useMemo 建议适当使用

在 deps 不变且非简单的基础类型运算的情况下使用

6
1
2
3
4
const a = 1;
const b = 2;
// const c = useMemo(() => a + b, [a, b])
const c = a + b // 内存消耗小
useState 的正确使用姿势
  1. 能⽤其他状态计算出来就不⽤单独声明状态。⼀个 state 必须不能通过其它 state/props 直接计算出来,否则就不⽤定义 state
    
    1. 保证数据源唯⼀,在项⽬中同⼀个数据,保证只存储在⼀个地⽅
      
      1. useState 适当合并
        

自定义 Hooks

注意:⾃定义 Hooks 本质上还是实现⼀个函数,关键在于实现逻辑
一般实现效果如:

1
const [a[,b,c...] ] = useXXX(arg[,arg2, ...])
setTitle hook
6
1
2
3
4
5
export default function useTitle(title) {
useEffect(() => {
document.title = title
}, [])
}
useScroll hooks
6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { useState, useEffect } from 'react'

export default function useScroll (scrollRef) {
const [pos, setPos] = useState([0, 0])

useEffect(() => {
function handleScrolle (e) {
setPos([scrollRef.current.scrollerLeft, scrollRef.current.scrollTop])
}
scrollRef.current.addEventListener('scroll', handleScrolle)

return () => {
scrollRef.current.removeEventListener('scroll', handleScrolle)
}
}, [])

return pos
}

Hooks VS HOC

  1. Hook 最典型的就是取代掉⽣命周期中⼤多数的功能,可以把更相关的逻辑放在⼀起,⽽⾮零散在各个⽣命周期⽅法中;
    
    1. ⾼阶组件可以将外部的属性功能到⼀个基础 Component 中,更多作为扩展能⼒的插件(如 react-swipeable-views 中的 autoPlay ⾼阶组件,通过注⼊状态化的 props 的⽅式对组件进⾏功能扩展,⽽不是直接将代码写在主库中);
      
      1. Hook 的写法可以让代码更加紧凑,更适合做 Controller 或者需要内聚的相关逻辑,⼀般与⽬标组件内强依赖,HOC更强调对原先组件能⼒的扩展;
        
        1. ⽬前 Hook 还处于相对早期阶段(React 16.8.0 才正式发布Hook 稳定版本),⼀些第三⽅的库可能还暂时⽆法兼容 Hook;
          

异步组件

随着项⽬的增⻓,代码包也会随之增⻓,尤其是在引⼊第三⽅的库的情况下,要避免因体积过⼤导致加载时间过⻓。
React16.6中,引⼊了 React.lazy 和 React.Suspense 两个API,再配合动态 import() 语法就可以实现组件代码打包分割和异步加载。

传统模式:渲染组件 -> 请求数据 -> 再渲染组件
异步模式:请求数据 -> 渲染组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { lazy, Suspense } from 'react'
// lazy 和 Suspense 配套使用,react 原生支持代码分割
const About = lazy(() => import(/* webpackChunkName: "about" */'./About'))

class App extends React.Component {
render () {
return (
<div>
<Suspense fallback={<div>loading</div>}>
<About />
</Suspense>
</div>
)
}
}

前置基础

  1. 动态 import
    相对于静态 import 的 import XX from XXX,动态 import 指在运⾏时加载
  2. 错误边界
    React V16 中引⼊,部分 UI 的 JS 错误不会导致整个应⽤崩溃;
    错误边界是⼀种 React 组件,错误边界在渲染期间、⽣命周期⽅法和整个组件树的构造函数中捕获错误,且会渲染出备⽤ UI ⽽不是崩溃的组件。

手写异步组件

Suspense 组件需要等待异步组件加载完成再渲染异步组件的内容。
1. lazy wrapper 住异步组件,React 第⼀次加载组件的时候,异步组件会发起请求,并且抛出异常,终⽌渲染;
2. Suspense ⾥有 componentDidCatch ⽣命周期函数,异步组件抛出异常会触发这个函数,然后改变状态使其渲染 fallback 参数传⼊的组件;
3. 异步组件的请求成功返回之后,Suspense 组件再次改变状态使其渲染正常⼦组件(即异步组件);

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// comp lazy
import React, { useEffect } from 'react'
export function lazy (fn) {
const fecher = {
status: 'pending',
result: null,
promise: null,
}
return function MyComponent() {
const getDataPromise = fn()
fetcher.promise = getDataPromise
getDataPromise.then(() => {
fecher.status = 'resolved'
fetcher.result = res.default
})
useEffect(() => {
if (fecher.status === 'pending') {
throw fetcher
}
}, [])
if (fecher.status === 'resolved') {
return fecher.result
}
return null
}
}
1
2
3
4
5
6
7
8
// comp About
const About = lazy(() => new Promise(resolve => {
setTimeout(() => {
resolve({
default: <div>component content</div>
})
}, 1000)
}))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// comp Suspense
import React from 'react'

export default class Suspense extends React.Component {
state = {
isRender: true
}

componentDidCatch (error, errorInfo) {
this.setState({isRender: false})
error.promise.then(() => {
this.setState({isRender: true})
})
}

render () {
const { fallback, children } = this.props
const { isRender } = this.state
return isRender ? children : fallback
}
}

React18 新特性

2021.11.15 React 18 升级到了beat版本,当前 17.0.2

发布节奏:

  • 库的 Alpha 版本:当天可⽤
  • 公开的 Beta 版:⾄少⼏个⽉
  • RC 版本:⾄少在 Beta 版发布后的⼏周
  • 正式版:⾄少在 RC 版本发布之后的⼏周

主要改动包括:
1. Automatic batching(⾃动批量更新)
2. startTransition
3. ⽀持 React.lazy 的 SSR 架构
4. Concurrent Mode (并发渲染、可选)

Automatic batching

将多个状态更新合并成⼀个重新渲染以取得更好的性能的⼀种优化⽅式;

  1. V18 前默认不 batching 的 scene:
    1. promise;
    2. setTimeout;
    3. 原⽣事件处理(native event handlers);
  2. V18 所有更新自动 batching

若不想 batching

1
2
3
4
5
6
7
8
9
10
11
12
import { flushSync } from 'react-dom'

function handleClick () {
flushSync(() => {
setCounter(c => c + 1)
})
// React has updated the DOM by now
flushSync(() => {
setFlag(f => !f)
})
// React has updated the DOM by now
}

startTransition

可以让我们的⻚⾯在多数据更新⾥保持响应。这个 API 通过标记某些更新为”transitions”,来提⾼⽤户交互;

实际:可以让我们的⻚⾯在展示时时刻保持re-render;

Example:我们更新 input 的 value 的同时⽤这个 value 去更新了⼀个有 30000 个 item 的 list。然⽽这种多数据更新让⻚⾯⽆法及时响应,也让⽤户输⼊或者其他⽤户交互感觉很慢。
Solution:

1
2
3
4
// 紧急更新:暂时用户输入
setInputValue(e.target.value)
// 非紧急更新:暂时结果
setContent(e.target.value)

V18 前:update 的优先级一致
V18:支持优先级手动设置

1
2
3
4
5
6
7
8
import {startTransition} from 'react'

setInputValue(input)

startTransition(() => {
setSearchQuery(input)
})
// 等同于先 setInputValue(e.target.value) 后执行 setContent(e.target.value)

react中的 update:

  • Urgent updates:reflect direct interaction, like typing, clicking, pressing, and so on; ● Transition updates:transition the UI from one view to another;
  • 误区
  1. 与setTimeout的区别
    startTransition 不会被放到下⼀次 event loop,是同步⽴即执⾏的,这也就意味着,⽐ timeout update 更早,低端机体验明显;

使⽤场景

  1. slow rendering:re-render 需要耗费⼤量的⼯作量;
  2. slow network:需要较⻓时间等待 response 的情况;

⽀持 React.lazy 的 SSR 架构

SSR场景
react 的 SSR(server side render)

  1. server:获取数据;
  2. server:组装返回带有 HTML 的接⼝;
  3. client:加载 JavaScript;
  4. client:hydration,将客户端的JS与服务端的HTML结合;
  • V18前:按序执⾏;
  • V18:⽀持拆解应⽤为独⽴单元,不影响其他模块;正常加载界⾯

SSR问题

  1. server:获取数据; –> 按序执⾏,必须在服务端返回所有 HTML;
  2. client:加载 JavaScript; –> 必须JS加载完成;
  3. client:hydration,将客户端的 JS 与服务端的 HTML 结合; –> hydrate后才能交互;

流式 HTML & 选择性 hydrate

  1. 流式HTML
  2. client 进⾏选择性的 hydration:
  3. JS选择性加载
  4. hydration 之前要求交互
  5. 记录操作⾏为,并优先执⾏Urgent comp的hydration;

Concurrent Mode (并发渲染、可选)

Concurrent Mode(以下简称CM)什么是 CM 和 suspense?在2019年 react conf提出了实验性的版本来⽀持CM 和 Suspense(可以理解为等待代码加载,且指定加载界⾯)

  • CM:
    • 可帮助应⽤保持响应,并根据⽤户的设备性能和⽹速进⾏适当的调整。
    • 阻塞渲染:如UI update,需要先执⾏对应视图操作,如更新DOM;
      solution:
      a. debounce:输⼊完成后响应,输⼊时不会更新;
      b. throttle:功率低场景卡顿;
      可中断渲染(CM):
      a. CPU-bound update: (例如创建新的 DOM 节点和运⾏组件中的代码):中断当前渲染,切换更⾼优先级;
      b. IO-bound update: (例如从⽹络加载代码或数据):response前先在内存进⾏渲染;
  • suspense
    以声明的⽅式来“等待”任何内容,包括数据

补充知识点

使用 useReducer + useContext 实现简易 Redux

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// simpleRedux

import React, { useReducer, createContext } from "react";

const Context = createContext();

const initState = {};

const reducer = (state, action) => {
switch (action.type) {
case "setState":
return { ...state, ...action.payload };
default:
return state;
}
};

export default Context;

export const Provider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initState);

return (
<Context.Provider value={{ state, dispatch }}>{children}</Context.Provider>
);
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// SimpleReduxDemo
import React, { useContext } from "react";
import Context from "./simpleRedux";

export default function SimpleReduxDemo () {
const { state, dispatch } = useContext(Context);

return (
<div>
<div>{JSON.stringify(state)}</div>
<button
onClick={() => {
dispatch({ type: "setState", payload: { time: Date.now() } });
}}
>
set Time
</button>
<button
onClick={() => {
dispatch({ type: "setState", payload: { random: Math.random() } });
}}
>
set Random
</button>
</div>
);
};
1
2
3
4
5
6
7
8
9
10
11
// App.js
import { Provider } from "./simpleRedux";
import SimpleReduxDemo from 'simpleReduxDemo'

export default function App() {
return (
<Provider>
<SimpleReduxDemo />
</Provider>
)
}

useDebounceFn

用来处理防抖函数的 Hook

6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import debounce from 'lodash/debounce'
import { useEffect, useMemo, useRef } from 'react'

function useDebounceFn(fn, options) {
const fnRef = useRef(fn);

const wait = options?.wait ?? 1000;

const debounced = useMemo(
() =>
debounce(
() => fnRef.current(...args),
wait,
options,
),
[],
);

useEffect(() => () => {
debounced.cancel()
}, [])

return {
run: debounced,
cancel: debounced.cancel,
flush: debounced.flush,
};
}

export default useDebounceFn;

useThrottleFn

用来处理函数节流的 Hook

6
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import throttle from 'lodash/throttle';
import { useEffect, useMemo, useRef } from 'react';

function useThrottleFn(fn, options) {
const fnRef = useRef(fn);

const wait = options?.wait ?? 1000;

const throttled = useMemo(
() =>
throttle(
() => fnRef.current(...args),
wait,
options,
),
[],
);

useEffect(() => () => {
throttled.cancel()
}, [])

return {
run: throttled,
cancel: throttled.cancel,
flush: throttled.flush,
};
}

export default useThrottleFn;