第十四章 客户端的数据获取 与 性能 【中】

143 阅读5分钟

请求瀑布流是如何出现的

终于,进入严肃的编码时间了。我们现在有了需要被移动的块,也知道他们是如何被组合起来的,就可以写问题项追逐app了。让我们从刚刚的示例代码开始写:

const App = () => {
    return (
      <>
        <Sidebar />
        <Issue />
      </>
    );
  };
  const Sidebar = () => {
    return; // some sidebar links
  };
  
  const Issue = () => {
    return (
      <>
        <Comments />
      </>
  );
};

const Comments = () => {
  return; // some issue comments
};

然后,我们把获取数据的操作放进一个钩子里:

export const useDtat = (url) => {
    const [state, setState] = useState();
    
    useEffect(() => {
        const dataFetch = async () => {
        
            const data = await (await fetch(url)).json();
            setState(data);
        }
    
        dataFetch();
    }, [url])
    
    return { data: state };
}

然后,就在Comment组件可以快乐的调用了:

const Comments = () => {
    // fetch is triggered in useEffect there, as normal
    const { data } = useData('/get-comments');
    
    // show loading state while waiting for the data
    if (!data) return 'loading';
    
    // rendering comments now that we have access to them!
    return data.map((comment) => <div>{comment.title}</div>);
};

然后,就在Issue组件也类似:

const Issue = () => {
    // fetch is triggered in useEffect there, as normal
    const { data } = useData('/get-issue');
    
    // show loading state while waiting for the data
    if (!data) return 'loading';
    
    // render actual issue now that the data is here!
    return (
      <div>
        <h3>{data.title}</h3>
        <p>{data.description}</p>
        <Comments />
      </div>
    );
 };

还有在App组件调用:

const App = () => {
    // fetch is triggered in useEffect there, as normal
    const { data } = useData('/get-sidebar');
    
    // show loading state while waiting for the data
    if (!data) return 'loading';
    
    return (
        <>
            <Sidebar data={data} />
            <Issue />
        </>
    );     
 }:  

然后,就完成了~

代码示例: advanced-react.com/examples/14…

但这个应用有一个小问题,非常的慢。

我们这个示例,是一个典型的瀑布流请求。还记得React的生命周期吗,只有当组件被return时,它们才会变挂载,渲染,然后在触发获取数据的钩子。在我们的例子中,每一个组件在等待数据加载时,都是显示 loading。只有当数据加载完成后,才会切换为另一个组件,再触发其获取数据的钩子,并如此循环往复。

image.png

如果你想向用户尽快展示页面,使用瀑布流不是最好的方案。接下来,我们将会介绍处理瀑布流问题的方案。

如何处理瀑布流问题

Promise.all 方法

最简单的方法,就是尽可能早地获取所有数据。在这个示例中,就是在App组件处获取所有数据。但要注意的是,我们仅仅是普通的调用,是远远不够的。这样的代码是不行的:

useEffect(async () => {
    const sidebar = await fetch('/get-sidebar');
    const issue = await fetch('/get-issue');
    const comments = await fetch('/get-comments');
}, [])

这样的代码不过是另一个瀑布流。加载所有接口的时间是 1s + 2s + 3s = 6s。我们需要一次性调用所有接口。如此一来,我们只需要等待三秒,获得了50%的性能提升!

其中一个方法是 Promise.all

useEffect(async () => {
    const [sidebar, issue, comments] = await Promise.all([
    fetch('/get-sidebar'),
    fetch('/get-issue'),
    fetch('/get-comments'),
    ]);
}, []);

之后,要把所有的值作为状态存在父组件中,再把这些状态作为属性传给子组件。

const useAllData = () => {
    const [sidebar, setSidebar] = useState();
    const [comments, setComments] = useState();
    const [issue, setIssue] = useState();
    useEffect(() => {
      const dataFetch = async () => {
        // waiting for allthethings in parallel
        const result = (
          await Promise.all([
            fetch(sidebarUrl),
            fetch(issueUrl),
            fetch(commentsUrl),
          ])
        ).map((r) => r.json());
        // and waiting a bit more - fetch API is cumbersome
        const [sidebarResult, issueResult, commentsResult] =
          await Promise.all(result);
        // when the data is ready, save it to state
        setSidebar(sidebarResult);
        setIssue(issueResult);
        setComments(commentsResult);
      };
      dataFetch();
    }, []);
    return { sidebar, comments, issue };
  };
  
  const App = () => {
    // all the fetches were triggered in parallel
    const { sidebar, comments, issue } = useAllData();
    // show loading state while waiting for all the data
    if (!sidebar || !comments || !issue) return 'loading';
      // render the actual app here and pass data from state to children
    return (
        <>
            <Sidebar data={state.sidebar} />
            <Issue
              comments={state.comments}
              issue={state.issue}
            />
        </>
     );
  };

代码示例: advanced-react.com/examples/14…

如此一来,应用启动就快了很多。

image.png

并行Promises 方法

但是,如果我不想等到所有接口加载完数据才展示内容,该怎么办。在我们这个例子中,Comment的数据是最慢获取的,也是最不重要的。为了等待这个数据,而给用户造成卡顿,真的值得吗?我们可以一起调用所有的接口,同时又保证这些接口互相独立吗?

当然可以!我们只需要将那些使用 async/await 语法的获取操作转换为合适的传统 Promise 形式,并在 then 回调函数中保存数据。

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));

如此一来,每一个数据获取都是独立的。那么App就可以先展示SiderbarIssue组件的内容了。

const App = () => {
    const { sidebar, issue, comments } = useAllData();
    
    // show loading state while waiting for sidebar
    if (!sidebar) return 'loading';
    
    // render sidebar as soon as its data is available
    // but show loading state instead of issue and comments while we're waiting for them
    return (
    <>
        <Sidebar data={sidebar} />
        {/*render local loading state for issue here if its data not available*/}
        {/*inside Issue component we'd have to render 'loading' for empty comments as well*/}
        {issue ? <Issue comments={comments} issue={issue} /> : 'loading''}
    </>
    )
 }

在这里,一旦SidebarIssue和评论Comments组件的数据可用,我们就立即渲染它们 —— 这与初始的瀑布流模式行为完全相同。但由于我们并行发起了这些请求,总体等待时间将从 6 秒降至仅 3 秒。我们在保持应用程序行为不变的情况下,大幅提升了其性能!

代码示例: advanced-react.com/examples/14…

image.png

但是,需要注意的是,我们在组件的最顶端触发了三次状态修改,也就触发了三次重新渲染。这些不必要的重新渲染,也会影响应用的性能,但也要视应用的大小、组件在组件树的位置而定。