React 实践优化
最近在React项目复盘中,发现很多很简单的内容,却往往处理的不是太好,所以,这篇文章主要针对项目中的一些点,记录下优化或使用的方法。
一、引用优化
反例:引用依赖没有顺序,看起来比较混乱
import React, { useState, useEffect } from "react";
import { Tabs, Card } from "antd";
import Base from "../../components/FormDemo/base";
import moment from "moment";
import UseFormDemo from "../../components/FormDemo/useForm";
import { getList } from "../api"
import lodash from 'lodash'
优化:依赖引入,第三方库放在前面,本地文件的引用,放在下面
import React, { useState, useEffect } from "react";
import lodash from 'lodash'
import moment from "moment";
import { Tabs, Card } from "antd";
import Base from "../../components/FormDemo/base";
import UseFormDemo from "../../components/FormDemo/useForm";
import { getList } from "../api"
二、关于对象的属性是否存在
1、Javascript获取对象的属性值,可能属性值不存在,善用||
&&
const object = {
a: 1,
b: 2
}
// es6+, 判断对象属性值是否存在
const c = object.c
// 优化写法
const c = object?.c?.d
2、非必要的三元选择符直接使用 ||
const size = sise ? size : 1
// 优化写法
const size = size || 1
3、判断数组长度,利用1==true,减少代码
const arr = []
if(Array.isArray(arr)&&arr.length > 0){
return true
}else{
return false
}
// 优化写法
Array.isArray(arr)&&arr.length ? true : false
4、if else => 替换成三元选择符
if(Array.isArray(arr)){
getList(arr)
}else{
getDetail()
}
// 优化写法
Array.isArray(arr) ? getList(arr) : getDetail()
三、FC中的常量和方法写在组件外
函数式组件每次变量更新都会导致函数的重新执行和渲染,如果将变量和普通函数定义在组件内,会触发无效的更新,浪费性能
- 对应常量的定义和普通方法应该放在函数组件外部
- 组件内的函数,可以使用useCallback,依赖变量更新
// 只有在id变化的情况下,才会触发函数的更新
const getList = useCallback((id) => {
Api.getList({id}).then((Data) => {
setData(Data)
})
}, [id])
- 使用useCallback处理ESlint检测告警处理
当依赖的函数中有可变的参数,那么在useEffect使用,就可能导致函数更新但是调用没有更新,从而导致变量和视图不对应
const [Data, setData] = useState([])
const [params, setParams] = useState("")
const getList = useCallback((params) => {
let data = {
query: params
}
Api.GetList(data).then(res => {
setData(Data)
})
}, [])
useEffect(() => {
getList()
}, [getList])
这样就成功解决了告警,也解决了参数更新,函数不更新的问题。
四、对组件使用prop-types参数校验
如果使用js开发,那么组件的参数会比较混乱,最好使用prop-types进行限制,这样开开发阶段就避免很多错误
子组件:使用
import PropTypes from "prop-types"
function TableWrap(){
return <div>一个表格组件</div>
}
TableWrap.defaultProps = {
usePagination: false
};
TableWrap.propTypes = {
columns: PropTypes.array.isRequired, // 标题数据
useReloadButton: PropTypes.bool, // 刷新表格按钮
leftAction: PropTypes.any, // 表格头部左侧按钮操作
}
export default TableWrap
父组件
import TableWrap from './TableWrap'
function FatherComponent(){
return <TableWrap useReloadButton={false} />
}
export default FatherComponent
父组件没有赋值cloumns参数,控制台报错
五、子组件和父组件交互
1、父组件给子组件传值
// 父组件
function Father(){
const data = "我是来自父组件的数据"
return <ChildComponent data={data} />
}
export default Father
// 子组件
function ChildComponent({data}){
return <div>{data}</div>
}
export default ChildComponent
2、子组件给父组件传值
一般子组件给父组件传值很简单,直接使用组件方法就可以了
// 父组件
function Father(){
const [message, setMessage] = useState("")
return <ChildComponent handleClick={setMessage} />
}
export default Father
// 子组件
function ChildComponent({handleClick}){
const [data, setData] = useState("")
return <div onClick={()=>handleClick(data)}>我是子组件</div>
}
export default ChildComponent
3、父组件调用子组件方法/或获取子组件数据
使用useImperativeHandle,来暴露子组件方法,或子组件的数据
// 父组件通过childRef.current调用子组件数据或方法
import React, { useRef } from 'react'
function Father(){
const childRef = useRef()
useEffect(() => {
childRef.current.fetchData()
}, [])
const getData = () => {
const result = childRef.current && childRef.current.data
console.log("子组件的数据是:", result)
}
return <div>
<Button onClick={getData}>获取子组件数据</Button>
<ChildComponent ref={childRef} />
</div>
}
export default Father
// 子组件暴露方法或数据给父组件,通过useImperativeHandle第二个参数,刷新暴露的数据或方法
import React, { useState, forwardRef, useImperativeHandle } from 'react'
function ChildComponent(props, ref){
const [Data, setData] = useState([{a: 1}])
useImperativeHandle(ref, () => ({
fetchData: () => fetchData(),
data: Data
}), [Data]);
const fetchData = () => {
console.log("获取数据")
}
return <div>我是子组件</div>
}
export default forwardRef(ChildComponent)
4、多个组件间的消息共享
- 对于多个组件简单数据传递或共享,我们可以采用Context,维护一个底层的数据绑定。
- 但这种做法也有弊端,组件依赖于Context,或者在子组件更新值,导致逻辑混乱,代码无法解耦,不利于复用。
- 如果对于需要全局管理更新的数据可以使用状态管理
Redux
或者Mobx
createContext
、useContext
、Provider
、consumer
的使用
Context的使用也很简单,具体实例:
Context.js: 用来创建一个Context
import React, { createContext } from 'react'
export default MessageContent = createContext(null)
Index.js: 用来包裹父组件,让数据可以共享
import React from 'react'
import MessageContent from './Context'
import Child from './Child'
const initState = {
username: "zhangsan",
age: 10
}
function Father(){
return <MessageContent.Provider value={initState}>
<Child1 />
<Child2 />
</MessageContent.Provider>
}
Child1.js:子组件利用useContext
获取Context共享的数据
import React, { useContext } from 'react'
import MessageContext from './Context'
function Child1(){
const { username, age } = useContext(MessageContext)
return <div>
<p>this is child1 component</p>,
<div>username: {username}</div>
<div>username: {age}</div>
</div>
}
Child2.js:子组件利用Consumer
读取共享数据
import React from 'react'
import MessageContext from './Context'
function Child2(){
return <MessageContext.Consumer>
{
({username, age}) => {
return <div>
<p>this is child2 component</p>
<div>username: {username}</div>
<div>age: {age}</div>
</div>
}
}
</MessageContext.Consumer>
}
从加载的组件,就可以看到这两种方式都可以获取共享的数据。
六、防抖和节流
1、防抖
防抖:当连续输入内容时,不希望每次都执行查询调用,在用户停止输入一段时间后执行一次
利用setTimeout,延迟执行的特性,在连续输入时候,
export function debounce(cb, wait = 2000) {
let timer = null
return (...args) => {
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
timer = null
cb(args)
}, wait)
}
}
使用防抖
import { debounce } from './debounce'
const getList = (data) => {
console.log("远程查询产品类型列表")
}
const debounceList = debounce(getList, 1000) //连续事件停止1秒后执行
// Input onChange事件
const onChange = (val) => {
debouncList({query: val})
}
在React中防抖函数处理
我们知道函数式组件,在每次刷新后都会重新初始化,那么就会导致如上所示的调用失效。所以我们可以利用useRef将debounce的函数绑定,这样就可以防止重复更新。
import React, { useState, useRef } from 'react'
import { Input, Button, Card } from "antd"
import { debounce } from "../../utils/common"
function Debounce(){
const [value, setValue] = useState(undefined)
const getQueryList = (params) => {
console.log("获取查询列表", params)
}
const debounceRef = useRef(debounce(getQueryList))
const onChange = (e) => {
console.log("连续输入")
setValue(e.target.value)
debounceRef.current({a: e.target.value})
}
return <Card>
<div>
<p>防抖:多次触发同一事件,经过一段时间后最少执行一次</p>
</div>
<div style={{marginBottom: 20}}>
输入内容:<Input value={value} onChange={onChange}/>
</div>
</Card>
}
export default Debounce
效果:
可以看到防抖,连续触发,只要事件不停,最后只会执行一次
应用场景(一般只需要最终触发一次的事件):Input输入、页面resize
2、节流
大量触发同样的事件,一段时间内,只执行一次,类似游戏中的技能冷却。
export function throttle(func, delay=1000){
// 初始化没有调用setTimeout
let isWorking = false;
return (params) => {
if(isWorking){
// 如果正在执行setTimeout,则return
return false
}
// 开始执行setTimeout
isWorking = true;
setTimeout(() => {
// 调用函数
func(params)
// 调用完毕后,告诉函数执行完了,可以再次延迟定时
isWorking = false
}, delay)
}
}
执行效果
import React, { useState, useRef } from 'react'
import { Input, Button, Card } from "antd"
import { throttle } from "../../utils/common"
function Throttle(){
const [value, setValue] = useState(undefined)
const getQueryList = (params) => {
console.log("获取查询列表", params)
}
const throttleRef = useRef(throttle(getQueryList, 1000))
const onChange = (e) => {
console.log("连续输入")
setValue(e.target.value)
throttleRef.current({a: e.target.value})
}
return <Card>
<div>
<p>节流:多次触发同一事件,经过一段时间最多只会执行最后一次</p>
</div>
<div style={{marginBottom: 20}}>
输入内容:<Input value={value} onChange={onChange}/>
</div>
</Card>
}
export default Throttle
节流的展示效果:
可以看到连续触发事件,但会按固定频率触发,最后也会执行一次。
应用场景(强制函数以固定的频率触发):input、keyup、resize、touchmove、 mousemove、scroll,这些不需要频繁触发但是却需要以特定频率触发的场景(这样说来,防抖的场景,其实都可以使用节流来实现)
七、Hooks的使用
1、memo、useMemo和useCallback的区别
memo用来优化React组件的渲染次数
一个简单的父组件
function Context(){
const [childData, setChildData] = useState(1)
const [child2Data, setChild2Data] = useState(2)
return <Card>
<div style={{marginBottom: 100}}>
<Child data={childData}/>
</div>
<div>
<Child2 data={child2Data}/>
</div>
<Button onClick={() => setChildData(childData+1)}>更新组件一的数据</Button>
<Button onClick={() => setChild2Data(child2Data+1)}>更新组件二的数据</Button>
</Card>
}
export default Context
包含两个子组件
function Child1({data}){
console.log("组件一的数据", data)
return <div>
<p>this is child1 component</p>
</div>
}
function Child2({data}){
console.log("组件二的数据", data)
return <div>
<p>this is child2 component</p>
</div>
}
当更新组件一的数据时,打印发现每次组件二也会更新
import React, { memo } from 'react'
function Child2({data}){
console.log("组件二的数据", data)
return <div>
<p>this is child2 component</p>
</div>
}
export default memo(Child2)
给组件使用组件2使用memo,再次更新组件一的数据发现
发现,只更新了一次,这是为什么呢?
React.memo
会对组件的props做浅比较,发现组件参数变化会更新,接着我们看下memo的第二个参数
import React, { memo } from 'react'
function diff(prev, next){
console.log("prev", prev, next)
return true
}
function Child2({data}){
console.log("组件二的数据", data)
return <div>
<p>this is child2 component</p>
</div>
}
export default memo(Child2, diff)
第二个参数是一个函数,他有两个参数,更新之前和更新之后的props,在这里我们可以人工对比参数,返回true则不渲染组件、返回false渲染组件
useMemo 用来优化React组件内部返回值的渲染次数
import Child from './Child'
function Father(){
return <Child
data={useMemo(() => {
name,
childName: `名字:${name}`
}),[name]}
>组件
</Child>
}
根据依赖,返回更新的值
useCallback用来优化React组件内部函数的渲染次数
function Child(){
const [query, setQuery] = useState(1)
const getList = useCallback(()=>{
const data = {query}
fetchList(data).then(res => {
console.log("接口请求", res)
})
}, [query])
useEffect(() => {
getList()
}, [getList])
return <div></div>
}
getList函数依赖于query参数,本来useEffect不能依赖函数,使用useCallback以后,就可以作为依赖更新
2、useEffect使用注意事项
组件告警,提示如下
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.in Notification
如下,常见的数据请求方式
function List(){
return <div>
<router1>路由一</router1>
<router2>路由二</router2>
</div>
}
function Router1(){
const [data, setData] = useState([])
useEffect(() => {
getList().then(({Data}) => {
setData(Data)
})
}, [])
return <div>路由组件一</div>
}
function Router2(){
return <div>路由组件二</div>
}
这个告警常见于,请求接口后,请求发出后,相应较慢,但是用户已经切换了组件路由,退出并且销毁,接口返回的数据没法使用setData,报错。
解决这个问题一般情况下,在组件销毁后拒绝执行setState,具体的实现如下:
import React, { useRef } from 'react'
function Router1(){
const mounted = useRef(true)
useEffect(() => {
getList().then({Data}=>{
if(Data&&mounted.current){
setData(Data)
}
})
return () => {
mounted.current = false
}
}, [])
}