一. React
性能优化SCU
1.React
更新机制
我们在前面已经学习
React
的渲染流程:jsxjsx -> 虚拟dom -> 真实dom
那么
React
的更新流程呢?
2.React
的更新流程
React
在props
或state
发生改变时,会调用React
的render
方法,会创建一颗不同的树React
需要基于这两颗不同的树之间的差别来判断如何有效的更新UI
:- 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为
O(n²)
,其中n
是树中元素的数量 https://grfia.dlsi.ua.es/ml/algorithms/references/editsurvey_bille.pdf
- 如果在
React
中使用了该算法,那么展示1000
个元素所需要执行的计算量将在十亿的量级范围 - 这个开销太过昂贵了,
React
的更新性能会变得非常低效
- 如果一棵树参考另外一棵树进行完全比较更新,那么即使是最先进的算法,该算法的复杂程度为
- 于是,
React
对这个算法进行了优化,将其优化成了O(n)
,如何优化的呢?- 同层节点之间相互比较,不会跨节点比较
- 不同类型的节点,产生不同的树结构
- 开发中,可以通过
key
来指定哪些节点在不同的渲染下保持稳定
3.keys
的优化
我们在前面遍历列表时,总是会提示一个警告,让我们加入一个
key
属性:方式一:在最后位置插入数据
- 这种情况,有无
key
意义并不大
- 这种情况,有无
方式二:在前面插入数据
- 这种做法,在没有
key
的情况下,所有的li
都需要进行修改
- 这种做法,在没有
当子元素(这里的
li
)拥有key
时,React
使用key
来匹配原有树上的子元素以及最新树上的子元素:- 新旧节点都存在,但顺序不同的元素仅仅进行位移,不需要进行任何的修改
key
的注意事项:key
应该是唯一的key
不要使用随机数(随机数在下一次render
时,会重新生成一个数字)- 使用
index
作为key
,对性能是没有优化的
4.render
函数被调用
- 我们使用之前的一个嵌套案例:
- 在
App
中,我们增加了一个计数器的代码 - 当点击
+1
时,会重新调用App
的render
函数 - 而当
App
的render
函数被调用时,所有的子组件的render
函数都会被重新调用
- 在
- 那么,我们可以思考一下,在以后的开发中,我们只要是修改了
App
中的数据,所有的组件都需要重新render
,即使进行diff
算法,性能必然是很低的:- 事实上,很多的组件没有必须要重新
render
- 它们调用
render
应该有一个前提,就是依赖的数据(state、props
)发生改变时,再调用自己的render
方法
- 事实上,很多的组件没有必须要重新
- 如何来控制
render
方法是否被调用呢?- 通过
shouldComponentUpdate
方法即可
- 通过
- 没有实现
shouldComponentUpdate
方法,只要是调用setState
,那么render
函数一定会被重新执行 - 自己实现
shouldComponentUpdate
优化操作- 判断
props/state
是否有发生改变
- 判断
5.shouldComponentUpdate
React
给我们提供了一个生命周期方法shouldComponentUpdate
(很多时候,我们简称为SCU
),这个方法接受参数,并且需要有返回值:- 该方法有两个参数:
- 参数一:
nextProps
修改之后,最新的props
属性 - 参数二:
nextState
修改之后,最新的state
属性
- 参数一:
- 该方法返回值是一个
boolean
类型:- 返回值为
true
,那么就需要调用render
方法 - 返回值为
false
,那么久不需要调用rende
r方法 - 默认返回的是
true
,也就是只要state
发生改变,就会调用render
方法
- 返回值为
- 比如我们在
App
中增加一个message
属性:jsx
中并没有依赖这个message
,那么它的改变不应该引起重新渲染- 但是因为
render
监听到state
的改变,就会重新render
,所以最后render
方法还是被重新调用了
6.PureComponent
如果所有的类,我们都需要手动来实现
shouldComponentUpdate
,那么会给我们开发者增加非常多的工作量- 我们来设想一下
shouldComponentUpdate
中的各种判断的目的是什么? props
或者state
中的数据是否发生了改变,来决定shouldComponentUpdate
返回true
或者false
- 我们来设想一下
事实上
React
已经考虑到了这一点,所以React
已经默认帮我们实现好了,如何实现呢?将
class
继承自PureComponent
jsximport { PureComponent } from 'react' class NavBar extends PureComponent { // ... }
7.shallowEqual
方法
PureComponent
组件内部比较新旧state
新旧props
的源码:

- 上面这个方法中,调用
!shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState)
- 这个**
shallowEqual
就是进行浅层比较**:

8.高阶组件memo
目前我们是针对类组件可以使用
PureComponent
,那么函数式组件呢?- 默认情况下,父组件在重新调用
render
方法时,函数式的子组件即使其props
没有改变,也是会重新跟着渲染的 - 事实上函数式组件我们在
props
没有改变时,也是不希望其重新渲染其DOM
树结构的
- 默认情况下,父组件在重新调用
我们需要使用一个高阶组件
memo
:- 通过
memo
包裹所返回的组件,在父组件重新render
时,只要内部props
没有改变,就不会重新渲染
jsximport { memo } from "react" const Profile = memo(function(props) { console.log("profile render") return <h2>Profile: {props.message}</h2> }) export default Profile
- 通过
9. 不可变数据的力量
直接修改组件内部原有的
state
, 重新设置一遍,在PureComponent
是不会重新渲染(re-render
)所以往往通过浅拷贝
state
的方式来修改jsximport React, { PureComponent } from 'react' export class App extends PureComponent { constructor() { super() this.state = { books: [ { name: "你不知道JS", price: 99, count: 1 }, { name: "JS高级程序设计", price: 88, count: 1 }, { name: "React高级设计", price: 78, count: 2 }, { name: "Vue高级设计", price: 95, count: 3 }, ], friend: { name: "kobe" }, message: "Hello World" } } // shouldComponentUpdate(nextProps, nextState) { // shallowEqual(nextProps, this.props) // shallowEqual(nextState, this.state) // } addNewBook() { const newBook = { name: "Angular高级设计", price: 88, count: 1 } // 1.直接修改原有的state, 重新设置一遍 // 在PureComponent是不能引入重新渲染(re-render) this.state.books.push(newBook) this.setState({ books: this.state.books }) // 2.赋值一份books, 在新的books中修改, 设置新的books const books = [...this.state.books] books.push(newBook) this.setState({ books: books }) } addBookCount(index) { // this.state.books[index].count++ const books = [...this.state.books] books[index].count++ this.setState({ books: books }) } render() { const { books } = this.state return ( <div> <h2>数据列表</h2> <ul> { books.map((item, index) => { return ( <li key={index}> <span>name:{item.name}-price:{item.price}-count:{item.count}</span> <button onClick={e => this.addBookCount(index)}>+1</button> </li> ) }) } </ul> <button onClick={e => this.addNewBook()}>添加新书籍</button> </div> ) } } export default App
二. 获取DOM
方式refs
1.如何使用ref
在
React
的开发模式中,通常情况下不需要、也不建议直接操作DOM
原生,但是某些特殊的情况,确实需要获取到DOM
进行某些操作:- 管理焦点,文本选择或媒体播放
- 触发强制动画
- 集成第三方
DOM
库 - 我们可以通过
refs
获取DOM
如何创建
refs
来获取对应的DOM
呢?目前有三种方式:方式一:传入字符串
- 使用时,通过
this.refs.传入的字符串格式
获取对应的元素
- 使用时,通过
方式二:传入一个对象
- 对象是通过
React.createRef()
方式创建出来的 - 使用时,获取到创建的对象其中有一个
current
属性就是对应的元素
- 对象是通过
方式三:传入一个函数
- 该函数会在
DOM
被挂载时进行回调,这个函数会传入一个元素对象,我们可以自己保存 - 使用时,直接拿到之前保存的元素对象即可
jsximport React, { PureComponent, createRef } from 'react' class App extends PureComponent { constructor() { super() this.titleRef = createRef() this.titleEl = null } getNativeDOM() { // 1.方式一: 在React元素上绑定一个ref字符串 console.log(this.refs.later) // 2.方式二: 提前创建好ref对象, createRef(), 将创建出来的对象绑定到元素 console.log(this.titleRef.current) // <h2>你好啊,李银河</h2> // 3.方式三: 传入一个回调函数, 在对应的元素被渲染之后, 回调函数被执行, 并且将元素作为参数传入回调函数 console.log(this.titleEl) } render() { return ( <div> <h2 ref="later">Hello World</h2> <h2 ref={this.titleRef}>你好啊,李银河</h2> <h2 ref={el => this.titleEl = el}>你好啊, 师姐</h2> <button onClick={e => this.getNativeDOM()}>获取DOM</button> </div> ) } }
- 该函数会在
2.ref
的类型
ref
的值根据节点的类型而有所不同:- 当
ref
属性用于HTML
元素时,构造函数中使用React.createRef()
创建的ref
接收底层DOM
元素作为其current
属性 - 当
ref
属性用于自定义class
组件时,ref
对象接收组件的挂载实例作为其current
属性 - 你不能在函数组件上使用
ref
属性,因为函数组件没有实例
- 当
这里我们演示一下
ref
引用一个class
组件对象:jsximport React, { PureComponent, createRef } from 'react' class NavBar extends PureComponent { fn1() { console.log('-------') } } class App extends PureComponent { constructor() { super() this.navBarRef = createRef() } getCpn() { console.log(this.navBarRef.current) // <NavBar>...</NavBar> this.navBarRef.fn1() // "-------" } render() { return ( <div> <NavBar ref={this.navBarRef}></NavBar> <button onClick={e => this.getCpn()}>获取组件实例</button> </div> ) } }
函数式组件是没有实例的,所以无法通过
ref
获取他们的实例:- 但是某些时候,我们可能想要获取函数式组件中的某个
DOM
元素 - 这个时候我们可以通过
React.forwardRef
,后面我们也会学习hooks
中如何使用ref
forwardRef
只能用于函数式组件,用于类组件会报错
jsximport React, { PureComponent, createRef, forwardRef } from 'react' const HelloWorld = forwardRef(function(props, ref) { // 组件实例的 ref 传递到了 forwardRef 中的回调函数中的第二个参数 return ( <div> <h1 ref={ref}>Hello World</h1> // 等价于 -> <h1 ref={this.h1Ref}></h1> </div> ) }) class App extends PureComponent { constructor() { super() this.h1Ref = createRef() } getCpn() { console.log(this.h1Ref.current) // <h1>Hello World</h1> } render() { return ( <div> <HelloWorld ref={this.h1Ref}/> <button onClick={e => this.getCpn()}>获取组件实例</button> </div> ) } }
- 但是某些时候,我们可能想要获取函数式组件中的某个
三. 受控和非受控组件
1.认识受控组件
在
React
中,HTML
表单的处理方式和普通的DOM
元素不太一样:表单元素通常会保存在一些内部的state
比如下面的
HTML
表单元素:这个处理方式是
DOM
默认处理HTML
表单的行为,在用户点击提交时会提交到某个服务器中,并且刷新页面在
React
中,并没有禁止这个行为,它依然是有效的jsx<form action='/abc'> <label> 名字:<input type='text' name='username' /> </label> <input type='submit' value='提交' /> </form>
但是通常情况下会使用
js
函数来方便的处理表单提交,同时还可以访问用户填写的表单数据实现这种效果的标准方式是使用“受控组件”
jsxclass App extends PureComponent { constructor() { super() this.state = { username: "" } handleSubmitClick(event) { // 1.阻止默认的行为 event.preventDefault() // 2.获取到所有的表单数据, 对数据进行组件 console.log("获取所有的输入内容") console.log(this.state.username) // 3.以网络请求的方式, 将数据传递给服务器(ajax/fetch/axios) } handleUsernameChange(event) { this.setState({ username: event.target.value }) } render() { const { username } = this.state return ( <div> <form onSubmit={e => this.handleSubmitClick(e)}> <label htmlFor="username">用户: <input id='username' type="text" name='username' value={username} onChange={e => this.handleUsernameChange(e)} /> </label> <button type='submit'>注册</button> </form> </div> ) } }
2.受控组件基本演练
- 在
HTML
中,表单元素(如<input>、 <textarea>
和<select>
)之类的表单元素通常自己维护state
,并根据用户输入进行更新 - 而在
React
中,可变状态(mutable state
)通常保存在组件的state
属性中,并且只能通过使用setState()
来更新- 我们将两者结合起来,使
React
的state
成为“唯一数据源” - 渲染表单的
React
组件还控制着用户输入过程中表单发生的操作 - 被
React
以这种方式控制取值的表单输入元素就叫做“受控组件”
- 我们将两者结合起来,使
- 由于在表单元素上设置了
value
属性,因此显示的值将始终为this.state.value
,这使得React
的state
成为唯一数据源 - 由于
handleUsernameChange
在每次按键时都会执行并更新React
的state
,因此显示的值将随着用户输入而更新

3.受控组件的其他演练
textarea
标签texteare
标签和input
比较相似
select
标签select
标签的使用也非常简单,只是它不需要通过selected
属性来控制哪一个被选中,它可以匹配state
的value
来选中jsxhandleFruitChange(event) { const options = Array.from(event.target.selectedOptions) const values = options.map(item => item.value) this.setState({ fruit: values }) // Array.from(可迭代对象 / 类数组) // Array.from(arguments) const values2 = Array.from(event.target.selectedOptions, item => item.value) console.log(values2) } <select value={fruit} onChange={e => this.handleFruitChange(e)} multiple> <option value="apple">苹果</option> <option value="orange">橘子</option> <option value="banana">香蕉</option> </select>
处理多个输入
多处理方式可以像单处理方式那样进行操作,但是需要多个监听方法:
这里我们可以使用
ES6
的一个语法:计算属性名(Computed property names
)jsxthis.state = { username: "", password: "" } handleInputChange(event) { this.setState({ [event.target.name]: event.target.value }) } <label htmlFor="username"> 用户: <input id='username' type="text" name='username' value={username} onChange={e => this.handleInputChange(e)} /> </label> <label htmlFor="password"> 密码: <input id='password' type="password" name='password' value={password} onChange={e => this.handleInputChange(e)} /> </label>
4.非受控组件
React
推荐大多数情况下使用受控组件来处理表单数据:- 一个受控组件中,表单数据是由
React
组件来管理的 - 另一种替代方案是使用非受控组件,这时表单数据将交由
DOM
节点来处理
- 一个受控组件中,表单数据是由
如果要使用非受控组件中的数据,那么我们需要使用
ref
来从DOM
节点中获取表单数据- 我们来进行一个简单的演练:
- 使用
ref
来获取input
元素
jsximport React, { createRef, PureComponent } from 'react' class App extends PureComponent { constructor() { super() this.state = { intro: "哈哈哈" } this.introRef = createRef() } componentDidMount() { // this.introRef.current.addEventListener } render() { const { intro } = this.state return ( <div> {/* 非受控组件 */} <input type="text" defaultValue={intro} ref={this.introRef} /> </div> ) } }
在非受控组件中通常使用
defaultValue
来设置默认值同样,
<input type="checkbox">
和<input type="radio">
支持defaultChecked
,<select>
和<textarea>
支持defaultValue
四. React
的高阶组件
1.认识高阶函数
- 高阶函数的维基百科定义:至少满足以下条件之一:
- 接受一个或多个函数作为输入
- 输出一个函数
js
中比较常见的filter、map、reduce
都是高阶函数- 那么说明是高阶组件呢?
- 高阶组件的英文是
Higher-Order Components
,简称为**HOC
** - 官方的定义:高阶组件是参数为组件,返回值为新组件的函数
- 高阶组件的英文是
- 我们可以进行如下的解析:
- 首先, 高阶组件 本身不是一个组件,而是一个函数
- 其次,这个函数的参数是一个组件,返回值也是一个组件
2.高阶组件的定义
高阶组件其实就是一个函数,对某个传入的组件,进行某些操作或者判断,然后再返回出去一个组件
高阶组件的调用过程类似于这样:
jsxconst EnhancedComponent = higherOrderComponent(WrappedComponent)
高阶函数的编写过程类似于这样:
jsxfunction higherOrderComponent(WrapperComponent) { class NewComponent extends PureComponent { render() { return <WrapperComponent/> } } NewComponent.displayName = 'Coderwhy' return NewComponent }
组件的名称问题:
在
ES6
中,类表达式中类名是可以省略的jsxfunction foo() { return class Bar extends PureComponent {} // 可以省略类名 return class extends PureComponent {} }
组件的名称都可以通过
displayName
来修改获取
class
组件名或函数式组件的名字,直接component.name
即可- 都是原生
js
中自带的name
属性
jsxclass App ... App.name function Bar() {} Bar.name
- 都是原生
高阶组件并不是
React API
的一部分,它是基于React
的组合特性而形成的设计模式高阶组件在一些
React
第三方库中非常常见:- 比如
redux
中的connect
- 比如
react-router
中的withRouter
- 比如
3.应用一 - props的增强
不修改原有代码的情况下,添加新的
props
jsxfunction enhanceProps(WrapperCpn, otherProps) { return props => <WrapperCpn {...props} {...otherProps} /> }
- 具体案例

利用高阶组件来共享
Context
jsxfunction withUser(WrapperCpn) { return props => { return ( <UserContext.Consumer> { value => { return <WrapperCpn {...props} {...value} /> } } </UserContext.Consumer> ) } }
具体案例

4,应用二 – 渲染判断鉴权
在开发中,我们可能遇到这样的场景:
- 某些页面是必须用户登录成功才能进行进入
- 如果用户没有登录成功,那么直接跳转到登录页面
这个时候,我们就可以使用高阶组件来完成鉴权操作:
jsxfunction loginAuth(OriginCpn) { return props => { return props.isLogin ? <OriginCpn/> : <LoginPage/> } }
具体案例
component.forceUpdate(callback)
强制让组件重新渲染,致使组件重新调用render
方法,此操作会跳过该组件的shouldComponentUpdate()

5.应用三 – 生命周期劫持
- 我们也可以利用高阶函数来劫持生命周期,在生命周期中完成自己的逻辑:

6.高阶函数的意义
- 我们会发现利用高阶组件可以针对某些
React
代码进行更加优雅的处理 - 其实早期的
React
有提供组件之间的一种复用方式是mixin
,目前已经不再建议使用:Mixin
可能会相互依赖,相互耦合,不利于代码维护- 不同的
Mixin
中的方法可能会相互冲突 Mixin
非常多时,组件处理起来会比较麻烦,甚至还要为其做相关处理,这样会给代码造成滚雪球式的复杂性
- 当然,
HOC
也有自己的一些缺陷:HOC
需要在原组件上进行包裹或者嵌套,如果大量使用HOC
,将会产生非常多的嵌套,这让调试变得非常困难HOC
可以劫持props
,在不遵守约定的情况下也可能造成冲突
Hooks
的出现,是开创性的,它解决了很多React
之前的存在的问题- 比如
this
指向问题、比如hoc
的嵌套复杂度问题等等
- 比如
7.ref
的转发
在前面我们学习
ref
时讲过,ref
不能应用于函数式组件:- 因为函数式组件没有实例,所以不能获取到对应的组件对象
但是,在开发中我们可能想要获取函数式组件中某个元素的
DOM
,这个时候我们应该如何操作呢?方式一:直接传入
ref
属性(错误做法)方式二:通过
forwardRef
高阶函数
五. portals
和fragment
1.Portals
的使用
某些情况下,我们希望渲染的内容独立于父组件,甚至是独立于当前挂载到的
DOM
元素中(默认都是挂载到id
为root
的DOM
元素上的)Portals
提供了一种将子节点渲染到存在于父组件以外的DOM
节点的优秀的方案:- 第一个参数(
child
)是任何可渲染的React
子元素,例如一个元素,字符串或fragment
- 第二个参数(
container
)是一个DOM
元素
jsxReactDOM.createPortal(child, container)
- 第一个参数(
通常来讲,当你从组件的
render
方法返回一个元素时,该元素将被挂载到DOM
节点中离其最近的父节点然而,有时候将子元素插入到
DOM
节点中的不同位置也是有好处的:

2.Modal
组件案例
- 比如说,我们准备开发一个
Modal
组件,它可以将它的子组件渲染到屏幕的中间位置:- 步骤一:修改
index.html
添加新的节点 - 步骤二:编写这个节点的样式
- 步骤三:编写组件代码
- 步骤一:修改

- 具体代码

3.fragment
在之前的开发中,我们总是在一个组件中返回内容时包裹一个
div
元素:我们又希望可以不渲染这样一个
div
应该如何操作呢?- 使用
Fragment
Fragment
允许你将子列表分组,而无需向DOM
添加额外节点
- 使用
React
还提供了Fragment
的短语法:它看起来像空标签
<> </>
jsx<> <h2>123</h2> // ... </>
但是,如果我们需要在
Fragment
中添加key
,那么就不能使用短语法

六. StrictMode
严格模式
1.StrictMode
StrictMode
是一个用来突出显示应用程序中潜在问题的工具:- 与
Fragment
一样,StrictMode
不会渲染任何可见的UI
- 它为其后代元素触发额外的检查和警告
- 严格模式检查仅在开发模式下运行,不会影响生产构建
- 与
可以为应用程序的任何部分启用严格模式:
不会对
Header
和Footer
组件运行严格模式检查但是,
ComponentOne
和ComponentTwo
以及它们的所有后代元素都将进行检查
2.严格模式检查的是什么?
- 但是检测,到底检测什么呢?
- 1.识别不安全的生命周期:
- 2.使用过时的
ref API
- 3.检查意外的副作用
- 这个组件的
constructor
及生命周期函数会被调用两次 - 这是严格模式下故意进行的操作,让你来查看在这里写的一些逻辑代码被调用多次时,是否会产生一些副作用
- 在生产环境中,是不会被调用两次的
- 在安装了
react devtool
浏览器插件的情况下,且是react18
之后,第二次调用的打印信息颜色为半灰色
- 这个组件的
- 4.使用废弃的
findDOMNode
方法- 在之前的
React API
中,可以通过findDOMNode
来获取DOM
,不过已经不推荐使用了,可以自行学习演练一下
- 在之前的
- 5.检测过时的
context API
- 早期的
Context
是通过static
属性声明Context
对象属性,通过getChildContext
返回Context
对象等方式来使用Context
的 - 目前这种方式已经不推荐使用,可以自行学习了解一下它的用法
- 早期的