软件的设计是为了服务理念。只有懂了设计理念,才能明白为了实现这样的理念需要如何架构。所以,在我们深入源码架构之前,先来聊聊React
理念。
# React理念
我们可以从官网看到React
的理念:
我们认为,React 是用 JavaScript 构建快速响应的大型 Web 应用程序的首选方式。它在 Facebook 和 Instagram 上表现优秀。
那么该如何理解快速响应?可以从两个角度来看:
- 速度快
- 响应自然
React
是如何实现这两点的呢?
# 理解“速度快”
每当聊到一款前端框架,拉出来比比渲染速度成了老生常谈。
这里提供了各种框架渲染速度的对比
我们经常用“前端三大框架”指React
、Vue
和Angular
。相比于使用模版语言的Vue
、Angular
,使用原生js(JSX
仅仅是js的语法糖)开发UI的React
在语法层面有更多灵活性。
然而,高灵活性意味着高不确定性。考虑如下Vue
模版语句:
<template>
<ul>
<li>0</li>
<li>{{ name }}</li>
<li>2</li>
<li>3</li>
</ul>
</template>
当编译时,由于模版语法的约束,Vue
可以明确知道在li
中,只有name
是变量,这可以提供一些优化线索。
而在React
中,以上代码可以写成如下JSX
:
function App({name}) {
const children = [];
for (let i = 0; i < 4; i++) {
children.push(<li>{i === 1 ? name : i}</li>)
}
return <ul>{children}</ul>
}
由于语法的灵活,在编译时无法区分可能变化的部分。所以在运行时,React
需要遍历每个li
,判断其数据是否更新。
基于以上原因,相比于Vue
、Angular
,缺少编译时优化手段的React
为了速度快需要在运行时做出更多努力。
比如
- 使用
PureComponent
或React.memo
构建组件 - 使用
shouldComponentUpdate
生命周期钩子 - 渲染列表时使用
key
- 使用
useCallback
和useMemo
缓存函数和变量
由开发者来显式的告诉React
哪些组件不需要重复计算、可以复用。
在后面源码的学习中,我们会看到这些优化手段是如何起作用的。比如经过优化后,React
会通过bailoutOnAlreadyFinishedWork方法跳过一些本次更新不需要处理的任务。
# 理解“响应自然”
该如何理解“响应自然”?React给出的答案是将人机交互研究的结果整合到真实的 UI 中。
设想以下场景:
有一个地址搜索框,在输入字符时会实时显示地址匹配结果。
当用户输入过快时可能输入变得不是那么流畅。这是由于下拉列表的更新会阻塞线程。我们一般是通过debounce
或 throttle
来减少输入内容时触发回调的次数来解决这个问题。
但这只是治标不治本。只要组件的更新操作是同步的,那么当更新开始直到渲染完毕前,组件中总会有一定数量的工作占用线程,浏览器没有空闲时间绘制UI,造成卡顿。
React核心团队成员Dan在介绍React为什么会异步(Concurrent Mode)更新组件时说:
让我们从“响应自然”的角度考虑:当输入字符时,用户是否在意下拉框能在一瞬间就更新?
事实是:并不在意。
如果我们能稍稍延迟下拉框更新的时间,为浏览器留出时间渲染UI,让输入不卡顿。这样的体验是更自然的。
为了实现这个目标,需要将同步的更新变为可中断的异步更新。
在浏览器每一帧的时间中,预留一些时间给JS线程,React
利用这部分时间更新组件(可以看到,在源码中,预留的初始时间是5ms)。
当预留的时间不够用时,React
将线程控制权交还给浏览器使其有时间渲染UI,React
则等待下一帧时间到来继续被中断的工作。
可以从Demo看到,当牺牲了列表的更新速度,React
大幅提高了输入响应速度,使交互更自然。
# 总结
通过以上内容,我们可以看到,React
为了践行“构建快速响应的大型 Web 应用程序”理念做出的努力。
这其中有些优化手段可以在现有架构上增加,而有些(如:异步可中断更新)只能重构整个架构实现。
最后再让我们看看,Dan回答网友关于React
发展方向的提问:
相比于新增feature,React
更在意底层抽象的表现力。结合理念,相信你已经明白这意味着什么了。