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

在React开发时,不只是重新渲染会影响性能。如果获取主要数据需要花费两秒钟的时间,那么即便有即时的重新渲染也帮不了你。又或者,如果页面在获取数据时显得非常 "杂乱无章",所有的用户界面元素都在动,加载指示器也转个不停,这会让你的用户看得头疼不已。在前端开发的领域,获取数据是非常麻烦的。

那么,在React中获取数据的最佳实践是什么?在这一章,将会学到:

  • 前端获取数据的方法。
  • 我们可以是仅仅使用fetch获取数据吗?
  • 性能好的应用意味着什么?
  • 在获取数据时,浏览器的限制是什么?
  • 什么是请求瀑布流,以及它如何出现?
  • 解决瀑布流问题的 一些方案。

获取数据的方式

概要性地说,在现代前端世界,有两种获取数据的方式:初始数据获取 与 按需数据获取 是 发生在用户在页面进行交互后。而在React中,这类数据获取,通常是在回调函数中触发的。

初始 数据获取,是发生在你打开一个页面,看到页面内容时。这是一项我们需要进口展示给用户的内容。在React中国呢,如果没有使用SSR,这类数据获取通常发生在useEffect中(如果是class组件,则是在componentDidMount)。

虽然这两个概念看着不一样,但其核心原则和基本模式是一样的。

开发React项目时,是否需要借助外部库来获取数据?

答案是,看情况而定。如果你仅仅是需要获取一些数据,不需要存储它,那只要在useEffect里使用fetch即可。

js 复制代码
const Component = () => {
    const [data, setData] = useState();
    
    useEffect(() => {
        // fetch data
        const dataFetch = async () => {
            const data = await (
                await fetch('https://run.mocky.io/v3/b3bcb9d2-d8e9-43c5-bfb7-0062c85be6f9',)
            ).json();
            // set state when the data received
            setData(data);
       };
    dataFetch();
    }, []);
    
    return <>...</>;
};

但是,一旦你的开发场景不再仅仅要求获取数据时,你就会遇到一系列难搞的问题了。如何进行错误处理?如果多个组件想获取同一个组件的数据该如何?我是否应该缓存数据?缓存多久?遇到竞态条件该怎么办?遇到内存泄露该怎么办?等等...

这些问题在开发React以外的项目时,也会遇到。这是获取数据时的普遍问题。要处理这类问题,无非是重新造轮子,或者借用现有的库。

像axios这样的库,封装了数据获取的基本概念,但并没有针对React进行特殊化处理。而像swr库,可以替我们做数据缓存,提升工作效率。但本质上而言,这些技术的选型并不重要,因为它们不过是以性能为代价,提升了开发者的效率罢了。为了开发出高性能的应用,你需要理解数据获取的基本原理,以及数据编排的模式与技术。

什么是性能好的应用?

在进入具体的模式设计和代码示例前,我们要搞明白什么是"性能好的应用"?其实也很简单,我们需要衡量多久能渲染出一个组件。而这个时间越少,性能就越好。

但是如果这个组件涉及到了异步操作,而且在大型应用中,衡量起来就不是这么简单了。

想象一下,我们正在为一个问题跟踪器实现一个问题查看界面。该界面左侧会有侧边栏导航,包含一系列链接;中间是主要的问题信息,比如问题标题、描述、负责人等;下方则是一个包含评论的部分。

而这个组件可以通过三种方法实现:

  1. 展示加载状态,直到所有数据都加载完,之后一次性完渲染页面所有内容。总计用时3秒。
  2. 先显示加载状态,直到侧边栏数据首先加载完成,然后渲染侧边栏,接着继续保持加载状态,直至主体部分的数据也加载完毕。侧边栏的显示大约需要 1 秒钟,应用程序其余部分的显示大约需要 3 秒钟。总体而言,整个过程大约需要 4 秒钟。
  3. 先显示加载状态,直到主要的问题数据加载完成,然后渲染该数据,同时对于侧边栏和评论部分继续保持加载状态。当侧边栏数据加载完成后,渲染侧边栏,此时评论部分仍处于加载状态。主体部分大约在 2 秒后显示,在那之后侧边栏大约 1 秒后显示,评论部分还需要大约 2 秒才能显示出来。总体来说,整个界面显示出来大约需要 5 秒钟。

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

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

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

这三个方案中,哪一个性能最好呢?

答案是,看具体情况。

第一个方案,就纯加载时间而言,它是用时最少的。但是在前三秒,用户什么都看不到。

第二个方案,只用了一秒就看到了侧边栏,似乎这个方案不错。但是它用了最长的时间才能看到页面的主要内容。

第三个方案,虽然最先展示了页面的主要内容,但是它的展示顺序,又与业界展示顺序相悖。。。

这总是取决于你试图向用户传达的信息。把你自己想象成一个讲故事的人,而应用程序就是你的故事。这个故事中最重要的部分是什么?第二重要的部分又是什么?你的故事有连贯性吗?你是可以将它分成若干部分来讲述,还是希望用户能立刻完整地看到整个故事,而没有任何中间步骤呢?

React的生命周期与数据获取

当你要调用接口请求数据时,你首先要搞清楚React的生命周期是如何被触发的。来看看这一段代码:

js 复制代码
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, setIsLoading] = useState(true);
    
    if (isLoading) return 'loading';

    return <Child />;
};

在这段代码中,Parent组件会基于状态,条件式地渲染Child组件。那么Child组件的useEffect会立即调用吗?但是,否。Child组件的useEffect只有在Parent组件的isLoading为false时,才会触发Child的渲染及其钩子函数。

如果我把Parent组件修改成这样,会如何:

js 复制代码
const Parent = () => {
    // set loading to true initially
    const [isLoading, setIsLoading] = useState(true);
    
    // child is now here ! before return
    const child = <Child />;
    
    if (isLoading) return 'loading';

    return child;
};

这段代码所实现的功能和之前是一样的:如果isLoadingfalse,展示 Child组件;如果isLoadingtrue,展示'loading'字符串。但是,现在Child是先于if代码执行,此时的useEffect会被执行吗?虽然答案很反直接,但还是不会。

当我们写const child = <Child />时,我们并没有渲染Child组件。<Child />不过是生成其描述对象的函数的语法糖罢了。只有当这个描述最终出现在实际可见的渲染树中时,它才会被渲染 ------ 也就是说,从组件中返回时才会被渲染。在那之前,它就只是作为一个对象闲置在那里,什么也不做。

React的生命周期还有很多知识点:生命周期被触发的顺序,绘图前触发了什么钩子,绘图后触发了什么钩子,哪些钩子拖延了渲染,关于useLayoutEffect等等。但所有这些在很久之后才会显得重要,当你已经完美地编排好了一切,并且现在正处于一个非常庞大、复杂的应用程序中,为争取哪怕几毫秒的性能提升而努力的时候。

浏览器的限制与数据获取

也许你会想,哎妈呀,这太复杂了。我们不能一次发起多个数据获取请求,把数据放进全局仓库,在有需要时使用这些数据,不就行了吗?干嘛老是为React生命周期和数据编排头疼?

我可以理解你的苦恼。如果这个应用比较简单,这样是可行的。但在大型应用中,我们有大量的数据获取请求要发起,这个策略是不行的。不行的原因有很多,除了要考虑服务器是否能承载这个请求频次外,浏览器本身对请求频次也有限制!!!

你知道浏览器限制了一次最多能发送几个数据获取请求吗?假设服务器使用的是 HTTP/1 协议(目前互联网中仍有 70% 的服务器使用该协议),这个数字其实并没有那么大。在谷歌浏览器(Chrome)中,这个数字仅仅是 6。也就是说只能同时进行 6 个并行请求!如果你同时发起更多的请求,那么其余所有请求都将不得不进入队列,等待第一个可用的时机。

而且对于一个大型应用程序来说,在初始数据获取时进行 6 个请求并非不合理。我们这个非常简单的 "问题追踪器" 就已经有三个请求了,而我们甚至还没有实现任何有实际价值的功能呢。想象一下,如果你在应用程序一开始就添加一个有点慢的分析请求,而这个请求实际上在一开始什么作用都没有起到,却最终导致整个应用体验变慢,你会得到多少愤怒的目光。

让我们看看代码吧:

js 复制代码
const App = () => {
    // I extracted fetching and useEffect into a hook
    const { data } = useData('/fetch-some-data');
    
    if (!data) return 'loading...';
    
    return <div>I'm an app</div>;
};

假设那里的获取请求超级快,仅耗时约 50 毫秒。如果在那个应用程序之前我仅仅添加六个耗时 10 秒的请求,并且不等待它们完成或对它们进行处理,那么整个应用程序的加载就会耗费这 10 秒钟(当然,是在谷歌浏览器中)。

js 复制代码
// no waiting, no resolving, just fetch and drop it
fetch('https://some-url.com/url1');
fetch('https://some-url.com/url2');
fetch('https://some-url.com/url3');
fetch('https://some-url.com/url4');
fetch('https://some-url.com/url5');
fetch('https://some-url.com/url6');

const App = () => {
  ... same app code
}

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

相关推荐
codingandsleeping1 分钟前
Express入门
javascript·后端·node.js
Vaclee4 分钟前
JavaScript-基础语法
开发语言·javascript·ecmascript
拉不动的猪26 分钟前
前端常见数组分析
前端·javascript·面试
小吕学编程42 分钟前
ES练习册
java·前端·elasticsearch
Asthenia04121 小时前
Netty编解码器详解与实战
前端
袁煦丞1 小时前
每天省2小时!这个网盘神器让我告别云存储混乱(附内网穿透神操作)
前端·程序员·远程工作
一个专注写代码的程序媛2 小时前
vue组件间通信
前端·javascript·vue.js
一笑code2 小时前
美团社招一面
前端·javascript·vue.js
懒懒是个程序员3 小时前
layui时间范围
前端·javascript·layui
NoneCoder3 小时前
HTML响应式网页设计与跨平台适配
前端·html