打破框架的范式之争

979 阅读9分钟
原文链接: zhuanlan.zhihu.com

在2019年9月17日举行的GitHub中国首场官方见面会上,我做了《打破框架范式之争》的主题分享,本文是其文字稿。正文开始——

在9月初左右,一个 React Meetup 邀请了 Svelte 框架的作者 Rich Harris 去做一个技术分享。

他的内容主旨如下所示:

在 React 的场子里,说 Svelte 比 React 更好。

相当于在 GitHub 的场子里,说开源不是一个好的选择。

哈哈,当然,他是开玩笑的。

实际上,他是站在自己的视角,指出他认为 React 里存在的问题。

可以访问 Rich Harris 的 Slides,阅读其中备注部分尤为重要。

我总结了一下,Rich 认为 React 中的核心问题是:

在 Web 里面,不管是 DOM,BOM,CSS ,JavaScript 还是 WebGL,都充满了内部状态和命令式风格的 API,跟 React 函数式风格的抽象方式相差甚远。

而 React Team 的成员们,还在致力于不断地扩大两者之间的鸿沟。

其结果就是,尽管 React Hooks 是一项具有开创性的优秀技术;然而,绝大部分 React 开发者,都难以理解和习惯 React Hooks 的一些机制。

(Rich 给了一个有问题的 React Hooks 例子,在场的 100 多名 React 开发者中,大约只有 4 人了解问题所在)。

在这种情况下,React Team 依然不认为这是 React 的问题,不试图解决困扰开发者们的反直觉事物。他们反而认为,需要教育 React 开发者,逐渐适应 React 的模式和思想,建立新的直觉。

并且,React Team 对于过去 4 年多的教育成果感到满意:大部分 React 开发者都不会在 render 函数里操作 DOM 了,甚至有些开发者,已经忘记 DOM API 怎么用。

React Team 对未来新的教育,也抱有信心。

Rich 对此不以为然,他在分享的最后指出,Svelte 框架与 React 不同,它致力于以符合人脑思维习惯的方式去构建 UI。

我们暂不评价 Rich 的观点,且看看其它开源作者们的反应。

Jordan 是 React 的原作者。他在推特中说道,如果说 DOM 是有状态/命令式的,所以我们就要拥抱它。那么,计算机也是。

他的吐槽比较隐晦。我做一个解读,未必准确,大家姑且一听。

函数式编程的根基 Lambda Calculus 是一个跟 Turing machine 计算能力等价的通用计算模型。

而当前主流的计算机,是建立在 Turing machine 和冯诺依曼架构上的设备。其中充满了可变的状态和命令式的指令集。

所有编写的函数式代码,最终都是通过非函数式的方式,运行在我们的电脑上。

那么,如果因为底层是有状态的,命令式的,就有了充分的理由去拥抱它。那函数式编程多年的发展,不可能存在。

更甚的是,我们生活的宇宙本身,也是充满状态和命令式的行为。我们有什么理由去发展纯粹的数学,发展这种远离客观实际的事物呢?

Jordan 本人已经不再直接参与 React 的核心开发。当前 React Team 在技术上的主导者,应该是 Sebastian,React-Hooks 正是他的灵感产物。他写了一整篇文章,解释为什么 React 要那样做。

在文中,他总结了两类 Reactivity 的处理方式。

一种是以 Vue 为代表的 mutable + change tracking。即可变的数据结构,配合变更追踪,触发更新函数。

另一种是以 React 为代表的 immutability + referential equality testing。即不可变的数据结构,配合反复执行的渲染函数,以及在函数执行过程中,通过数据的引用相等性判断,找出变更部分,只应用变化的部分到 UI 上。

Sebastian 文章开篇就表示,两种实践方式都有各自的权衡。因为 mutable 的方式,已经有其它框架们很好的研究,所以 React Team 决定专注于挖掘 immutble 的方向,看看最后能引领我们走到哪一步。

Vue 作者尤雨溪发的一条推特里,给出了 Vue, React 和 Svelte 三个框架在 Reactivity 机制上各自的权衡和优缺点对比。

Reactivity with different tradeoffs

这条推特广受好评,感兴趣的同学,可以访问上面的链接。

至此,我们可以整理出框架的范式之争的要点所在:

对于框架的作者们来说,范式差异,只是权衡的不同。

然而,对于框架的使用者来说,范式差异,可能是信仰的不同。

这种争议不是第一次发生,早就由来已久。我们可以听听曾经社区里一些特别的声音。

上图引号内的所有内容,皆为原文引用。不知道为什么会有开发者,打着最佳实践的名义,急于划清界线。

仿佛回到了 20 世纪初,认为物理学大厦已经落成,所剩只是一些修饰工作。所有重大问题都已经被解决,不必再耍花样的寻找新的联结点,只要站队、坚定的站队即可。因此,用了 A 范式,就得排斥 B 范式。

不过,正如当年经典物理学的上空,还有两朵乌云一样。

如果没有被偏见遮蔽双眼,其实我们可以很容易发现,在 React 和 Vue 里,已经存在多范式之间的联结点。

从 Vue v2.0 版本开始,就有一个内置的 virtual-dom,通过 functional-ui 的方式,实现跨端渲染。也支持跟 react 类似的 render 方法和 jsx 语法。

Vue 作者,自身始终保持着开放和海纳百川的心态,吸收其他框架中对 Vue 有增益或启发的部分。mutable 只是 Vue 的显著特征,而非唯一目标。

他不会因为 virtual-dom 属于 functional-ui 而不使用它。他没有因为 react-hooks 属于 functional programming 风格而排斥这种思路,而是设法化用在 Vue 3.0 中,形成结合了 Vue 特色的 Vue composition API。

此外,在《揭秘Vue-3.0最具潜力的API》文章中,我演示了,如何基于 Vue 3.x API 实现 React 风格的 immutability + referential equality testing,验证了 vue 中 mutable -> immutable 两种范式之间衔接的可行性。

React 里也有 mutable 的部分,比如最直观的 useRef。它不但是 mutable 的,甚至是 React 亲自进行 mutate。

如上图所示,useRef(null) 初始值是 null,在渲染到 DOM 之后,React 亲手以 mutable 的方式,将 div 元素赋值给 ref.current,因此我们才得以在 useEffect 内部访问 div 元素。

useEffect 函数更是明确表明,可以在内部执行命令式风格的 side-effects 副作用代码。

在事件处理函数里,我们也可以执行副作用代码。

React 自始至终,都只是致力于让 render 函数/function-component 尽可能没有副作用和可变数据。而不是排斥一切 mutable 和 side-effects。

React 所做的是,延后和隔离副作用,以便更好的管理。

即便是最纯正的函数式编程语言,也要处理 side-effects。早在 1989 年,他们就找到了一个高效和安全的方式,即可以处理副作用,又可以保持代码的纯函数特征。

On the Expressiveness of Purely Functional IO Systems

感兴趣的同学,可以点击查看上述 Paper。可以看到,React 目前的做法,与 30 年前的 Paper 里的 Dialogue 模式如出一辙(详细内容,在本文中不便展开,按下不表)。

有趣的是,React Team 里的几个成员,如 redux 作者 dan 等,出生于 1992 年。在上述 Paper 发表时,他们和 JavaScript 都还未出生。而 Paper 里的思想,对 30 年后的 Web 框架设计,仍有巨大启发。

如上,我们看到,不管是 Vue 还是 React,已然包含多种范式,只是各有侧重。

刻意的拔高框架侧重部分,上升为信仰,很容易产生【证实偏见】。即只关注到了符合自己设想的信息,而忽略了不符合的部分。比如:

虽然 Sebastian 强调了 React 走 immutable 路线,这不意味着他否定 mutable 对于 React 的正面作用。

他在一条推特中表示,Mobx 这种 mutable/reactive 风格,跟 React 路线不一致;但 immer 这个通过 mutable 的方式,产生 immutable-data 的库,跟 React 路线完全匹配。

可见 Sebastian 本人,对于寻找 mutable 和 immutable 两种范式的衔接点,持开放和肯定态度。

Mobx 作者甚至不完全同意 Mobx 跟 React 的 immutable 路线有冲突的看法。

他在一条推特中表达了他在思考 Mobx 风格和 immutable 风格的衔接点。

打破框架的范式之争,其实是改变思路。从思考不同范式之间的竞争关系,转变成思考多个范式之间的协同关系。

受到 immer 和 vue 的启发,我也做了一个开源项目,尝试整合 mutable, immutable 和 reactive 三种模式。

如上所示,bistate 像 immer 那样,可以在 immutable 和 mutable 两种操作状态中切换。在 React 要求 immutable 的阶段,它是 immutable 的;在 React 支持 mutable 的阶段,它通过 useMutate 切换为 mutable 的。

除此之外,它又像 Vue 那样是 reactive 的,父节点的监听器,可以响应对子节点的修改。

immutable, mutable 和 reactive 三种模式,分别在它们适用的地方,各自发挥作用。

我们既得到了 immutable-data 带来的程序执行上的可预测性,也得到了 mutable-data 带来的代码书写上的便利性,和 reactive-data 带来的状态管理上的简洁性。

可以点击《响应式 React Hooks 状态管理库——Bistate 介绍》了解更多。

总结

面对框架之间的范式之争,我们需要主动接收正面的、反面的,各种不同的信息,减少信息极化。

我们需要了解 JavaScript 是一个多范式的语言。而不同的范式,有各自适用的场景。

就算框架作者们强调了自己关注的范式和路线,不代表他们否定其它范式。

我们可以通过编译技术,和语言的元编程特性(比如 Proxy/Reflect),让我们的代码得到新的语义和行为,构建不同范式之间的衔接点。

保持开放和包容的心态。

打破框架的范式之争,实际上,是打破我们心中的偏见。