《你可能不知道的React》-- 一个普普通通的UI库
一个普普通通的UI库
建议时长: 🍱 20分钟
“路漫漫其修远兮,吾将上下而求索。” ——《离骚》
背景 🐣
最近重新回看了一遍JSConf Iceland 2018中 Dan Abramov的演讲,发现虽然是几年前的演讲了,但是却道出了React的使命,强烈推荐各位看官,或许对React会有新的认识。
书接上回,上篇文章提到了React在不断为开发者提供持续高效地开发UI界面的能力。有小伙伴和我说不太理解这意思,那么这一次我们就结合演讲,深入的讲解一下。
正文 🤔
React的使命
我们通过两个场景来聊一聊React的使命。
场景1
给定一个输入框,输入框变化时,搜索对应的商品列表并展示。
说明🪧:输入内容越复杂,商品列表也越复杂。
在这个场景下,我们不难发现,如果不做任何优化,低端机根本跟不上,就会造成页面卡顿掉帧,甚至“假死”,我们的输入就无法等到快速的响应。
如果我们把展示的商品列表细化成一个个的React元素,那么当元素达到一定的数量和层级,就变成了一棵巨复杂的渲染树🌲,然后我们再把渲染树变化给可视化,通过图表来反应变化就会变得很直观。
这其实就是Dan在演讲中演示1的Demo,由于没有找到原版的代码,找了好久,从codesanbox上fork了一个项目,想体验的小伙伴可以前往这个简化版的☁️ Demo玩一玩。
从上图中不难发现,对于这类场景,我们现在最常用的方案就是防抖/节流。但是这种一把梭的降级方案有个很明显的问题,因为无法做到设备自适应,使得性能好的机器也跟着遭罪,让部分用户群体丢失了原有的体验。
这便引发了React的第一个使命——解决CPU瓶颈, 那么何为CPU瓶颈呢?简单来说,就是在React看来,CPU解决的是创建React元素、渲染等同步的工作,需要解决不同性能的设备表现不一致的问题。
React给出的方案就是Time Slicing(时间分片)。关于时间分片的解释,其实就是把复杂的工作任务(大概大于50ms)拆分成一个个执行时间很短的任务去逐个执行,这种机制被称为时间分片。
之前看过一个很形象的解释就是“蚂蚁搬家”,按照这个解释,时间分片就是搬的东西(任务)不会少,根据每只蚂蚁的能力(设备性能)搬运适量的东西,而且聪明的蚂蚁优先搬运重要的东西。
场景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还是寄予厚望的,一直在努力完成它的使命。
不过如果你想要体验一下这种更符合UI直觉的开发方式,可以看看集成了Suspense的数据请求库。比如SWR 或 React 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库,却致力于解决用户体验的两大瓶颈,为用户的设备和网络自适应之路不断求索。
对于CPU瓶颈,从Fiber架构的转变倒React18正式面世,在Time Slicing的方案实施上,我想React给出了一张高分的答卷;
对于IO瓶颈,虽然Suspense目前只是支持CSR/SSR的动态加载组件的场景,但是相信React在未来一定会演讲中提到的坑填上的,还是很值得期待的。
彩蛋 🥚
还记得上一篇彩蛋提出的问题吗? 👈 点击展开参考答案:问:Virtual Dom是为了更好的性能吗,它的优势是什么?
答:不是,性能不是目的,是为了更高效的开发体验,具体优势如下:
- 在高效开发体验的同时,仍然保持一个还不错的性能,Vdom保证了性能的下限
- 无需手动操作DOM,声明式编写UI,让的代码更加高效可靠
- 通过Vdom(本质是js对象)将UI内存化,可以更方便的实现跨平台开发
这篇文章更加详细说明了原因,如果想要进一步了解Vdom,可以读一读。
今天的彩蛋还是提出一个问题,欢迎 👏 大家评论区积极讨论,问题如下:
如何解决React中的Race Conditions(竞态条件)问题?
温馨提示✨:
- useEffect + didCancel标识
- AbortController
- Suspense
参考答案在下一篇文章彩蛋区公布哦🙈~