React 十年——过去、现在和未来

2023-12-31 660 次浏览 前端

引言

不知不觉间,距离 React 发布已经过去十年。前端业界在这十年间也发生了相当多的变化。

这篇文章并非严谨和客观的历史回顾,而更多的是作为一篇非正式的技术杂谈和个人拙见分享。

过去——经典时期

在声明式 Web UI 开发的浪潮下,React 早期凭借其简洁的语法、一切皆 JS 的自由度,迅速取得了一席之地,并很快在流行度上超越了早期已经有一定基础的 AngularJS。React 自身轻量、不需要记忆太多语法,只需要触发事件、调用 setState 和轻量地封装组件即可达到看起来还不错的开发体验。尤其是它推广的 JSX 语法,在当时相当惊艳,让受够了记忆 HTML 模板语法的开发者仿佛看到了自由的灯塔。

尽管我在那个时候还是个在上中学的小屁孩,但当我后来学习了 Web 开发,用 PHP/JSP + jQuery 写过很多小项目并后来转向 React 的时候,我也被这种表达 UI 的灵活程度和便利所折服。并在后面的一段时间里成了 React 的忠实拥趸。

我习惯将 Hooks 推出前的 React 技术栈称为经典时期,以便和当前的技术栈作区分。

高速发展

在后文引出和分析 React 的影响前,让我们从过往经历中回顾为什么它可以在几年内迅速成长为前端框架中的领导者。

简明自由的设计

谈到 React 的设计,在早期的声明式启蒙时代,看到它的简短几行的语法示例后,我敢说很少有人能不对这种语法产生兴趣。如果说 jQuery 除了解决兼容性问题以外的最大功绩是 selector 语法,那么 React 与它相似,其推广的组件封装和 JSX 语法无疑是足够亮眼的,在当时流行的诸多模板引擎中,这种强大自由度足够吸睛,也在多数情况下足够好用,显著推动了组件化的进程。对比同时期的其他对手,React 有用近乎独特的设计和易用性,同时拥有 JS 主导 UI 的自由度。

让我们回看 React 经典时期的设计:以组件为基本单元定义 UI 片段,将内部状态、事件逻辑、渲染函数放在一个捆绑包(class)里。在若干年后的今天,这个组件的定义规范已经几乎成为了声明式 UI 开发的标准。你可以在大多数框架中找到这些组件化的相同要素:状态、逻辑、UI 模板的封装结构。

先发优势

React 在很早的时候便为我们带来了相对成熟的声明式开发方式,这使得第一批从命令式 UI 开发中跳脱出来的开发者受益。在早期的几年,React 始终处于足够先进的行列,其他后来者尽管很快拥有了类似甚至不输 React 的功能和体验,但时间上的优势已经让 React 得到了有效的传播,并积累了一批原始生态。

生态大爆炸

React 的成功,除了自身的设计和先发优势以外,另一个不容忽视的因素是社区生态。

React 团队十分精明的一点是:他们只负责最核心的部分,而将补充剩下 90% 功能的机会让给社区。同时还能让自己专注于输出核心理念和价值观。在一个没有最佳实践的社区里,人人都想输出自己的观点和范式。这成为了 React 早期扩展生态的一个重要策略:基于一个没有最佳实践指导且充满漏洞和话题度的新概念,实现最小模型,然后推动舆论发酵。在一个恰好处于流行的风口浪尖机遇里,一部分人热衷于贡献生态中缺失的基础部件,另一部分人热衷于输出自己对某类问题的最佳实践,并与其他的派系展开长久的论战。

通常来讲(参照多年来 Web 领域的经验),社区活跃程度及生态内项目数量与框架成熟度基本成正比,但这个经验在 React 社区并不适用,因为 React 社区存在一个繁荣表象下的致命问题:质量。其活跃的社区掩盖了实际上生态质量参差不齐的弊病,这无疑会对技术选型造成误导,并持续加剧这种失衡。举两个典型的例子:React Router 和各类状态管理库。前者为社区维护,功能质量落后于竞品的 Vue Router(其由官方维护),后者则凸显了数量不等于质量。

KPI 驱动力

一个框架是否 KPI 友好,是否能留给团队进行深度定制和基建的空间,也是团队进行技术选型时的重要参考。是否面向 KPI 并无高低对错之分,虽然看起来有点功利,但这是一个不得不承认的要素,客观上对 React 占用率提升起到了一定的推动作用,并形成了招聘-面试学习-社区热度-影响企业技术栈的正向循环。

痛点与局限性

我十分认同 React 经典时期对声明式 UI 开发思想的卓越贡献,尤其是其推广的组件化、单向数据流和 JSX。这些理念无一不深刻影响了之后声明式 UI 的发展。但在这个时期,随着大家对 React 探索逐渐完善,越来越多痛点也浮现出来。

状态管理

在当年,维持 React 社区活跃的一个重要源动力就是状态管理。看起来很多开发者并不满足于 Redux 的教义,不断尝试去做新的东西,甚至能看到一个月内就有好几个新轮子涌现的「开源盛世」。

彼时,我就已经察觉到了不对劲,这个社区开始变得奇怪。一方面是大家都在为一些没有做出真正效率提升价值的轮子比来比去;另一方面是,社区里充斥着一种近乎狂热的宗教氛围。从那时开始,网上已经可以看到一些嘲讽「前端娱乐圈」的言论,涉世未深的我只觉得这是来自后端程序员的傲慢与偏见。

回到状态管理上来,作为垄断者,Redux 看似是在推崇一种最佳实践。是的,它非常有迷惑性,很容易让新手认为这就是版本答案,盲目相信这样组织代码会对日后有无可估量的好处。但当我写过很多这种为了教条而教条的样板代码后,我并没有找到这么做带来的任何实际收益(包括神乎其神的什么时间旅行能力,我敢说并没有多少人真正需要它),反而在项目里为了加一个后端接口修改数个文件,使我不得不花了小半天时间写了一个从后端接口文档自动解析接口并生成代码和 patch 到项目的工具。

当时以 Redux 为代表的 Flux 教派的影响力是空前绝后的,它足以影响业界的其他方案,如 Vue 生态的官方状态管理方案 VueX。当然,我很庆幸能遇到真正解决一些实际痛点的方案,如 MobX,尽管很多人将其视为孽物。

状态管理的繁荣(或乱象)直到后来才慢慢趋于平静。但我从一开始搜到 Redux 的时候就产生了一个疑问,只是为了获得跨组件的响应性状态,我们是否已经本末倒置?难道这不应该是框架(库)提供的基本特性?

不可变

作为 React 经典时期几乎唯二的函数式部分(另一个是渲染函数),我对不可变数据的约定持保留意见。虽然它几乎一边倒地被「前端精英人士」鼓吹,时至今日你仍然能搜到大量的文章介绍它的理论收益。但在 React 整体面向对象、局部函数式的设计下,以普遍理性而论,类组件范式下实施不可变的必要性是存疑的。或者换个角度想想,如果你借其他类库提供的语法糖改成直接赋值,也并不会对开发造成多少影响(看看有多少「逆子」为了减轻心智负担已经这么做了)。在普通用户的视角下,不可变的约定反而成为了负担。

回过头来看,我更愿意认为这个设计是 React 为了掩盖自己底层设计的简单粗暴(必须由用户显式通知更新,而逃避实现声明式 UI 开发中最重要的变更检测和依赖追踪的责任)找到的一个美丽动听的借口,但它刚好也是 FP 的基本特性。

状态驱动陷阱

尽管 React 团队一直强调使用者应该仅把它当做视图的渲染方案,但他们并没有给出明确的最佳实践和足够的扩展性来支持。在有意无意的发展下,React 已经和状态驱动模式深度绑定——至少在绝大多数使用者的经历上是如此。最终的结果是,使用者们非常容易写出极其复杂的组件,将绝大部分逻辑耦合在视图组件中,并在 componentWillReceivePropscomponentDidUpdate 等生命周期上堆砌大量的逻辑和特殊判断,边写边骂。

造成大多数使用者陷入状态驱动泥潭的根源,正是框架的不作为。为了完成必需的逻辑,人们不得不从纷繁的 diff 中分离出多种不同情况进行处理。这就是这种模式的最大弊端之一:丢弃了状态变化的本源,然后靠残缺的线索重新构筑世界。

无尽的重渲

由于 React 的设计,即面向对象的容器和纯渲染函数组成的组件,渲染阶段成为了一段纯函数,React 希望重复执行它,以满足内心函数式的崇高目标。但多年过去了,事实已经无情地打脸了这种理想化的设计:大多数使用者们不得不手动干预渲染来防止无尽的重新渲染以优化性能,包括但不限于使用 reselect 库、memorize (computed)、shouldComponentUpdate 生命周期等。

糟糕的复用支持

在这个类组件作为唯一有状态组件的时期,React 几乎没有给出任何在复用性上的支持。是的,一切全靠语言自身的能力,导致人们只能以 HOC、类装饰器等主流方式实现最基础的组合和修改能力,或借用其他库通过强制更新等方式扩展组件能力。

官方的不作为变相让不少使用者拒绝思考,将自己局限在 React 的框框中,也让之后的 React Hooks 提供的逻辑复用能力被当做 R 氏银弹一般被吹上天。他们以为是 Hooks 拯救了他们,将其奉做真理,认为这是神级特性。而真相是:这仅仅只是实现复用和组合的多种方式之一,且功能原始的一种罢了。

事实上,如果在类组件上实现一个支持外部数据响应性和生命周期合并的简易依赖注入(只需要瞬态作用域),我们就可以拥有不逊于 Hooks 的复用能力。同时,一旦我们不小心额外加了几行代码,实现了单例作用域,那就顺便实现了状态管理功能,一脚把 Redux 踹进垃圾桶。是的,事情本就是这么简单。


彼时,React 因状态管理混乱且繁琐、生命周期和状态驱动泥潭、性能优化负担、复用困难等问题被越来越多使用者诟病——尽管不少人(包括我)仍然喜欢用 React,毕竟它带来的自由度、抽离组件的舒畅感和声明式语法足够好用,也让人变得慵懒。

我会对 React 经典时期给予一个整体中上的正面评价。因为它极大推动了声明式 UI 开发在探索阶段的发展,而且基本确定了一套基本模式:组件和单向数据流。我对 React 的积极评价基本都是现代声明式 UI 框架共同承认和吸收的部分,而并不包含其函数式部分(渲染函数和不可变)。这是因为它的扣分项大多都来源于局部的函数式设计。

现在——祸乱时期

此时,你站在 Web 前端的岔路口前,作为带领众人的巨兽,你的选择将有权改变世界。

选择左边的第一条岔路,你将选择「以人为本,面向工业」,深刻反思自己的地位和影响力,逐个解决子民们倾诉的所有问题。当然这意味着你将不得不做出一些妥协,甚至抛弃自己原初定下的规则;

选择中间的第二条岔路,你将选择「理想主义」,为了极致的探索函数式 UI 的潜力,自造一门语言,舍弃 JS 的包袱,拥抱完全的 FP、完全的理想,和完全的自己;

选择右边的第三条岔路,你将选择「裹挟」,你决定利用自己的影响力,将尽可能多的 JS 世界的人带入伪函数式的漩涡并迫使他们认同自己的意志,进而裹挟更多人加入这场疯狂的革命。尽管你比谁都清楚,一旦这场革命失败,整个业界需要比这多得多的时间重新回到正轨,自己也将背负骂名。

2018 年,React 这头巨兽,几乎没有任何犹豫地,选择了最右边的岔路。自此,一场加速的革命开始了。

React Hooks:祸乱之源

React Hooks 的发布,在整个 Web 前端乃至 UI 开发领域都是一个重要的节点。其影响范围之广、时间之久,足以留下浓墨重彩的一笔。它引发了 React 社区乃至前端业界的一次巨大分裂,我们可以找到很多剖析其设计与使用问题的文章,而我自己也在数个月前(《四年了,我为什么还是不喜欢 React Hooks?》)肤浅地斗胆评价了它,所以在这里我们不打算再去花篇幅再次批判 Hooks 的设计问题(心累),尽量快速地带过,而着重于讨论它的影响和意义。

糟糕的答卷

对比上一章列举的痛点,让我们看看 Hooks 解决了哪些:

  • 状态管理:没有足够好用,但提供了多个 API,基本使 Redux 跌落神坛
  • 糟糕的复用支持:通过函数组合提供了较为局限的复用能力
  • 不可变、状态驱动陷阱、无尽的重渲:无变化

对 Hooks 交出的答卷,我是不满意的,并且拒绝评分。它只解决了少部分痛点,但由于它的额外心智负担,某些场合下几乎抵消掉了这部分收益。对于一些之前使用深度定制的技术栈的团队,切换到 Hooks 甚至多是负收益(因为在之前这些团队往往已经自行解决了大部分痛点,且拥有严格的架构设计)。如果你有深入使用,那么一定对它的心智负担有了自己的认识,即便对一些浅浅体验过的人,Hooks 也未必丝滑,在理解它的基本运行原理前,挫败感是很强的。

但客观来说,它绝对不是一无是处的垃圾。我比较欣赏的一个亮点是,它给出了一个关于副作用如何归纳和处理的具体实践方式,虽然不完善,但确实给了一个解法,通过副作用统合生命周期和状态驱动逻辑,和状态及渲染逻辑包在一起,算是实现了一个比较亲民的函数式 UI 开发的初级形态。而不是在其经典时期将这些概念作为一个纸上空谈的标语,让社区陷入无休止的内耗。

影响和意义

如果事情简单到,React Hooks 只是个小众方案圈地自萌,那我反而会对它的大胆设计点个 star,而且这篇文章也不会出现。但实际上,它的威力在后面几年才慢慢显现出来,并开始影响到每个前端开发者,即便你根本不关心 React。

我们只需要简短的一句话概述它对外界的最大影响:推广了形式上的函数潮流和基于闭包的组件。

看起来平平无奇的一句话,对吧?但这种以函数闭包定义组件的形式,深刻地流行了起来,其中受影响最大的当属 Vue Composition API。这种思潮让前端生态上的很多其他组件和库也倾向于使用这种基于基础 ES 语法的函数 + 模块导出的形式。

在之前的文章里我已经详细剖析过为什么 Vue Composition API、Solid.js 等实质上都是基于对象的,但它们或多或少都受了 React Hooks 的组件定义形式和函数组合方式影响,以闭包形式描述组件。之前我就已经将这种,在提供了类和对象语法的高级语言里,装瘸子一般笨拙地用闭包重新模拟对象的返祖行为戏称为 COP(面向闭包编程)。

COP 相比 OOP 弊端非常多,虽然思想都是 OO 的,但它自愿舍弃了语言的 OO 特性,摒弃了语言现有的和潜在的为结构化面向对象提供的诸多特性,像是要逃避什么一般地自愿进行语法降级,自废武功,这种行为就像你从已经习惯的 C++ 退回 C 语言。诚然,使用 COP 模拟面向对象经验中的一些流行模式足够应付很多需要, 而且部分能力,如简易的组合和封装也能轻松实现,但它决定了上限:你将很难使用一些常见工具来解耦和复用逻辑,如容器管理的依赖注入、实现替换、AOP 等。并且由于闭包的不可观测性质,其内部无法被结构化观察、改写、装饰等,进而丧失了使用者上层架构的空间。

难道有人觉得闭包模拟对象和函数组合这种原始的代码组织方式,是在面向对象之后出现的新东西吗?

有的精神函数式拥护者(即不一定是真正 FP 的用户,但喜欢用 JS 的 export function 来实现一切并天然抗拒 class 之类单词的群体)可能会迫不及待地反驳。但我并非否定这种模式,也不反对 Vue 等框架的选择,这其中包含很多历史包袱和权衡。实际上,函数和类都作为 JS 的一等公民,没有什么优劣之分(但在具体使用场景上,一定有优劣之分)。我只是想要揭露一个无情的现实:JS 是函数式和面向对象两条腿走路的,两个路线都可以发展。由 React 主导的函数式浪潮,占据了天时地利人和,我们给了它很多年的时间,一厢情愿地信任它,给予它最多的资源,期望它能通过影响力,为 JS 带来更多函数式特性并取得成果。但结果是,语言并未有什么实质的函数式特性突破,反倒是 React 把自己变成了 FP 的孤儿,且潜移默化影响了大量业界其他有影响力的框架和库自我退化,回到了只需要基础语法就能运行的形态。与此同时,另外一边,面向对象作为社区里的弱势一方,缺少活跃和现象级项目去推动语言发展,甚至被一些特性拖了后腿。

在这里,我不想讨论一些诸如有影响力的框架应该独善其身还是推动业界发展之类的宏大问题。但那可以简单概括为做与不做的问题,无关对错。可是,如果客观上对业界产生了负面影响甚至带歪了路线,拖慢了 JS 在软件工业领域的发展,我觉得这种事是值得被拉出来批判的。

现在,让我们给出五年后看 React Hooks 的一点总结:它以一个怪异的面孔,强力地分化了前端社区,并深刻影响了前端业界的发展方向。它并未成功推进 JS 的函数式革命,也并未说服 FP 成为声明式 UI 开发的优秀范式,反而通过影响业界,变相阻碍了 JS 面向对象路线的发展,裹挟着整个业界走了一条弯路。

并发模式

作为现代 React 时期的为数不多的新东西,一开始我本以为是什么突破渲染线程的牛 X 技术,但得知真相的我眼泪差点掉下来。React 的并发渲染其实没有什么值得讨论和评价的必要,它和 Fiber 一样,都是 React 为了解决框架自身问题做的补丁,其他框架一般没必要借鉴。但有一个值得注意的点是,它很有可能是 Hooks 的部分设计考量。我对此表示理解。

React Server Component

作为突破传统 SSR 的一种优化能力,RSC 可以提供更细致的服务端响应片段,以此优化用户体验。

RSC 虽然适用范围较窄,但作为一种边缘优化体验的尝试,我十分认可这种探索。但局限性使得它应该较难推广。总体来说,是有一定价值的方案。

未来?

在后声明式开发时代,前期探索已基本稳定。是时候总结和展望未来了。

声明式开发:函数式 or 面向对象?

经过几年的沉淀,这个时代的主流 UI 开发模式——声明式开发已基本趋同和稳定。我们根据主要范式将主流方案分为两大阵营:面向对象阵营(Angular、Vue、Svelte、Solid.js、Swift UI、Jetpack Compose、Flutter 等)和函数式阵营(React)。

React 似乎被孤立了。但这不重要,我们可以从几个维度进行一下简单的对比。

上手难度开发效率功能性工程上限
面向对象非常低依赖框架和语言的能力。通常较容易扩展功能极高
函数式中高中等,熟练掌握后高可以满足基本需要,但不排除部分 FP 语言下的框架可以提供很强的功能扩展保留,暂未看到高上限的例子

通过简单对比,我们不难得出一个结论:面向对象方案在各个维度上,对比函数式均没有短板,且几乎各个方面都有已知的工程实践证明领先。

欸???

怎么,哪里有问题吗?

如果有问题,不妨两边各抽一个,试试能想到的常见用例写出来进行对比,我相信大多数人会得到和我一样的结论。或者还有一个我自己用过的有效的方法:无论是简单的例子,还是那种折腾了好一会采用 Hooks 写出来然后通便般神清气爽的东西,尝试用其他框架的语法或伪代码重写一遍。每次当我用这个办法思考时,我总会感到极强的挫败感。久而久之,我发现大多数时候,函数式 UI 开发的理论优雅,用面向对象方式都能更直接、更高效的实现出来。我发现:强迫自己使用函数式思维思考已经变成了一种思想钢印,当我久违地抬头,才发现外面的世界已经如此现代和壮丽。

当我还摇摆不定时,有人告诉我你这样是不对的,你应该将这一层只用于渲染,不要耦合逻辑。我也尝试了,然后得到了更加叛道的一条经验:当试图摆脱状态驱动,在外部进行业务逻辑建模时,面向对象是唯一的选择,尽管我确实可以试图让 View 层尽可能薄,但当外面的业务建模,甚至全栈系统上的其他部分都是面向对象的时候,我为什么要单独在 View 层去使用 FP?而不是统一使用同一套范式?

至此,我以这样的心路历程结束了摇摆和自我怀疑。但我并不会根据喜好全盘接受或否定某一种技术,而是俱收并蓄。比如我通常在业务逻辑和视图开发使用面向对象,而在一些流式事件处理或计算管道等场景切换到函数式,是的,它们协调的很好。我也对以 React 为首的函数式 UI 探索给予肯定,它们为大家带来了同一个问题在其他角度的解法。

是时候发表我对声明式开发中的路线之争的看法了:现阶段(2023 年),以 React 为标杆的函数式模型,探索出了一条基本完善的可用于开发 UI 的范式和实践,但对比面向对象模型,未显露明显的优势。我们可以继续期待函数式 UI 开发的研究和发展,现阶段基于面向对象模型的声明式 UI 仍是最完备的解决方案

React 的未来?

以当前趋势,除非 React 推出惊艳的改良成果,否则我对它的未来是悲观的:随着其他优秀方案兴起和完善,React 已经没有像之前那样足够多的领导技术以支撑其霸权,它将继续缓慢衰退,逐渐失去吸引力,尽管现在的社区体量和生态惯性还足够它继续霸占流量很久,但这场衰退是必然的、不可逆转的。

结语

本文通过粗浅回顾 React 十年变革,以及探讨其背后引发的行业变革和意义,拙劣地表达了对现状的思考和未来的展望。

纵观 React 的十年历程,其实也是函数式编程思想在最广阔平台高速发展的十年。我们认可它将一个晦涩的学术理念带给大众并引发思考以及对提升用户体验所做的努力。

很多人喜欢说一句话:「软件系统的复杂度不会凭空消失,只会转移」,这句话在某些角度上显然是对的。甚至引申一下,当我们不需要写庞大的汇编就能实现漂亮的 Web 程序,是因为我们站在一个又一个巨人的肩膀上,他们承担了原有的复杂度,不断抽象、不断简化重复工作,使人在更接近自然语言的层面专注创作,这才是软件发展的趋势。所以,声明式 UI 为什么能在大多数场景取代命令式 UI 甚至立即模式 GUI?因为它进一步为开发者降低了复杂度、提升了效率。而所谓框架之争、路线之争什么的只是表层,只有始终遵循发展规律的一方才能不被时代淘汰。

最后,请允许我送一句话给我最喜欢的年度 React 生态贡献者——Next.js 团队:如果最近真那么闲,建议研究量子力学。

参考资料


bLue 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。
本文地址:https://dreamer.blue/blog/post/2023/12/31/react-ten-years.dream

还不快抢沙发

添加新评论