Appearance
在自身使用 React
实现业务逻辑的时候,经常会遇到组件内部请求数据的场景,如果不依赖第三方我们一般会使用如下代码:
import React, { useEffect, useState } from 'react'
function ajaxData() {
return fetch('https://jsonplaceholder.typicode.com/users').then((response) =>
response.json()
)
}
function App() {
const [loading, setLoading] = useState(false)
const [data, setData] = useState([])
useEffect(() => {
setLoading(true)
ajaxData()
.then((res) => {
setData(res)
console.log('no optimize res', res)
})
.finally(() => {
console.log('no optimize finally')
setLoading(false)
})
}, [])
if (loading) {
return <>Loading...</>
}
return (
<>
Main Content
<div>
{data.map((v) => (
<div key={v.id}>{v.name}</div>
))}
</div>
</>
)
}
export default function Root() {
const [key, setKey] = useState(0)
return (
<div style={{ position: 'relative' }}>
<App key={key} />
<div style={{ position: 'absolute', right: 0, top: 0 }}>
<button className="demo-btn" onClick={() => setKey((pre) => pre + 1)}>
refresh
</button>
</div>
</div>
)
}
但是以上例子会存在内存泄露的风险, 5-6
行当因为组件销毁的情况发生时,例如马上跳转了其它页面,因为闭包原因,你会发现它仍然会执行。此处你可以尝试多点击几次 refresh
按钮,来复现这种情况。
useEffect(() => {
setLoading(true)
ajaxData()
.then((res) => {
setData(res.data)
console.log(res)
})
.finally(() => {
console.log('finally')
setLoading(false)
})
}, [])
针对以上情况,优化方案如下
同时在代码复用层面,以上也可以进行优化,毕竟大家也不想每个组件内部的请求都重复以上的逻辑,因此当以上优化处理后,可以增加自定义 hooks 进行包装.
我门首先增加组件销毁的状态判断,代码如下:
import React, { useEffect, useRef, useState } from 'react'
async function ajaxData() {
return fetch('https://jsonplaceholder.typicode.com/users').then((response) =>
response.json()
)
}
function App() {
const unMountedRef = useRef()
const [loading, setLoading] = useState(false)
const [data, setData] = useState([])
useEffect(() => {
setLoading(true)
unMountedRef.current = false
const promise = ajaxData()
promise
.then((res) => {
if (!unMountedRef.current) {
setData(res)
console.log('optimize res', res)
}
})
.finally(() => {
if (!unMountedRef.current) {
console.log('optimize finally')
}
setLoading(false)
})
return () => {
unMountedRef.current = true
}
}, [])
if (loading) {
return <>Loading...</>
}
return (
<>
Main Content
<div>
{data.map((v) => (
<div key={v.id}>{v.name}</div>
))}
</div>
</>
)
}
export default function Root() {
const [key, setKey] = useState(0)
return (
<div style={{ position: 'relative' }}>
<App key={key} />
<div style={{ position: 'absolute', right: 0, top: 0 }}>
<button className="demo-btn" onClick={() => setKey((pre) => pre + 1)}>
refresh
</button>
</div>
</div>
)
}
以上代码当当组件销毁后,不会再执行回掉中的操作,避免了内存溢出的风险。让我门再进一步,将接口请求也取消试试,代码如下:
import React, { useEffect, useRef, useState } from 'react'
function ajaxData() {
const controller = new AbortController()
const signal = controller.signal
const promise = fetch('https://jsonplaceholder.typicode.com/users', {
signal
}).then((response) => response.json())
return Object.assign(promise, {
abort(...reset) {
controller.abort(reset)
}
})
}
function App() {
const unMountedRef = useRef()
const [loading, setLoading] = useState(false)
const [data, setData] = useState([])
useEffect(() => {
setLoading(true)
unMountedRef.current = false
const promise = ajaxData()
promise
.then((res) => {
if (!unMountedRef.current) {
setData(res)
console.log('optimize-fetch res', res)
}
})
.finally(() => {
if (!unMountedRef.current) {
console.log('optimize-fetch finally')
setLoading(false)
}
})
return () => {
promise.abort('component destroy')
unMountedRef.current = true
}
}, [])
if (loading) {
return <>Loading...</>
}
return (
<>
Main Content
<div>
{data.map((v) => (
<div key={v.id}>{v.name}</div>
))}
</div>
</>
)
}
export default function Root() {
const [key, setKey] = useState(0)
return (
<div style={{ position: 'relative' }}>
<App key={key} />
<div style={{ position: 'absolute', right: 0, top: 0 }}>
<button className="demo-btn" onClick={() => setKey((pre) => pre + 1)}>
refresh
</button>
</div>
</div>
)
}
以上代同时当组件销毁的时候,取消了相关的web
请求操作,同时又增加了销毁判断,减少闭包导致的内存溢出风险。
接下来,我们一起将此功能进行封装,复用下相关逻辑,代码如下:
import React, { useState } from 'react'
import useRequest from './useRequest'
function ajaxData() {
const controller = new AbortController()
const signal = controller.signal
const promise = fetch('https://jsonplaceholder.typicode.com/users', {
signal
}).then((response) => response.json())
return Object.assign(promise, {
abort(...reset) {
controller.abort(reset)
}
})
}
function App() {
const { loading, data } = useRequest(ajaxData)
if (loading) {
return <>Loading...</>
}
return (
<>
Main Content
<div>
{data.map((v) => (
<div key={v.id}>{v.name}</div>
))}
</div>
</>
)
}
export default function Root() {
const [key, setKey] = useState(0)
return (
<div style={{ position: 'relative' }}>
<App key={key} />
<div style={{ position: 'absolute', right: 0, top: 0 }}>
<button className="demo-btn" onClick={() => setKey((pre) => pre + 1)}>
refresh
</button>
</div>
</div>
)
}
import { useEffect, useRef, useState } from 'react'
export default function useRequest(fn) {
const unMountedRef = useRef()
const [loading, setLoading] = useState(false)
const [data, setData] = useState([])
useEffect(() => {
setLoading(true)
unMountedRef.current = false
const promise = fn()
promise
.then((res) => {
if (!unMountedRef.current) {
setData(res)
console.log('optimize-hook res', res)
}
})
.finally(() => {
if (!unMountedRef.current) {
console.log('optimize-hook finally')
setLoading(false)
}
})
return () => {
if (promise && promise.abort) {
promise.abort('component destroy')
}
unMountedRef.current = true
}
}, [])
return {
loading,
data
}
}
以上hook
使用,缺少参数传递,我们把这个加上,当前函数签名如下:
function useRequest<TBody>(
service: Promise<TBody>,
params?: Record<string, any>
): { loading: boolean, data: TBody }
具体实现如下,其中加入了以下功能:
params
改动会自动进行数据重新请求params
进行了深度比较优化,优化触发频率 import React, { useState } from 'react'
import useRequest from './useRequestFinal'
function ajaxData(params = {}) {
const controller = new AbortController()
const signal = controller.signal
const url = new URL('https://jsonplaceholder.typicode.com/comments')
Object.keys(params).forEach((field) => {
url.searchParams.set(field, params[field])
})
const promise = fetch(url, {
signal
}).then((response) => response.json())
return Object.assign(promise, {
abort(...reset) {
controller.abort(reset)
}
})
}
function App() {
const [params, setParams] = useState({
postId: 1
})
const { loading, data } = useRequest(ajaxData, params)
if (loading) {
return <>Loading...</>
}
return (
<>
<div>
<button
className="demo-btn"
onClick={() => {
setParams((pre) => ({ postId: pre.postId + 1 }))
}}
>
differ params
</button>
<button
className="demo-btn"
onClick={() => setParams({ postId: params.postId })}
>
same params
</button>
</div>
Main Content
<div>
{data.map((v) => (
<div key={v.id}>{v.name}</div>
))}
</div>
</>
)
}
export default function Root() {
const [key, setKey] = useState(0)
return (
<div style={{ position: 'relative' }}>
<App key={key} />
<div style={{ position: 'absolute', right: 0, top: 0 }}>
<button className="demo-btn" onClick={() => setKey((pre) => pre + 1)}>
refresh
</button>
</div>
</div>
)
}
import { useRef, useState } from 'react'
import { useCompareEffect } from '@slsanyi/hooks'
export default function useRequest(fn, params) {
const unMountedRef = useRef()
const [loading, setLoading] = useState(false)
const [data, setData] = useState([])
useCompareEffect(() => {
setLoading(true)
unMountedRef.current = false
const promise = fn(params)
promise
.then((res) => {
if (!unMountedRef.current) {
setData(res)
console.log('optimize-hook-final res', res)
}
})
.finally(() => {
if (!unMountedRef.current) {
console.log('optimize-hook-final finally')
setLoading(false)
}
})
return () => {
if (promise && promise.abort) {
promise.abort('component destroy')
}
unMountedRef.current = true
}
}, [params])
return {
loading,
data
}
}
以上是简易实现 useRequest
的一种方式,有如下功能
web
请求自动取消 方式.web
请求.当然,其中也包含了一些可以提炼出来的能力
深度对比Effect, 可参考
useDeepCompareEffect获取上一个props的hook, 可参考
usePrevious深度对比值帮助函数,可参考
react-fast-compare对于简单的项目,可以参照以上自行实现处理。但是对于公司项目来说,可以使用成熟的库更加合适 例如 ahooks 或者 reactuses,它们可以更好的帮助你管理和复用代码,当然前提是你需要习惯那种代码风格 😜