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

138 阅读6分钟

通过数据Provider来抽象数据获取

之前的例子,本质上就是把状态提升到了渲染树的顶部,虽然这样可以提升应用的性能,但这样的架构设计是糟糕的,代码可读性也不好。

当这个应用后面需要跨组件传值时,开发体验会非常糟糕。

幸运的是,有一个简单的方法可以解决这个问题:我们可以为这个应用引入“data provider”。在React中,“data provider”实质上就是一个context:

const Context = React.createContext();

export const CommentDataProvider = ({ children }) => {
    const [comment, setComment] = useState();
    
    useEffect(async () => {
        fetch('/get-comments')
            .then((data) => data.json())
            .then((data) => setComment(data))
    }, []);
    
    return (
        <Context.Provider value={comment}>
            {children}
        </Context.Provider>
    )
}

export const useComment = () => useComment(Context);

对于另外几套数据的请求,其逻辑是一样的。之后,我们的App组件就简化成这样了:

const App = () => {
    const sidebar= useSidebar();
    const issue = useIssue();

    // show loading status while waiting for sidebar
    if (!sidebar) return 'loading';
    
    // no more props drilling for any of those
    return (
        <>
            <Sidebar />
            {issue ? <Issue /> : 'loading'}
        </>
    )
}

之后,这三个“data provider”会包裹整个App组件,当App组件被挂载时,这三个provider的数据会被并行请求。

export const VeryRootApp = () => { 
  return
      (
        <SidebarDataProvider>
          <IssueDataProvider>
            <CommentsDataProvider>
              <App />
            </CommentsDataProvider>
          </IssueDataProvider>
        </SidebarDataProvider>
      );
};

如此一来,如果更深处的Comments想获取相关数据,直接通过“data provider”即可。

const Comments = () => {
  // Look! No props drilling!
  const comments = useComments();
 };

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

如果我在React启动前获取数据,会怎么样?

这是最后一个处理瀑布流的技巧。理解它是非常重要的,这样你就可以在Code Review 和 PR Review时阻止同事这么做。因为这么做是非常危险的,需慎重的使用这个技巧。

让我们再次看看Comment组件,现在的Comment在组件内部获取数据。

const Comments = () => {
      const [data, setData] = useState();

      useEffect(() => {
          const dataFetch = async () => {
              const data = await (
                  await fetch('/get-comments')
              ).json(); 

              setData(data);
          };

          dataFetch();
      }, [url]);
      if (!data) return 'loading';
     return data.map((comment) => <div>{comment.title}</div>);
 };

注意第六行的代码。什么是fetch('/get-comments')?它本质上是在useEffect里调用的一个promise。这个promise的调用并不依赖这个组件的属性、状态等React生命周期内的事物。那么,如果我把这段代码移动到更顶端,在声明这个组件前调用这个promise会怎么样?之后在Comment组件的useEffect钩子里await这个promise就行了。

const commentsPromise = fetch('/get-comments');

const Comments = () => {
    useEffect(() => {
        const dataFetch = async () => {
            // just await the variable here
            const data = await (await commentsPromise).json();

            setState(data);
        };

        dataFetch();
    }, [url]);
};

一个非常神奇的事实:这个fetch调用基本上‘逃避’了所有React生命周期,并且会在JavaScript在页面上加载后立即执行,这甚至发生在根组件的第一个请求之前。它将被触发,JavaScript会继续处理其他任务,而数据会默默地等待,直到有人真正解析它。

还记得瀑布流的图片吗?

image.png

这是把Comments的数据请求放在React App启动之前:

image.png

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

理论上而言,我们可以通过把数据请求置于 React 生命周期之外,这样就解决了瀑布流问题。但是为什么这种做法没有被普遍采用呢?

很简单。这是因为浏览器的限制。“并发只有6个请求,下一个请求将被排队。并且这样的fetch调用将立即且完全不可控地被触发。一个组件如果获取大量数据并且在应用中很少渲染,那么在传统的瀑布式方法中,它不会引起任何问题,直到它实际被渲染。但是使用这个技巧,它可能会占用获取关键数据的宝贵时间。对于任何试图理解为什么一个从未在屏幕上渲染的组件会减慢整个应用的人来说,祝你好运。

在我看来,这种模式只有两个‘合理’的使用场景:在路由器级别预加载一些关键资源,以及在懒加载组件中预加载数据。

在第一种情况下,你实际上需要尽快获取数据,并且你肯定知道这些数据是关键的且需要立即使用。对于懒加载组件来说,它们的JavaScript代码只有在它们进入渲染树时才会被下载和执行,这意味着在所有关键数据被获取和渲染之后。因此,这是安全的。

如果我用外部库来获取数据?

目前为止,我们所有的例子使用的都是原生的fetch方法。在实际开发环境中,人们想使用独立于React的数据获取库。但是不论你使用什么库,产生瀑布流的原理是一样的,在React生命周期内和周期外获取数据的原理也是一样的。

像独立于React的Axios库,本质上就是把复杂的fetch抽象为axios.get

与React集成的库,如SWR等,使用类似查询的API,额外地抽象了处理useCallback、状态以及其他诸如错误处理和缓存等事项。相比之下,不使用这些库的代码会非常复杂,仍需要很多额外的工作才能达到生产就绪状态。

const Comments = () => {
    const [data, setData] = useState();
    
    useEffect(() => {
    const dataFetch = async () => {
        const data = await (
            await fetch('/get-comments')
        ).json();
        
        setState(data);
    };
        dataFetch();
    }, [url]);
   
    // the rest of comments code
};

使用了swr后,代码会是这样:

const Comments = () => {
    const { data } = useSWR('/get-comments', fetcher)
   
    // the rest of comments code
};

使用Suspense会怎么样?

在讨论数据获取时,如果漏了Suspense,那这个讨论是不完整的。那么,Suspense呢?其实没有什么特别的。在这本书出版时,Suspense用于数据获取仍然是一个未经文档记录的特性,在像Next.js这样的框架之外,React并不官方支持或推荐使用它。

image.png

如果你有机会使用了这些框架,你将会阅读相关文档,来学习这个API。

如果有一天,这个特性被普遍使用了,这个特性可以很好的解决数据获取的问题吗?并不行。

Suspense本质上就是用一个更酷炫的方来展示数据加载时的loading的方法罢了。

没有Suspense时,我们这样写:

const Comments = ({ commments }) => {
    if (!comments) return 'loading';
    
    // render comments
};

有了Suspense后,代码是这样的:

const Issue = () => {
    return (
        <>
            {/*issue data*/}
            <Suspense fallback="loading">
                <Comments />
            </Suspense>
        </>
    )
};

除此之外,浏览器的限制依旧存在,React生命周期不变,瀑布流的本质不变。

知识概要

在前端开发领域,数据获取一直是一个复杂的问题。在下一章,我们将会讨论继续讨论这个问题,要讨论竞态条件。在此之前,我们需要掌握:

  • 我们可以把数据获取分为两类:初始获取和按需获取。
  • 我们可以用fetch来获取数据,而非第三方库。但是这样要手动处理很多情况。
  • 一个应用是否性能出色,是一个主观的问题。这取决于你想要传达什么信息给用户。
  • 在获取数据时,尤其是初始数据获取,我们需要关注浏览器在并行请求上的限制。
  • 当多条数据不能被并行请求时,就会产生瀑布流问题。
  • 我们可以使用Promise.all,并行 promises,或者 Context 来处理瀑布流问题。
  • 我们可以在React初始化之前预先加载关键资源,但在此之前,我们要铭记浏览器对并行请求的限制。