数据管理类库;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)
  ...
}