一. 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,那么久不需要调用render方法 - 默认返回的是
true,也就是只要state发生改变,就会调用render方法
- 返回值为
- 比如我们在
App中增加一个message属性:jsx中并没有依赖这个message,那么它的改变不应该引起重新渲染- 但是因为
render监听到state的改变,就会重新render,所以最后render方法还是被重新调用了
6.PureComponent
如果所有的类,我们都需要手动来实现
shouldComponentUpdate,那么会给我们开发者增加非常多的工作量- 我们来设想一下
shouldComponentUpdate中的各种判断的目的是什么? props或者state中的数据是否发生了改变,来决定shouldComponentUpdate返回true或者false
- 我们来设想一下
事实上
React已经考虑到了这一点,所以React已经默认帮我们实现好了,如何实现呢?将
class继承自PureComponentjsximport { 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中如何使用refforwardRef只能用于函数式组件,用于类组件会报错
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的增强
不修改原有代码的情况下,添加新的
propsjsxfunction enhanceProps(WrapperCpn, otherProps) { return props => <WrapperCpn {...props} {...otherProps} /> }- 具体案例

利用高阶组件来共享
Contextjsxfunction 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的 - 目前这种方式已经不推荐使用,可以自行学习了解一下它的用法
- 早期的
