likes
comments
collection
share

《你可能不知道的React》-- 一个普普通通的UI库

作者站长头像
站长
· 阅读数 22

一个普普通通的UI库

建议时长: 🍱 20分钟

“路漫漫其修远兮,吾将上下而求索。” ——《离骚》

《你可能不知道的React》-- 一个普普通通的UI库

背景 🐣

最近重新回看了一遍JSConf Iceland 2018中 Dan Abramov的演讲,发现虽然是几年前的演讲了,但是却道出了React的使命,强烈推荐各位看官,或许对React会有新的认识。

书接上回,上篇文章提到了React在不断为开发者提供持续高效地开发UI界面的能力。有小伙伴和我说不太理解这意思,那么这一次我们就结合演讲,深入的讲解一下。

《你可能不知道的React》-- 一个普普通通的UI库

正文 🤔

React的使命

我们通过两个场景来聊一聊React的使命。

场景1

给定一个输入框,输入框变化时,搜索对应的商品列表并展示。

说明🪧:输入内容越复杂,商品列表也越复杂。

在这个场景下,我们不难发现,如果不做任何优化,低端机根本跟不上,就会造成页面卡顿掉帧,甚至“假死”,我们的输入就无法等到快速的响应。

如果我们把展示的商品列表细化成一个个的React元素,那么当元素达到一定的数量和层级,就变成了一棵巨复杂的渲染树🌲,然后我们再把渲染树变化给可视化,通过图表来反应变化就会变得很直观。

这其实就是Dan在演讲中演示1的Demo,由于没有找到原版的代码,找了好久,从codesanbox上fork了一个项目,想体验的小伙伴可以前往这个简化版的☁️ Demo玩一玩。

《你可能不知道的React》-- 一个普普通通的UI库

从上图中不难发现,对于这类场景,我们现在最常用的方案就是防抖/节流。但是这种一把梭的降级方案有个很明显的问题,因为无法做到设备自适应,使得性能好的机器也跟着遭罪,让部分用户群体丢失了原有的体验。

这便引发了React的第一个使命——解决CPU瓶颈, 那么何为CPU瓶颈呢?简单来说,就是在React看来,CPU解决的是创建React元素、渲染等同步的工作,需要解决不同性能的设备表现不一致的问题。

React给出的方案就是Time Slicing(时间分片)。关于时间分片的解释,其实就是把复杂的工作任务(大概大于50ms)拆分成一个个执行时间很短的任务去逐个执行,这种机制被称为时间分片。

之前看过一个很形象的解释就是“蚂蚁搬家”,按照这个解释,时间分片就是搬的东西(任务)不会少,根据每只蚂蚁的能力(设备性能)搬运适量的东西,而且聪明的蚂蚁优先搬运重要的东西。

《你可能不知道的React》-- 一个普普通通的UI库


场景2

给定一个新闻列表,点击可查看新闻的详细信息和相关评论。

说明🪧:每个新闻都需要发起网络请求,新闻详情页包含了内容和评论等多个板块,且不同类目的新闻复杂程度不同。

React Hooks出现之后,这种需求你会怎么实现呢?

其实大体思路就是,获取新闻列表并展示,当用户点击具体的新闻时,根据新闻id请求具体的详情信息,返回后展示详情页内容。大概的实现如下👇:

function App() {
    const [news, setNews] = useState(defaultNews);
    const [detail, setDetail] = useState(null);
    const [loading, setLoading] = useState(false)
    const handleOnNewsClick = async (id) => {
        setLoading(true)
        const { data } = await fetchNewsDetail(id);
        setDetail({ ...data });
        setLoading(false);
        gotoNewsDetailPage()
    };

    return (
        <>
            <NewsListPage data={news} onNewsClick={handleOnNewsClick} />
            <NewsDetailPage detail={detail} />
            <Loading loading={loading} />
        </>
    );
}

在上面的实现中,如果你对UI开发有自己的思考,会发现存在有几个问题。

我们以交互为目的出发,不难得出,当我们点击具体的新闻,是希望能立马看到具体的内容的。可是由于网络的差异,网络请求使我们不可忽略的一环,我们并不总能立马看到内容,这时候为了提高一点点用户体验,我们会适当得加上类似加载中的效果,但是归根结底,这种Data Fetching和Loading其实都是页面UI呈现的中间态,也就是说,如果存在一个页面加载器,在处理网络请求过程中的把数据状态化,通过各种状态(等待态、错误态、页面数据状态等)去驱动UI,那么作为开发者,就可以更关注UI界面的开发,这对于大型项目和交互复杂的项目,简直是天大的福音。

然后我们从用户体验的优先级为切入点,发现用户在看到具体的新闻详情之前,其实不太关注评论模块、封面模块等非主流模块,这就意味着,如果这些资源和新闻详情的资源同时加载,无疑是拖慢了速度,降低了用户体验。换句话说,这些非主要模块其实是低优先级的,是可以延迟加载的,这其实就是所谓的Code Splitting技术。

如果你看过jsconf演讲中Dan关于demo2的演示,就会发现其实React一直在致力于解决上面提到的问题。

针对Data Fetching, 我们通过引入createFetcher接口来实现异步读取数据,不仅提供缓存数据的能力,还会等待请求响应后再渲染具体UI,下面通过伪代码改造一下新闻详情页,改造如下:

import {createFetcher} from 'react-future' // 未来的特性 🔴
const newsDetailFetcher = createFetcher(fetchNewsDetail)

function NewsDetailPage() {
  const detail = newsDetailFetcher.read(id)

  return  <Detail {...detail} />
}

上面只是针对接口请求做,如果要加上Code Splitting,我们可以通过引入内置fetcher的页面加载器,实现动态加载资源, 伪代码如下:

import {createFetcher} from 'react-future'
const newsDetailPageFetcher = createFetcher(
  () => import('./NewsDetailPage')
)

function DetailPageLoader(props) {
  const NewsDetailPage = newsDetailPageFetcher.read().default

  return <NewsDetailPage {...props} />
}

同理地,评论模块和封面等模块都可以使用上面的方式实现资源的动态加载。

👆上面的createFetcher其实是一个拦截了传入的Promise中的then方法,在Promise状态变化时进行对应的操作,这里很巧妙的用到了throw语法,因此外面组件/页面加载器需要对这种行为做拦截处理的,下面给一下简单的实现方案:

function createFetcher(promise) {
    let status = "pending"
    let res
    const handler = promise.then((value) => {
       status = "success"
       res = value
    }, (reason) => {
       status = "error";
       res = reason
    })
    
    return {
       read() {
           switch(status) {
               case "pending":
                   throw handler
               case "error":
                   throw res
               case "success":
                   return res
           }
       }
    }
    
}

通过上面的改进,我们可以进一步窥探React的第二个使命,就是致力于解决IO瓶颈。按照Dan的话总结,便是:

We've built a generic way to suspend rendering while they load asynchronous data.

(我们已经构建了一种通用方法来在加载异步数据时暂停渲染。)

如果你足够仔细,可以发现上面的代码都是来自react-future这个未来的库,说白了就是还没有实现🤣;关于这些未来的特性,React给出的方案就是Suspense,那什么是Suspense呢?

Suspense不是一个数据请求的库,而是一种机制,就像是上面的createFetcher一样,不关心你的数据请求,只是在请求响应变化和UI更新直接建立联系的机制,从而避免了开发者书写类似if(loading) return <Loading />这样的样板代码。很遗憾的是目前仅支持通过React.lazy动态加载组件这一场景,即使是React18,也只是兼容了SSR,不过可以看出,React团队对Suspense还是寄予厚望的,一直在努力完成它的使命。

《你可能不知道的React》-- 一个普普通通的UI库

不过如果你想要体验一下这种更符合UI直觉的开发方式,可以看看集成了Suspense的数据请求库。比如SWRReact Query

⏰ 温馨提示:这些库都声明了这是实验性🧪的,所以只适合体验和探索,如果想要用到线上项目的小伙伴,除非头铁,不然还是再等等吧。

React到底是什么

通过上述两个场景,我们发现React不断地在解决CPU和IO两大瓶颈,这就是它的使命。这时候我们去看看文档中React对自己的定义:A JavaScript library for building user interfaces. 这个定位显然是”谦虚“了,按照React的使命,它甚至有点想把自己搞成一个”操作系统”的节奏。因此我单方面决定重新下个定义,从使命的角度谈一谈React到底是什么?

一个支持声明式地描述宿主环境的在不同时刻的UI,实现异步渲染的JavaScript库。

  • 声明式:通过JSX开发组件,通过数据(状态)驱动UI视图。
  • 宿主环境:React是平台无关的,是一个抽象的API库,针对不同的宿主环境如Web、Node、原生移动应用,都提供了具体实现的库。
  • 不同时刻:React是UI内存化的一种体现,由于是基于Immutable Data的,就像电影每一帧一样,不同的时刻对应不同的React渲染树,自然对应不同的画面(UI)。
  • 异步渲染:通过引入Time Slicing来解决CPU瓶颈,引入Suspense来解决IO瓶颈,从而做到自适应用户的设备和网络, 一图以概之:

《你可能不知道的React》-- 一个普普通通的UI库

写在最后 🥳

React,一个普普通通的UI库,却致力于解决用户体验的两大瓶颈,为用户的设备和网络自适应之路不断求索。

对于CPU瓶颈,从Fiber架构的转变倒React18正式面世,在Time Slicing的方案实施上,我想React给出了一张高分的答卷;

对于IO瓶颈,虽然Suspense目前只是支持CSR/SSR的动态加载组件的场景,但是相信React在未来一定会演讲中提到的坑填上的,还是很值得期待的。

彩蛋 🥚

还记得上一篇彩蛋提出的问题吗? 👈 点击展开参考答案:

问:Virtual Dom是为了更好的性能吗,它的优势是什么?

答:不是,性能不是目的,是为了更高效的开发体验,具体优势如下:

  • 在高效开发体验的同时,仍然保持一个还不错的性能,Vdom保证了性能的下限
  • 无需手动操作DOM,声明式编写UI,让的代码更加高效可靠
  • 通过Vdom(本质是js对象)将UI内存化,可以更方便的实现跨平台开发

这篇文章更加详细说明了原因,如果想要进一步了解Vdom,可以读一读。

今天的彩蛋还是提出一个问题,欢迎 👏 大家评论区积极讨论,问题如下:

如何解决React中的Race Conditions(竞态条件)问题?

温馨提示✨:

参考答案在下一篇文章彩蛋区公布哦🙈~