前端数据获取(data fetching)
数据管理类库;GraphQL;Suspence;useEffect瀑布流;fetch-on-render,fetch-then-render,render-as-you-fetch模式;
数据获取分类
- 初始数据获取(initial data fetching)
- 按需数据获取(data fetching on demand)
React中,对于初始数据获取,通常在useEffect(或componentDidMount)中来发起这类数据请求
const component = () => {
const [data, setData] = useState()
useEffect(async () => {
// fetch data
const data = await(await fetch('...').json())
// set state when the data received
setState(data)
}, [])
return <>...<>
}
当场景变复杂时,面临的棘手问题:错误处理如何实现?如何处理多个组件从同一个接口获取数据?这些数据是否要缓存?缓存时间是多久?竞态问题(race conditions)如何处理?如果从屏幕上删除组件,该怎么半:取消请求?内存泄露如何解决?
方式1:造轮子
方式2:依靠成熟类库,axios将一些功能进行了抽象和封装,swr能处理几乎所有的事情包括缓存。
应用的性能
如何测量性能?只需测量渲染的耗时,数字越小,性能越好。
数据数据属于典型的异步操作。
React生命周期与数据获取
设计数据请求方案时,要特别注意React生命周期被触发的时机。
const child = () => {
useEffect(() => {
// do something here, like fetching data for the child
}, [])
return <div>Some child</div>
}
const Parent = () => {
// set loading to true initially
const [isLoading, setIseLoading] = useState(true)
if (isLoading) return 'loading'
return <Child />
}
浏览器限制和数据获取
浏览器对相同的host可以处理的并行请求数是有限制的。假设服务器是HTTP1(仍占互联网的70%),在Chrome中,最多只能有6个并行请求。
如果同时发起更多请求,剩下的都必须排队。
经典的请求瀑布流,fetch sidebar -> fetch issue -> fetch components。但这个并不算好方案,效率太低。
解决请求瀑布流的方案
Promise.all
将请求尽可能放在组件树的最顶层。不要使用以下:
// 串行请求
useEffect(async () => {
const sidebar = await fetch('/get-sidebar')
const issue = await fetch('/get-issue')
const comments = await fetch('/get-comments')
}, [])
使用 Promise.all:
// 并发请求
useEffect(async () => {
const [sidebar, issue, comments] = await Promise.all([
fetch('/get-sidebar'),
fetch('/get-issue'),
fetch('/get-comments')
])
}, [])
并发后独立渲染
fetch('/get-sidebar').then(data => data.json()).then(data => setSidebar(data))
fetch('/get-issue').then(data => data.json()).then(data => setIssue(data))
fetch('/get-comments').then(data => data.json()).then(data => setComments(data))
这里要注意,值触发了三次state变化,会引起父组件的三次重新渲染,考虑到这些重新渲染发生在顶层组件,像这样的不必要的重新渲染会引起App中较多的不必要的重新渲染。
抽象封装数据获取(Data providers)
data providers是对数据请求的一种抽象,能让我们在app中的某个地方请求数据,然后在其他地方访问数据,可以绕过中间的所有组件。
本质是为每个请求做一层迷你的缓存。(在原生React中,它是一个context)
const Context = React.createContext()
export const CommentsDataProvider = ({ children }) => {
const [comments, setComments] = useState()
useEffect(async () => {
fetch('/get-comments').then(data => data.json()).then(data => setComments(data));
}, [])
return (
<Context.Provider value={comments}>
{children}
</Context.Provider>
)
}
export const comments = () => useContext(commentsConext)
进一步改造 App:
const App = () => {
const sidebar = useSidebar();
const issue = useIssue();
// show loading state while waiting for sidebar
if (!sidebar) return 'loading'
// no more props drilling for any of those
return (
<>
<Sidebar />
${issue ? <Issue /> : 'loading'}
</>
)
}
用这三个Provider来包裹App组件,只要它们被挂载,就会立即并发请求数据:
export const veryRootApp = () => {
return (
<SidebarDataProvider>
<IssueDataProvider>
<CommentsDataProvider>
<App />
</CommentsDataProvider>
</IssueDataProvider>
</SidebarDataProvider>
)
}
访问 Comments 这种层级较深的组件时,通过 data provider 来访问数据:
const Comments = () => {
// Look! No props drilling!
const comments = useComments();
}
将请求数据独立出来
const commentsPromise = fetch('/get-comments')
const Comments = () => {
...
const data = await (await commentsPromise).json()
...
}
但这种逃离于组件的请求,容易不可控,还存在请求阻塞的风险。
这种方式的适用场景:
- 在路由层预加载一些关键资源
- 在lazy-loaded组件中预请求数据
使用第三方库进行一些简化
const Comments = () => {
const { data } = useSWR('/get-comments', fetcher)
...
}