React 是用来构建用户界面的,所以 React 性能优化的核心是减少渲染真实 DOM 节点的频率,减少 Virtual DOM diff 的频率。
1 组件卸载前进行清理操作
在组件中为 window 注册的全局事件,以及定时器,在组件卸载前要清理掉,放置组件卸载后继续执行影响应用性能。
// App.js
import { useState, useEffect } from 'react'
import ReactDOM from 'react-dom'
function App() {
let [count, setCount] = useState(0)
// 使用 useEffect 只开启一个定时器
useEffect(() => {
setInterval(() => {
console.log(count)
setCount(++count)
}, 1000)
}, [])
return (
<>
<span>{count}</span>
<button onClick={() => ReactDOM.unmountComponentAtNode(document.getElementById('root'))}>卸载组件</button>
</>
)
}
export default App
点击按钮卸载组件后发现定时器还在执行。
优化方法:
// 使用 useEffect 只开启一个定时器
useEffect(() => {
let timer = setInterval(() => {
console.log(count)
setCount(++count)
}, 1000)
// 返回一个组件卸载时执行的清理函数
return () => {
// 清理定时器
clearInterval(timer)
}
}, [])
对于类组件可以使用 componentWillUnmount
钩子函数。
2 使用纯组件降低组件重新渲染的频率
什么是纯组件
与纯函数同理,纯组件就是相同的输入(state
,props
)呈现相同的输出。
在输入内容相同的情况下,纯组件不会被重新渲染。
如何实现纯组件
React 提供了 PureComponent 类,类组件在继承它以后,类组件就变成了纯组件。
纯组件会对 props
和 state
进行浅层比较,如果上一次的 props
、state
和下一次的 props
、 state
相同,则不会重新渲染组件。
因为 props
和 state
前后的值相同,就算重新渲染(render),经过 diff 比较后,真实 DOM 也不会变化。
所以纯组件通过避免重新渲染,减少不必要的开销,
什么是浅层比较
浅比较指的是:
- 比较基本数据类型是否具有相同的值,如 1 是否等于 1,true 是否等于 true
- 比较复杂数据类型的第一层值是否相同,如对象中的每个属性的值是否相同
浅比较难道没有性能消耗么
和进行 diff 比较操作相比,浅层比较将消耗更少的性能。
diff 操作会重新遍历整颗 Virtual DOM 树,而浅层比较只操作当前组件的 state
和 props
。
示例
// App.js
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.state = {
name: '张三'
}
}
updateName() {
setInterval(() => {
this.setState({
name: '张三'
})
}, 1000)
}
componentDidMount() {
// 组件挂在完成 1 秒后,将组件状态更改为和之前一样的值
this.updateName()
}
render() {
const { name } = this.state
return (
<>
<RegularComponent name={name} />
<PureComponent name={name} />
</>
)
}
}
// 常规组件
class RegularComponent extends React.Component {
render() {
console.log('RegularComponent')
return <div>{this.props.name}</div>
}
}
// 纯组件
class PureComponent extends React.PureComponent {
render() {
console.log('PureComponent')
return <div>{this.props.name}</div>
}
}
纯组件只渲染了一次。
3 使用 React.memo 进行组件缓存
Reacr.memo 介绍
React.memo 的原理同 React.PureComponent 一样,都是在内部做一个浅比较,创建一个纯组件。
区别是:
- React.memo 是一个用于函数组件的方法,一个高阶组件。
- React.PureComonent 是用于类组件的。
- React.memo 只比较
props
,因为只有类组件才有state
。
什么是高阶组件
高阶组件(HOC:Higher Order Component)是 React 应用中共享代码,增加逻辑复用的一种方式。
高阶组件的核心思想就是在组件的外层再包裹一层执行逻辑的组件,在外层组件中执行逻辑,再将逻辑执行的结果传递到内层组件,从而实现逻辑复用。
高阶组件的形式是一个函数,接收组件作为参数,返回一个新的组件。
参数组件就是需要复用逻辑的组件,函数内部返回的新组件就是执行逻辑的组件,在新组件内部执行完逻辑以后再调用参数组件并将逻辑结果传递给参数组件。
高阶组件 - 类组件示例
函数名通常以 with
开头,接收的参数组件形参名通常为 wrappedComponent
,返回的组件名称和函数名称一i杨,不会 with
中的 w
要大写。
下面 withResizable
方法是用于创建高阶组件的方法,它创建的组件可以复用实时获取窗口尺寸的逻辑。
// App.js
import React from 'react'
function withResizable(WrappedComponent) {
class WithResizable extends React.Component {
constructor() {
super()
this.state = {
sizes: [window.innerWidth, window.innerHeight]
}
this.updateSizes = this.updateSizes.bind(this)
}
updateSizes() {
this.setState({
sizes: [window.innerWidth, window.innerHeight]
})
}
componentDidMount() {
// 组件加载完成后绑定 resize 事件
// 实时获取窗口尺寸
window.addEventListener('resize', this.updateSizes)
}
render() {
return <WrappedComponent sizes={this.state.sizes} />
}
}
return WithResizable
}
export class A extends React.Component {
render() {
return <div>{this.props.sizes.join('-')}</div>
}
}
export class B extends React.Component {
render() {
return <div>{this.props.sizes.join('-')}</div>
}
}
const WappedA = withResizable(A)
const WappedB = withResizable(B)
export default class App extends React.Component {
render() {
return (
<>
<WappedA />
<WappedB />
</>
)
}
}
React.memo 使用
// App.js
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.state = {
name: '张三'
}
}
updateName() {
setInterval(() => {
this.setState({
name: '张三'
})
}, 1000)
}
componentDidMount() {
// 组件挂在完成 1 秒后,将组件状态更改为和之前一样的值
this.updateName()
}
render() {
const { name } = this.state
return (
<>
<RegularComponent name={name} />
<MemoComponent name={name} />
</>
)
}
}
// 常规组件
function RegularComponent(props) {
console.log('RegularComponent')
return <div>{props.name}</div>
}
// 纯组件
function WrappedComponent(props) {
console.log('MemoComponent')
return <div>{props.name}</div>
}
const MemoComponent = React.memo(WrappedComponent)
memo 自定义比较逻辑
memo 方法创建的组件默认执行的是浅比较,所以当向组件传递的数据的更深层级发生变化,组件无法重新渲染。
// App.js
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.state = {
name: '张三'
}
}
updateName() {
setInterval(() => {
this.setState({
name: '张三'
})
}, 1000)
}
componentDidMount() {
// 组件挂在完成 1 秒后,将组件状态更改为和之前一样的值
this.updateName()
}
render() {
const { name } = this.state
return (
<>
<ShowPersonMemoDomponent person={{ name: name, age: 20 }} />
</>
)
}
}
function ShowPersonDomponent(props) {
console.log('ShowPersonDomponent Rendering')
const { name, age } = props.person
return (
<div>
{name} {age}
</div>
)
}
const ShowPersonMemoDomponent = React.memo(ShowPersonDomponent)
而 memo 方法提供第二个参数,可以接收一个比较函数,该函数接收新旧 props
(prevProps
和 nextProps
),内部定义手动比较逻辑,返回一个 Boolean 值,表示新旧 props
是否一致,如果返回 true
则不进行渲染,如果返回 false
则组件重新渲染。
function comparePerson(prevProps, nextProps) {
if (prevProps.person.name === nextProps.person.name && prevProps.person.age === nextProps.person.age) {
return true
} else {
return false
}
}
const ShowPersonMemoDomponent = React.memo(ShowPersonDomponent, comparePerson)
4 shouldComponentUpdate 减少组件渲染频率
shouldComponentUpdate 是类组件的生命周期函数,在组件 props
或 state
发生变化后触发。
函数默认返回 true
,表示重新渲染组件,返回 false
阻止组件重新渲染。
函数接收两个参数:nextProps
和 nextState
。
它类似 React.memo 方法的第二个参数,可以自定义比较逻辑决定组件是否需要重新渲染。
React.PureComponent 内部以浅比较
props
和state
的方式实现了 shouldComponentUpdate。如果只进行浅比较逻辑,使用 React.PureComponent 更便捷。
如果在 React.PureComponent 创建的组件中重新定义 shouldComponentUpdate,React 会发出错误提示。
// App.js
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.state = {
name: '张三',
age: 20,
job: '服务员'
}
}
componentDidMount() {
setTimeout(() => {
this.setState({
job: '出纳员'
})
}, 1000)
}
shouldComponentUpdate(nextProps, nextState) {
// 仅当姓名或年领发生变化时重新渲染组件
if (nextState.name === this.state.name && nextState.age === this.state.age) {
return false
} else {
return true
}
}
render() {
console.log('render')
const { name, age } = this.state
return (
<>
{name} {age}
</>
)
}
}
5 使用组件懒加载
使用组件懒加载可以减少 bundle 文件大小,加快组件呈递速度。
懒加载是打包层面上的性能优化,在任何涉及打包的框架都可行。
使用懒加载的模块,将会抽离单独作为一个文件去打包。
组件也是模块。
单个页面并不展示所有的组件,如果不是用懒加载,那么在打包项目的时候,所有的组件默认将会打包到一个文件中,这个文件将会很大,加载页面时也会增加时间。
使用组件懒加载,每个组件将单独抽离成一个文件,最终打包的 bundle就会变小,用户第一次加载页面时只需加载页面所需的 bundle 即可。
简单来说,懒加载就是对打包文件进行拆分,最终降低打包文件的大小,让用户加载页面时只需加载所需的内容,从而加快加载页面的速度。
路由懒加载
懒加载最常用的就是路由组件。
使用 React.lazy() 和 React.Suspense 实现组件懒加载:
- React.lazy() 函数用于处理动态引入的组件。
- 它接收一个函数,函数需要动态调用
import()
,函数必须返回一个 Promise,该 Promise 需要 resovle 一个 default export 的 React 组件。 - 此方法创建的组件会在组件首次渲染时,自动导入
import
的组件。
- 它接收一个函数,函数需要动态调用
- React.Suspense 组件用于包括 lazy 组件,展示 loading 内容。
- lazy 组件可以嵌套在 Suspense 中任意层级,Suspense 组件会在所有包裹的 lazy 组件加载完成后显示加载的内容。
- 组件的
fallback
属性接收任何懒加载组件加载过程中想展示的 React 元素。
// App.js
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'
import React from 'react'
import About from './About'
import Home from './Home'
export default function App() {
return (
<BrowserRouter>
<div>
<Link to="/home">Home</Link>|<Link to="/about">About</Link>
</div>
<Switch>
<Route path="/home">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
</Switch>
</BrowserRouter>
)
}
在初始加载页面后,切换路由没有加载新的打包文件。
使用懒加载:
// App.js
import { BrowserRouter, Switch, Route, Link } from 'react-router-dom'
import React from 'react'
const Home = React.lazy(() => import(/* webpackChunkName: 'Home' */'./Home.js'))
const About = React.lazy(() => import(/* webpackChunkName: 'About' */'./About.js'))
export default function App() {
return (
<BrowserRouter>
<div>
<Link to="/home">Home</Link>|<Link to="/about">About</Link>
</div>
<React.Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route path="/home">
<Home />
</Route>
<Route path="/about">
<About />
</Route>
</Switch>
</React.Suspense>
</BrowserRouter>
)
}
切换路由后加载组件对应的打包文件。
6 使用 Fragment 以避免额外的标记
React 中的 jsx 只能有一个根元素(多个同级元素必须包裹在一个元素下)。
function App() {
return (
<div>
<div>message a</div>
<div>message b</div>
</div>
)
}
为了满足这个条件,我们通常会在最外层添加一个 div,但是这样的话又会多创建一个 DOM 元素,如果每个组件都需要包裹一层 div,浏览器渲染引擎的负担就会加剧,React 进行 diff 比较的时候也会多比较一层。
为了解决这个问题,React 推出了 React.Fragment 组件,它能够在不额外创建 DOM 元素的情况下,返回多个元素。
import React from 'react'
function App() {
return (
<React.Fragment>
<div>message a</div>
<div>message b</div>
</React.Fragment>
)
}
也可以使用简写:
function App() {
return (
<>
<div>message a</div>
<div>message b</div>
</>
)
}
7 不要使用内联函数定义
内联函数:在元素行内定义函数,例如示例中的 onChange 事件处理函数:
// App.js
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.state = {
inputValue: ''
}
}
render() {
return (
<input
value={this.state.inputValue}
onChange={e =>
this.setState({
inputValue: e.target.value
})
}
/>
)
}
}
使用内联函数后,render 方法每次运行时都会创建该函数的新实例,导致 React 在进行 Virtual DOM 比对时,新旧函数比对不相等,导致 React 总是为元素绑定新的函数实例,而旧的函数实例又要交给垃圾回收器处理。
正确做法是在组件中单独定义函数,将函数绑定给事件:
// App.js
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.state = {
inputValue: ''
}
this.handleChange = this.handleChange.bind(this)
}
handleChange(e) {
this.setState({
inputValue: e.target.value
})
}
render() {
return <input
value={this.state.inputValue}
onChange={this.handleChange}
/>
}
}
8 在构造函数中进行函数 this 绑定
更正 this 指向
在类组件中如果使用 fn() {}
这种方式定义事件函数,事件函数 this
默认指向 undefined
。
这是因为 ES6 中规定,类和模块内部默认就是严格模式,严格模式下函数内的 this
默认指向 undefined
而不是全局(window),如果函数有调用者,则 this
指向调用者。
import React from 'react'
export default class App extends React.Component {
handleClick() {
console.log(this) // undefined
}
render() {
return <button onClick={this.handleClick}>按钮</button>
}
}
所以函数内部的 this
指向需要被更正,更正方式有两种:
- 在构造函数中更正(推荐)
- 在行内进行更正
- 使用箭头函数
三者看起来没有太大区别,但是对性能的影响是不同的。
构造函数更正 vs 行内更正
// 构造函数中更正
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.handleClick = this.handleClick.bind(this)
}
handleClick() {
console.log(this) // App 实例
}
render() {
return <button onClick={this.handleClick}>按钮</button>
}
}
// 行内更正
import React from 'react'
export default class App extends React.Component {
handleClick() {
console.log(this) // App 实例
}
render() {
return <button onClick={this.handleClick.bind(this)}>按钮</button>
}
}
行内更正在 render
方法内,组件每次渲染都会调用 bind
方法生成新的函数实例。
而构造函数只执行一次,所以在构造函数内更正 this
指向只会生成一次函数实例。
classs 定义方法的区别
ES6 中 class 内定义方法的几种方式:
class A {
// 1. 直接定义
fn1() {}
// 2. 等号定义
fn2 = function() {}
fn3 = () => {}
}
console.dir(A)
console.log(new A())
两者的区别是:
- 直接定义是定义在
class
的原型上,等效于使用prototype
定义。 - 等号定义是定义在类创建的实例对象上的,等效于在构造函数中定义。
// 等效的另一种写法
class A {
constructor() {
this.fn2 = function() {}
this.fn3 = () => {}
}
}
A.prototype.fn1 = function() {}
console.dir(A)
console.log(new A())
箭头函数 this 指向
箭头函数中 this
指向定义函数的上下文环境,箭头函数本身不绑定 this
。
所以在类组件中使用箭头函数定义方法,this
指向的就是组件实例。
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.handleClick = () => {
console.log(this) // App 实例
}
}
// 或者这样定义
// handleClick = () => {
// console.log(this)
// }
render() {
return <button onClick={this.handleClick}>按钮</button>
}
}
尽管箭头函数在 this
指向上占据优势,但也有不利的一面。
由于使用箭头函数定义的方法是被添加到类组件的实例对象属性上,而不是类的原型对象属性上。
如果组件被多次使用,那么每个组件实例对象上都会添加功能相同的函数,放弃了这个函数的可重用性,造成了资源浪费。
结论
更正函数内部 this
指向的最佳做法仍是在构造函数中使用 bind
方法进行绑定。
9 避免使用内联样式属性
当使用内联 style 为元素添加样式时,内联 style 会被编译为 JavaScript 代码。
通过 JavaScript 代码将样式规则映射到元素的身上,浏览器就会花费更多的事件执行脚本和渲染 UI,从而增加了组件的渲染时间。
// App.js
export default function App() {
return <div style={{ backgroundColor: 'pink' }}>App</div>
}
更好的办法是将 CSS 文件导入样式组件:
/* App.css */
.bg-pink {
background-color: pink;
}
// App.js
import './App.css'
export default function App() {
return <div className="bg-pink">App</div>
}
10 优化条件渲染
频繁的挂载和卸载组件是一项耗性能的操作,为了确保应用程序的性能,应该减少组件挂载和卸载的次数。
在 React 中我们经常会根据条件渲染不同的组件,条件渲染是一项必做的优化操作。
function App() {
if (true) {
return (
<>
<AdminHeader />
<Header />
<Content />
</>
)
} else {
return (
<>
<Header />
<Content />
</>
)
}
}
上面的示例组件中,当渲染条件发生变化时,React 内部进行的 Virtual DOM 对比:
- 第一个组件:AdminHeader -> Header
- 第二个组件:Header -> Content
- 第三个组件:Content -> undefined
对比后,React 就会卸载 AdminHeader、Header、Content,重新挂载 Header 和 Content。
这种挂载和卸载就是没有必要的。
更优的短路写法:
function App() {
return (
<>
{true && <AdminHeader />}
<Header />
<Content />
</>
)
}
当条件由 true
变为 false
,Virtual DOM 对比:
- 第一个组件:AdminHeader -> false
- 第二个组件:Header -> Header
- 第三个组件:Content -> Content
这样条件切换只会挂载和卸载 AdminHeader 组件。
知识点:使用短路写法或三元表达式返回 Boolean 值、null
或 undefined
,既不会忽略也不会渲染任何内容,会保留返回值用于 Virtual DOM 比对。
查看打印结果:
const jsx = (
<>
<h1>1</h1>
<h2>2</h2>
<h3>3</h3>
</>
)
const jsx2 = (
<>
{false && <h1>1</h1>}
{false ? <h2>2</h2> : null}
{false ? <h3>3</h3> : undefined}
</>
)
console.log(jsx.props.children, jsx2.props.children)
11 不要在 render 方法中更改应用状态
当应用程序状态发生更改时,React 会调用 render
方法,如果 render
方法中继续更改应用程序状态,就会发生 render
方法循环调用导致应用报错。
Error: Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.state = { name: '张三' }
}
render() {
this.setState({ name: '李四' })
return <div>{this.state.name}</div>
}
}
与其它生命周期函数不同,render
方法应该被作为纯函数。
这意味着,在 render
方法中不要做以下事情:
- 调用
setState
方法 - 使用其它手段查询更改原生 DOM 有还俗
- 其它更改应用程序的任何操作
render
方法的执行要根据状态的改变,这样可以保持组件的行为和渲染方式一致。
12 为组件创建错误边界
错误捕获
默认情况下,组件渲染错误会导致整个应用程序中断。
创建错误边界可确保在特定组件发生错误时应用程序不会中断。
错误边界是一个 class 组件,可以捕获发生在子组件树任何位置的 JavaScript 错误,并打印这些操作,同时显示降级 UI,而并不会渲染那些发生崩溃的子组件树。
错误边界可以在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
错误边界使用
错误边界涉及两个生命周期函数,当一个 class 组件中定义了任意一个或两个时,它就变成一个错误边界:
static getDerivedStateFromError(error)
- 静态方法,接收抛出的错误,返回的对象会和
state
进行合并,更改应用程序状态 - 会在渲染阶段调用,不允许出现副作用,内部
this
指向undefined
- 官方建议使用它渲染降级 UI
- 静态方法,接收抛出的错误,返回的对象会和
componentDidCatch(error, info)
- error:抛出的错误
- info:组件引发错误的栈信息
- 会在提交阶段被调用,允许执行副作用
- 官方建议使用它记录错误
注意:错误边界需要在生产环境展示效果,开发环境会被编译器的错误提示界面覆盖。
推荐用法:可以定义一个错误边界组件,然后用它包裹要捕获错误的组件或组件树。
import React from 'react'
function Foo () {
// throw new Error('I crashed!')
return <div>Foo work</div>
}
class MyErErrorBoundary extends React.Component {
constructor() {
super()
this.state = { hasError: false }
}
static getDerivedStateFromError(error) {
return {
hasError: true
}
}
componentDidCatch(error, errorInfo) {
// 记录错误
// logError(error)
}
render() {
if (this.state.hasError) {
// 降级 UI
return <h1>Something went wrong</h1>
}
return this.props.children
}
}
export default function App() {
return <MyErErrorBoundary>
<Foo />
</MyErErrorBoundary>
}
无法捕获的场景
错误边界无法捕获以下场景中产生的错误:
- 事件处理(如点击按钮时发生的错误)
- 异步代码(如
setTimeout
) - 服务端渲染
- 错误边界组件自身跑出来的错误(并非它的子组件)
13 为列表数据添加唯一标识
当需要渲染列表数据时,应该为每一个列表项添加 key
属性,key
属性的值必须是唯一的。
key
属性可以让 React 直接了当的知道哪些列表项发生了变化,从而避免了 React 内部逐一遍历 Virtual DOM 查找变化所带来的性能消耗,避免了元素因为位置变化而导致的重新创建。
当列表数据没有唯一标识时,可以临时使用索引作为 key
属性的值,但仅限于列表项是静态的,不会被动态改变。比如不会对列表项进行排序或者过滤,不会从顶部或中间添加或删除项目。当发生以上行为时,索引会被更改,并不可靠。
render() {
return <ul>
{ datalist.map((item, index) => <li key={index}>{item}</li>) }
</ul>
}
14 长列表优化(虚拟列表)
虚拟列表
工作中,我们经常需要一次性获取数据并以列表的形式展示,如果列表项过多,一次性渲染就会出现页面卡顿从而产生性能问题。
可以使用分页、上拉加载、虚拟列表技术解决,它们的优化原理都是每次只渲染几条数据。
- 分页:通过切换页码加载数据
- 上拉加载:通过滚动加载更多数据
- 虚拟列表:只加载用户可视区域内的数据,根据用户滚动位置逐步渲染其余数据
react-virtualized 可以实现虚拟列表,它会在页面中生成一块滚动区域,在区域内进行列表内容的展示。
它会根据可视区域大小生成数据,如可视区域内正好可以放置 10 条数据,它就会渲染 10 条数据,然后再根据用户的滚动位置,不断的渲染数据并替换区域内数据,再通过定位的方式设置列表项的位置,形成滚动的视觉效果。
示例
使用 faker 生成假数据
npm i faker
import faker from 'faker'
// 生成假数据
function createRecord(count) {
const records = []
for (let i = 0; i < count; i++) {
records.push({
username: faker.internet.userName(),
email: faker.internet.email()
})
}
return records
}
使用 react-virtualized 实现虚拟列表
npm i react-virtualized
import faker from 'faker'
import { List } from 'react-virtualized'
// 生成假数据
function createRecord(count) {
const records = []
for (let i = 0; i < count; i++) {
records.push({
username: faker.internet.userName(),
email: faker.internet.email()
})
}
return records
}
const records = createRecord(1000)
// 渲染单条数据
function rowRenderer({ index, key, style }) {
return <div key={key} style={style}>
<b>{records[index].username}</b>:{records[index].email}
</div>
}
// List 组件创建虚拟列表
export default function App() {
return <List width={400} height={600} rowHeight={44} rowCount={records.length} rowRenderer={rowRenderer} />
// width: 指定窗口宽度
// height: 指定窗口高度
// rowHeight: 指定每一行的高度
// rowCount: 指定数据条数,用于计算列表的总高度
// rowRenderer: 指定渲染函数,内部会根据 rowCount 进行遍历,遍历过程中会不断调用渲染函数并将相关信息传递给渲染函数,渲染函数内部返回每一行要渲染的 JSX 内容
}
15 节流和防抖
节流和防抖可以用于控制可以频繁触发事件的场景下,调用的事件处理函数的次数。
节流和防抖就是一个使用了闭包的函数,接收一个事件处理函数为参数,返回一个包含控制逻辑的新函数。
节流和防抖不只用于 React 项目,任何触发事件处理函数的操作都可以用这种方式优化性能。
有很多第三方库实现了节流与防抖,如 lodash,throttle-debounce
节流 throttle
指定时间间隔内只调用一次事件处理函数,额外的忽略。
目的:允许频繁触发,限制触发频率。
原理:设置一个状态开关,当事件触发时判断开关状态,如果是关闭状态则不执行处理函数,如果是开启则执行事件处理函数,并修改状态,指定时间后再改回来。
// 示例:页面滚动时执行一些操作
function throttle(fn, time) {
let canRun = true
return function() {
if (!canRun) return
fn.call(this, ...arguments)
canRun = false
setTimeout(() => {
canRun = true
}, time)
}
}
function getScrollTop(event) {
console.log(document.documentElement.scrollTop)
}
window.addEventListener('scroll', throttle(getScrollTop, 1000))
上例是立即执行,非立即执行如下:
function throttle(fn, time) {
let canRun = true
return function() {
if (!canRun) return
canRun = false
setTimeout(() => {
fn.call(this, ...arguments)
canRun = true
}, time)
}
}
防抖 debounce
频繁触发时,只保证最后一次触发时调用事件处理函数,之前的忽略。
目的:频繁触发一个事件,保证最后一次执行即可。
原理:设置一个定时器,内容就是执行事件处理函数,每次触发事件都重置定时器。
常用场景:支持模糊远程搜索的 Select 组件,用户频繁输入关键字时,每输入一次就会触发一次网络请求,导致性能问题,使用防抖限制在用户停止输入指定时间后才发送请求,避免额外的请求造成的资源浪费,甚至短时间内发送的多次请求因响应时间不同造成的处理顺序错误。
import React from 'react'
export default class App extends React.Component {
constructor() {
super()
this.handleSearch = this.handleSearch.bind(this)
}
handleSearch() {
console.log('发送网络请求')
}
render() {
return <input onInput={debounce(this.handleSearch, 1000)} />
}
}
function debounce(fn, time) {
let timer = true
return function () {
clearTimeout(timer)
timer = setTimeout(() => {
fn.call(this, ...arguments)
}, time)
}
}
16 外部资源使用 CDN 加速
-
什么是 CDN(Content Delivery Network)?
内容交付网络(CDN)指的是地理上分散的服务器组,它们一起工作以提供网络内容的快速交付。
-
使用 CDN 有什么好处?
- 浏览器对于同一个域下的并发请求有数量上的限制,主流浏览器的同一域下的并发请求数量是 6 或 10,意味着超过并发数量的其它资源需要等待,这就增加了应用呈递的时间。我们可以将不同的资源放在不同的 CDN 中,这样就可以突破浏览器的并发限制,加快应用的呈递速度。
- CDN 通常由大型公司托管,在全球都分布了数据中心,当你向 CDN 发送请求时,它会通过离你最近的数据中心进行响应,减少网络延迟,增加程序性能。
- 如果其它网站也是使用了相同 CDN 地址的资源,就会缓存下来,享受缓存带来的速度优势。