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

在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...

相关推荐
GISer_Jing5 分钟前
React Native从入门到进阶详解
javascript·react native·react.js
Pandaconda8 分钟前
【后端开发面试题】每日 3 题(五)
javascript·数据库·mysql·golang·node.js·go·协程
黑风风20 分钟前
深入理解 Promise 和 Async/Await,并结合 Axios 实践
开发语言·前端·javascript
我命由我1234529 分钟前
Tailwind CSS 问题:npm error could not determine executable to run
前端·css·前端框架·npm·node.js·html·html5
浩男孩1 小时前
面试官提问:TypeScript 中的 Type 和 Interface 有什么区别?
前端·typescript
m0_582481491 小时前
qt作业day2
java·linux·前端
好想Z☡zᶻ1 小时前
调用的子组件中使用v-model绑定数据以及使用@调用方法
前端·javascript·vue.js
new Vue()1 小时前
el-table input textarea 文本域 自适应高度,切换分页滚动失效处理办法
javascript·vue.js·elementui
seven1082 小时前
cursor MCP server 如何AI 编程中实现动态数据获取
前端·cursor·mcp
予安灵2 小时前
《白帽子讲 Web 安全》之文件操作安全
前端·安全·web安全·系统安全·网络攻击模型·安全威胁分析·文件操作安全