高级指引
代码分割
知识点:
- 打包:是一个将文件引入并合并到一个单独文件的过程,最终形成一个bundle。接着在页面引入该bundle,整个应用即可一次性加载
- 打包工具:webpack、rollup、browserify
- 若使用的是create react app、nextjs、gatsby,则内置了webpack。否则需要自己配置webpack构建打包
- 代码分割:出现场景是避免打包出过大体积的代码包,它是打包工具支持的一项技术,能够创建多个包并在运行时动态加载
- 代码分割的优点:
- 懒加载当前用户所需的内容,显著提高应用性能
- 避免加载用户永不需要的代码,并在初始加载的时候减少所需加载的代码量
- 代码分割的方式:
- 使用
import()
函数:无webpack需要配置,若使用了babel需要导入@babel/plugin-syntax-dynamic-import
插件 - 使用
React.lazy
函数:像渲染常规组件一样处理动态引入的组件,接受一个返回import()函数的回调函数作为参数;应该在Suspense
组件中渲染lazy组件,这样可以在等待加载lazy时进行降级(例如使用骨架屏、loading等);该函数仅支持默认导出(即仅会导出export default的内容),若想支持命名导出(即export)的内容,需要引入中间模块,然后中间模块默认导出引入的具名导出 - 在路由上进行代码分割
- 使用
Suspense
组件:可以嵌套在懒加载组件上面的任意层级上,同时可以包含多个懒加载组件- 引入异常捕获边界组件去处理模块加载失败,以显示良好的用户体验并管理恢复事宜
javascript
// 类似promise
import('./my-comp').then(mycomp => {
console.log(mycomp.getName())
})
1
2
3
4
2
3
4
javascript
import React, { Suspense } from 'react'
import Tabs from './tabs'
import MyErrorBoundary from './my-error-boundary'
const OtherComp = React.lazy(() => import('./other-comp'))
const OtherComp2 = React.lazy(() => import('./other-comp2'))
function MyComp () {
const [tab, setTab] = React.useState('photos')
function handleTabSelect(tab) {
// 1. 为了避免这种情况,而是仍然展示切换tab之前的组件,需要在切换函数里面使用startTransition函数
startTransition(() => {
setTab(tab)
})
}
return (
<div>
// 嵌套一个错误处理组件,用于处理模块加载失败的情况
<MyErrorBoundary>
<Tabs onTabSelect={handleTabSelect}/>
<Suspense fallback={<div>loading</div>}>
// 懒加载组件需要在Suspense内部
<OtherComp/>
// Suspense可以包裹多个懒加载组件
<OtherComp2/>
// 当切换tab时,会展示另一个组件,而当另一个组件未准备好(即加载完成时),会展示fallback的内容
// 1. 为了避免这种情况,而是仍然展示切换tab之前的组件,需要在切换函数里面使用startTransition函数
{ tab === 'photos' ? <OtherComp/> : </OtherComp2/> }
</Suspense>
</MyErrorBoundary>
</div>
)
}
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
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
javascript
import React, { Suspense, lazy } from 'react'
import { BrowserRouer as Router, Routes, Route } from 'react-router-dom'
const Home = lazy(() => import('./home'))
const About = lazy(() => import('./about'))
const App = () => (
<Router>
<Suspense fallback={<div>loading</div>}>
<Routes>
<Route path="/" element={<Home/>}/>
<Routes path="about" element={<About/>}/>
</Routes>
</Suspense>
</Router>
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
javascript
// 使用中间模块导出具名导出
// my.js
export const my = () => <div/>
// middle.js
export { my as default } from './my.js'
// 引入
import React, {lazy} from 'react'
const MyComp = lazy(() => import('./middle.js'))
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
Context
知识点:
- Context:提供了一个无需为每层组件手动添加props,就能在组件树间进行数据传递的方法
- 设计的目的是共享一些对于整个组件树而言全局的数据,比如用户信息、主题、语言等
javascript
// 动态的context
import React from "react"
const themes = {
light: {
foreground: '#000000',
background: '#eeeeee'
},
dark: {
foreground: '#ffffff',
background: '#222222'
}
}
// 创建一个Context对象,当React渲染了订阅了这个Context对象的组件,这个组件会从组件树中离自身最近的那个匹配的Provider读取当前的context的值
// 参数是默认值,仅在未匹配到Provider时才会生效,若value的值是undefined,默认值同样不生效
const ThemeContext = React.createContext(themes.dark)
// 打开react devtools时,展示的节点名称ThemeContext => MyThemeContext
ThemeContext.displayName = 'MyThemeContext'
class ThemedButton extends React.Component {
// 对于启用了public class fields,可以使用类属性
// 该属性,可以赋值为由React.createContext()创建的Context对象
// 定义了该属性之后,可以在任意生命周期和render中使用this.context获取最近的Context的值
static contextType = ThemeContext
render() {
let props = this.props
let theme = this.context
return (
// 将父组件上所有的props都作为他自己的props
<button {...props} style={{ backgroundColor: theme.background }} />
)
}
}
// 该属性,可以赋值为由React.createContext()创建的Context对象
// 定义了该属性之后,可以在任意生命周期和render中使用this.context获取最近的Context的值
// ThemedButton.contextType = ThemeContext
function Toolbar (props) {
return (
<ThemedButton onClick={props.changeTheme}>
切换主题
</ThemedButton>
)
}
export default class ChangeTheme extends React.Component {
constructor(props) {
super(props)
this.state = {
theme: themes.light
}
this.toggleTheme = () => {
// 函数式设置state
this.setState(state => ({
theme: state.theme === themes.light ? themes.dark : themes.light
}))
console.log(this.state.theme)
}
}
render () {
return (
<div>
{/* 给themecontext设置值,下级组件只要定义了contextType为ThemeContext,就能通过this.context获取该值 */}
{/* context的传值,放在state中更好,不然会存在一些异常 */}
{/*
Provider接收一个value属性,传递给下级组件
多个Provider可以嵌套使用,里面的会覆盖外面的
当传递对象给value时,可能会导致一些问题,除非是将对象设置到state中,然后将state的该对象传给value
*/}
<ThemeContext.Provider value={this.state.theme}>
{/* 将方法作为props传递给toolbar组件 */}
<Toolbar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
<section>
<div style={{ backgroundColor: this.state.theme.background, height: '100px', margin: '10px 0' }} />
</section>
</div>
)
}
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
javascript
// 动态的context
import React from 'react'
const themes = {
light: {
foreground: '#000000',
background: '#eeeeee',
},
dark: {
foreground: '#ffffff',
background: '#222222',
},
}
// 在context定义一个函数,用于更新context上的theme
const ThemeContext = React.createContext({
theme: themes.dark,
toggleTheme: () => {}
})
function ThemedButton() {
return (
// 通过ThemeContext.Consumer获取context
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => (
<button onClick={toggleTheme} style={{ backgroundColor: theme.background }}>
切换主题
</button>
)}
</ThemeContext.Consumer>
)
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
)
}
export default class ChangeTheme extends React.Component {
constructor(props) {
super(props)
this.toggleTheme = () => {
// 函数式设置state
this.setState((state) => ({
theme: state.theme === themes.light ? themes.dark : themes.light,
}))
console.log(this.state.theme)
}
this.state = {
theme: themes.light,
// 这里若想使用this.toggleTheme,必须先在它上面定义this.toggleTheme
toggleTheme: this.toggleTheme,
}
}
render() {
return (
<div>
<ThemeContext.Provider value={this.state}>
{/* 将方法作为props传递给toolbar组件 */}
<Toolbar />
</ThemeContext.Provider>
<section>
<div style={{ backgroundColor: this.state.theme.background, height: '100px', margin: '10px 0' }} />
</section>
</div>
)
}
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
javascript
// 多个动态的context
import React from "react"
const themes = {
light: {
foreground: '#000000',
background: '#eeeeee'
},
dark: {
foreground: '#ffffff',
background: '#222222'
}
}
const ThemeContext = React.createContext(themes.dark)
const UserContext = React.createContext({
name: 'Guest'
})
function ThemedButton() {
return (
// 通过ThemeContext.Consumer获取context
<ThemeContext.Consumer>
{({ theme, toggleTheme }) => (
<div style={{ color: theme.foreground, backgroundColor: theme.background }}>
<button onClick={toggleTheme}>
切换主题
</button>
<hr />
<UserContext.Consumer>
{({ user, toggleUser }) => (
<button onClick={toggleUser}>
{user === 'Guest' ? '登录' : '你好' + user + ',欢迎使用'}
</button>
)}
</UserContext.Consumer>
</div>
)}
</ThemeContext.Consumer>
)
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
)
}
export default class ChangeTheme extends React.Component {
constructor(props) {
super(props)
this.toggleTheme = () => {
// 函数式设置state
this.setState((state) => ({
theme: state.theme === themes.light ? themes.dark : themes.light,
}))
console.log(this.state.theme)
}
this.state = {
theme: themes.light,
user: 'Guest',
toggleUser: this.toggleUser,
// 这里若想使用this.toggleTheme,必须先在它上面定义this.toggleTheme
toggleTheme: this.toggleTheme,
}
}
toggleUser = () => {
this.setState({
user: this.state.user === 'Jade Qiu' ? 'Guest' : 'Jade Qiu',
})
}
render() {
return (
<div>
<ThemeContext.Provider value={this.state}>
{/* 将方法作为props传递给toolbar组件 */}
<UserContext.Provider value={this.state}>
<Toolbar />
</UserContext.Provider>
</ThemeContext.Provider>
<section>
<div style={{ backgroundColor: this.state.theme.background, height: '100px', margin: '10px 0' }} />
</section>
</div>
)
}
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
context vs props组合:
- context适用于组件树上多个层级都需要某些数据的情况
- props传递封装(slots)组件适用于组件树上单个少量的层级需要数据的情况(不涉及太多的层级)
html
// 若需要渲染以下ui结构
<Page user={user} avatarSize={avatarSize}>
<PageLayout user={user} avatarSize={avatarSize}>
<NavigationBar user={user} avatarSize={avatarSize}>
<Link href={user.permalink}>
<Avatar user={user} size={avatarSize} />
</Link>
</NavigationBar>
<Feed user={user} />
</PageLayout>
</Page>
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
javascript
// props组合,该种情形适合组件树中少量的地方需要某些数据
function Page (props) {
const user = props.user
const userLink = (
<Link href={user.permalink}>
<Avatar user={user} size={props.avatarSize} />
</Link>
)
const content = <Feed user={user} />
return <PageLayout userLink={userLink} content={content}/>
}
// 在page上使用
<Page user={user} avatarSize={avatarSize}>
// 在pageLayout上使用
<PageLayout userLink={ }>
// ......
// 渲染
{props.userLink}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
性能优化(未完成)
性能优化解决方案:
- 虚拟化长列表:当渲染一个大量数据的列表时,推荐使用虚拟滚动技术,它会在有限的时间内仅渲染有限的内容,并奇迹般降低重新渲染组件消耗的时间,以及创建DOM节点的数量。热门虚拟滚动库有
react-window
,react-virtualized
- 避免调停:即阻止不需要的组件更新。当组件的props和state变更的时候,react会将最新返回的元素与之前渲染的元素进行对象,以此更新变化了的节点,这会造成组件的重新渲染,若这个过程很慢,可以通过覆盖
shouldComponentUpdate
钩子进行提速,这个钩子会在重新渲染前触发,默认情况下返回true让react执行更新,若不需要更新,可以返回false跳过更新(即render调用和之后的操作过程)。大部分情况下可以继承React.PureComponent
代替手写该钩子,它用当前与之前的props和states的浅比较覆写了这个钩子(相当于省略了该钩子) - 避免直接更改props和state的值,需要使用正确的方法,比如对于state(使用setState,不能直接赋值)
shouldComponentUpdate
的作用:未完成
javascript
// 写法一:shouldComponentUpdate钩子
class CounterButton extends React.Component {
constructor (props) {
super(props)
this.state = {
count: 1
}
}
shouldComponentUpdate (nextProps, nextState) {
// 不相等,则更新
if (this.props.color !== nextProps.color) {
return true
}
if (this.state.count !== nextState.count) {
return true
}
return false
}
render () {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
)
}
}
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
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
javascript
// 写法二:React.PureComponent写法
class CounterButton extends React.PureComponent {
constructor (props) {
super(props)
this.state = {
count: 1
}
}
render () {
return (
<button
color={this.props.color}
onClick={() => this.setState(state => ({count: state.count + 1}))}>
Count: {this.state.count}
</button>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Portals(传送门)
定义:
- Portal提供了一种将子节点渲染到存在于父组件以外的DOM节点的方案
语法:`ReactDOM.createPortal(child, container)
- child: 表示任何可渲染的react元素,字符串,fragment片段
- container:表示传送到的dom元素
javascript
// 传送门功能
import React from 'react'
import ReactDOM from 'react-dom'
// 导入css
import './portal.css'
function ModalWrapper() {
return <div className="modal-wrapper"></div>
}
class Modal extends React.Component {
constructor(props) {
super(props)
this.modalWrapper = document.querySelector('.modal-wrapper')
this.el = document.createElement('div')
}
componentDidMount() {
this.modalWrapper.appendChild(this.el)
}
componentWillUnmount() {
this.modalWrapper.removeChild(this.el)
}
render() {
return ReactDOM.createPortal(this.props.children, this.el)
}
}
export default class HasPortal extends React.Component {
constructor(props) {
super(props)
this.state = {
show: false,
count: 0,
}
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
this.setState({
count: this.state.count + 1,
})
}
showPortal = (show) => {
this.setState((state) => ({
show: show,
}))
}
render() {
const modal = this.state.show ? (
<Modal>
<div className="modal-inner">
这个是在modal内部
<button onClick={() => this.showPortal(false)}>关闭portal</button>
</div>
</Modal>
) : null
return (
<>
<div className="normal-content">
这是正常的内容
<button onClick={() => this.showPortal(true)}>打开portal</button>
{/*
事件冒泡:portal定义在哪个组件上,该组件的祖先就能够捕获到portal的事件,而不是在其传送到的组件上
这里能够监听到portal点击事件,而不是在modal-wrapper组件上监听
实际上,portal不管嵌入到哪个dom上,那个dom都不能监听portal的事件,只有代码上实际存放的portal的父组件上才能够监听到
*/}
<div onClick={this.handleClick}>
<div>当前打开portal的次数:{this.state.count}</div>
{modal}
</div>
</div>
<ModalWrapper />
</>
)
}
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
diffing算法(待完成)
即:高级指引-协调相关内容
render props
定义:
- render props指一种在react组件之间使用一个值为函数的prop共享代码,类似抽象出一种公用的组件的技术
使用方法:传递一个叫render(这个prop不一定就叫render,也可以叫其他名字)的prop,该prop是一个返回一个react元素的函数,使用时,调用该prop渲染业务逻辑。可以结合HOC使用
注意事项:
- 当render props和react.PureComponent一起使用时,由于后者是props的浅比较,render又是一个函数,所以总会返回false,解决该问题的方式是将render函数定义成一个实例方法,则每次props引用的render都是指向同一个实例的
javascript
// 使用render-props构建一个随鼠标移动获取物体位置的组件
import React from 'react'
import PropTypes from 'prop-types'
class Cat extends React.Component {
render() {
const mouse = this.props.mouse
console.log(this.props.mouse, this.props)
return (
<img
src="https://img0.baidu.com/it/u=3269422069,1239134731&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1669741200&t=12e460e1449403b649088b7d5fc86e32"
style={{ position: 'absolute', left: mouse.x, top: mouse.y, height: '100px', width: '100px' }}
alt="cat"
/>
)
}
}
class Mouse extends React.Component {
constructor(props) {
super(props)
this.handleMouseMove = this.handleMouseMove.bind(this)
this.state = {
x: 0,
y: 0,
}
}
handleMouseMove(e) {
this.setState({
x: e.clientX,
y: e.clientY,
})
}
render() {
return (
<div style={{ height: '100vh' }} onMouseMove={this.handleMouseMove}>
{/* 根据传过来的render props动态渲染内容 */}
{this.props.render(this.state)}
</div>
)
}
}
// 由于render可以是任意名字,当将render命名为children时,必须声明children是一个函数类型:
Mouse.propTypes = {
render: PropTypes.func.isRequired
}
// export default
class MouseTracker extends React.Component {
render() {
return (
<div>
<h1>移动鼠标</h1>
<Mouse render={(mouse) => <Cat mouse={mouse} />} />
</div>
)
}
}
export { MouseTracker }
// 导出函数也可以换成一个HOC组件
function withMouse(Component) {
return class extends React.Component {
render () {
// 这里的props:即使用导入当前文件的组件传入的,在route文件内
return <Mouse render={(mouse) => <Component {...this.props} mouse={mouse} />} />
}
}
}
export default withMouse(Cat)
// 使用hoc
<WithMouse data-cus={'这是传给Component的{...this.props}'} />
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
类型检查
方式:
- typescript
- flow
- 内置的Prop-types组件(不推荐,用在极小型项目中)
错误边界
历史因素:
- react16以前,组件内的js错误会导致整个应用崩溃
定义:
- 错误边界是一种react组件,可以捕获并控制台打印发生在其子组件树任何位置的js错误,同时用错误边界定义的ui替换崩溃的子组件
- 若一个class组件定义了
static getDerivedStateFromError()
或componentDidCatch()
的其中一个,他就变成了一个错误边界,抛出错误后,使用前者控制渲染备用的ui的逻辑,使用后者打印错误信息
注意:
- 错误边界可以捕获发生在整个子组件树的渲染期间、生命周期方法、构造函数中的错误
- 错误边界无法捕获事件处理函数、异步代码、服务端渲染、错误边界组件自身(非子组件)发生的错误
- 错误边界自身发生的错误,会冒泡到最近的上层错误边界,类似catch()的工作机制
- 错误边界的细粒度由使用者自身决定
- 自react16开始,未被错误边界捕获的错误将导致整个react组件被卸载
- 捕获事件处理器的错误,使用try-catch等js普通的错误处理方式
javascript
class MyErrorBoundary extends React.Component {
constructor (props) {
super(props)
this.state = {
hasError: false
}
}
static getDerivedStateFromError (err) {
// 当出错时,渲染备用的ui的逻辑
return {
hasError: true
}
}
componentDidCatch(err, errInfo) {
console.log(err, errInfo)
}
render () {
// 如果出错了,渲染备用ui
if (this.state.hasError) {
return <div>出错了</div>
}
return this.props.children
}
}
// 使用组件
<ErrorBoundary>
<xxx></xxx>
</ErrorBoundary>
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
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