Dan Abramov 访谈实录
译者注:本译文是在「在线对话 React.js 核心开发者」一个半小时直播的基础上进行的原文翻译,包括了直播中的所有问答内容,尽可能保留了 Dan 回答的中心语义,部分表达为了语句通顺、方便理解,适当使用了成语或短语替代。文章超过 1.5w 字,文中包含了大量的专业概念与启发性思想,通读大约需要 30 分钟。建议先收藏下来,慢慢阅读。最后祝大家都能够有所收获!
主持人:Hi Dan,来跟观看直播的小伙伴们打个招呼吧!
Dan:嗨,大家好!
主持人:现场大多数人都已经认识你了,可不可以再介绍你一下你自己?
Dan:谢邀,我是 Dan,是 React 的核心维护者之一,过去 5 年一直在 React Team 工作。这差不多就是我的自我介绍了。
12 岁开始编程
主持人:我从你的博客中看到,你从 12 岁就开始编程了,非常的厉害,你是怎样对编程产生兴趣的呢?
Dan:应该说是机缘巧合吧,我其实并不是自己主动去学习编程的。当时在学校做作业的时候,我很喜欢做一些展示。我不知道大家现在还用不用 PPT ,当时我用的是 PowerPoint 2003
,PPT 中内置了一种编程语言。如果你点击右键打开菜单,可以完成一些宏录制的操作。这些操作会产生很多小的程序,我起初不知道它们是什么,但是我非常的感兴趣,之后我就买了一些书,从此开始了编程的学习。
React 状态管理工具
主持人:这是你第一次和中国的工程师见面,我们这边准备了很多问题,你准备好回答热心网友们的提问了吗?
Dan:我准备好了。
主持人:你对 React 状态管理工具怎么看的?
Dan:我想不同的人可能会选用不同的库,如果要我来做推荐的话,我可能不会去推荐任何一个特定的库。真正重要的问题不是选择什么状态管理库,而是理解「状态」的类型,理解「状态」是什么。因为「状态」是一个比较笼统的、比较大的概念,我认为我们不应该去浮于表面地去讨论哪个库更好,而是去看你要处理的状态是什么种类,使用这些库的目的是什么,选用不同方案带来的差异是什么。比如说,有一些 UI 状态,针对的是显示的 UI 组件。针对文本输入框来说,文本内容有哪些,文本是否被选中,是否处于 focus / hover 状态,或者是记录当前页面被选中的 Tab 之类的,这些都是 UI 的状态。另一种状态更像是 cache,缓存从服务器返回的数据,它可能更像是某种草稿。所以我们需要结合实际的需求去选择状态管理工具。对于 UI 状态来说,我个人不推荐使用任何库,使用 React 自带的状态就够了。可以使用 React State 或者是 React Context,通过组件树将状态传递下去。对于数据缓存的场景来说,我并不推荐使用 “状态管理” (此处 Dan 用手比了个引号 ✌️✌️)相关的解决方案。我更建议你使用一些专门用来处理数据缓存的库,比如 React Query
,Apollo
和 React Relay
。
主持人:现在很多人都想用 React Hooks 和 Context 去代替 Redux
,你刚才的回答也提到了类似的点,你可不可以分享一些 React Hooks 背后的设计理念?
Dan:现在 React Hooks
只能被用于函数组件,我们也没有计划让它们兼容 class 组件,因为这是两种不同的范式,把它们混淆在一起并不合理。
我认为,这更像是一个关于 Hooks 设计理念的问题,我不确定应该怎么回答。总的来说,Hooks 某种程度上反映了我们是如何看待 React Component 的。从概念上讲,Component 就是一个函数,它接收一些 props 属性,然后返回 UI。Hooks 是对这个函数的一个增强,组件树上有了状态的一席之地,你可以用 Hooks 去记录状态,或是去记录行为等等。这就是一个大致的思路。顺着这个思路走下去,React 可能不像一个库,你可以把他想象成是 UI 的编程语言。在编程语言中,我们有函数、变量这些概念。如果把 React 看作是一门 “伪” 编程语言的话,Component 就对应编程语言中的函数,而 Hooks 就对应变量。这是我自己的想法,不知其他同学能不能 get 到。
快速上手 React
主持人:很多开发者认为 React 的入门非常的难,所以有没有推荐新手快速上手的方法?
Dan:我认为这取决于大家为什么觉得入门很难,大家遇到的问题可能不尽相同。其中一点是,React 需要你有 JavaScript 编程基础。如果你是第一次接触编程的话确实会比较困难,因为它不是使用 HTML 和模板进行网站搭建的。对于有些库和框架,你从模板开始,在模板上添加一些条件判断或循环,通过不断的小修小补,你就逐渐学会了如何进行编程。但在 React 这里,你是从编程语言开始,一上来就要写代码。如果你不会编程,React 的学习曲线就比较陡峭。有些人刚接触 React 觉得很难上手,等过一段时间学会了 JavaScript ,再回来看 React 就不那么困难了,这就是 JavaScript 带来的门槛。另一方面,我觉得有个好的开发环境很重要。比如 Next.js
或是 create-react-app
这类工具会帮你把项目开发所需的配置都准备好。有时候,人们觉得入门难只是因为搞不懂如何才能创建一个单页应用(SPA)。这些并不是 React 的问题,但人们往往会将它们归咎到 React 身上。如果你把 Next.js
作为项目起点的话,我觉得会是一个不错的开始。
React 避坑
主持人:下一个问题。对于已经开始用 React 的人来说,可不可以给一些建议,帮助大家避免一些坑?
Dan:有一点很重要,如果你使用 React Hooks,那么通常都要使用配套的 lint 规则。我们对 Hooks 有两个推荐的规则,可以使用插件的方式集成到你的项目里。此外,我觉得理解 React 中的两个概念很重要。其一是 不变性(immutability ),你需要知道如何做到更新 (update) 状态,而不改变(mutate)它。这一点你可以用 扩展操作符(...)或是其他语法实现。这里推荐一个很棒的库叫 immer,它可以让你在编程中使用 Vue / Svelte 风格的变更操作,但同时又能够保持不变性。如果你对这点很苦恼的话不妨试一试。另一点是你要理解 React 的渲染流程应当是纯粹的(Rendering is supposed to be pure),当组件进行渲染的时候,它就是在计算下一个 UI 应该长什么样子,你不应该在渲染流程中掺杂其他的操作。我认为理解 UI 是计算结果 (UI is a calculation)这个范式是非常重要的。
函数式编程
主持人:目前 React 越来越靠近函数式编程(functional programming)了,但有人说 JavaScript 并不是一个注重函数式编程的语言,你如何看待这个矛盾?
Dan:可能 React 确实比其他 UI 库更偏向函数式一些,但我不认为它是一个面向函数式编程的框架。人们在这方面其实有个共识,那就是所有喜欢函数式编程的人都不觉得 React 是函数式的,因为它不够“纯粹”或是一些其他的原因。对于 React,我更喜欢「functional-lite / functional-light programming」这个概念。React 从函数式编程中借鉴的一个很重要的思想就是将复杂的事物分解成几个可以组合的函数,像是尽可能地去使用不变性(immutability)。但 React 的代码看上去并不像传统的函数式编程代码,因为 React 倾向于使用更加直接的编码风格。举个例子,如果你需要一个循环,你可以直接在代码中写循环,而不需要写更高阶的函数,然后再用复杂的方式将他们组合起来。写 React 的方法就和写 JavaScript 一样,所以代码看上去也不像是 Ramda 或者 Lodash FP 那样的「函数式」。所以 React 应该算是处于一种中间地带吧。它既集成了函数式编程的想法与概念,又能让你在大多数的时候使用 JavaScript 主流的编程方式去进行编码,我觉得这应该不算是矛盾吧。
前端发展太快?
主持人:目前的前端是一个正在飞速发展的领域,你是如何不断提高自己,跟随技术进步的脚步的呢?
Dan:很有趣的问题,其实我觉得前端的发展并没有那么快。我这些年没有看到太多的新的东西,有可能是我自己看的不够仔细吧。不过我看到的大多数成果都是已有功能的迭代与完善。有些东西每一年或者两年发布个新的版本,但是大多数情况下不管是新的发行版还是新的库,用的思想还是原来的思想。所以,如果你对已经存在的东西足够熟悉、理解足够深的话,你可能就不会对新出现的东西感到新奇了,因为它们某种程度上讲是同质化的东西。所以我觉得准备迎接新事物的最好方法就是去熟悉已有的东西,当你足够熟悉之后,你看到就只有相似性了。
主持人:React 的未来发展趋势是怎样的?
Dan:这是个大问题啊,像是未来十年的路线图之类的,我们现在正在做的很多功能其实都与这个问题相关。
主要问题
主持人:那么目前最主要的 Issue 有哪些呢?
Dan:这一点很有意思,我们其实在面对很多不同的问题,也有不同的 team 在用不同的方式去解决它们。我可以给你举几个例子,比如说数据获取(data-fetching)。如何设计一个具有良好扩展性的数据获取方案,使得组件的数量不会引起请求 Waterfall,从而保证 UI 的一致性。你可以将逻辑写在离被调用位置更近的地方,这样数据和代码就能实现并行加载。data-fetching 是一个相当复杂的问题,你还要考虑到如何让用户用着方便,现在的 data-fetching 其实还是挺恼人的。这是当下要解决的一个大问题,另一个大的主题是各种的优化,关于代码分割(code-splitting),服务器端渲染(Server Side Rendering),如何将多余的东西从 JavaScript 的打包中剔除出去,如何让 Hydration 的花销更小等等。这可能听上去像是一个完全不同的领域,但是类似的点还有很多,通过这些优化我们可以让 React 从本质上优于第三方库和其他解决方案。另一个大的主题是关于动画(animation)的,但我们还没有开始这个方向的相关工作。但对于上述的所有方向,我们都会尝试与以往不同的方案去解决。很多 UI 框架或者库都倾向于去独立应对单点的问题,针对单个问题给出不同的解决方案,通过提供某些便利的 API 实现更优的开发体验等等。但我们在我们看来,有些问题是彼此关联的。我们会尝试使用更理论、更系统的方式串联起相关联的问题,然后解决它们。所以如果要我讲 React 未来的样子的话,我希望它能够成为一个工具,一个帮助我写组件的工具,里面集成了 data-fetching,代码分割,动画渲染等等所有功能,而且这些功能都无缝地组合在一起。因为 React 的中心思想就是像乐高一样,将各部分功能组合起来,我们希望在未来能够支持这些功能。
Concurrent Mode
主持人:有些朋友们问到了 concurrent mode 的相关问题。React 的 concurrent mode 自 16 版本就已经提出了,但到了 React 18 才终于发布了出来。React Team 在设计这个功能的时候遇到了哪些问题和挑战,你们对于这个功能在将来又有哪些计划呢?
Dan:好的,我首先要说明一下,React 18 还没有正式发布,我们目前只为库的维护者们发布了一个 alpha 版本。如果你访问 reactjs.org/blog,你会看到一篇名为《The Plan for React 18》的文章。如果朋友们想去了解 React 18 的新功能或者发布的时间计划可以去看一下。我们还建立了一个工作小组,并邀请了 50 名左右的社区开发者来协助我们完成一些 React 18 发布的相关工作,比如确保整个 React 生态中的库都能兼容新的版本。相关信息都在 GitHub Discussion 里面,非组员虽然不能评论,但是可以阅读。你可以从中找到很多关于发版的信息,其中就包括你问的 concurrent mode 的信息。正如你所说,我们在很多年前就声明了要做真正的 concurrent mode,不过这期间有一个很大的策略上的变化,那就是它不再作为一个单独的模式(mode) 来呈现。所以我们最终添加到 React 中的可能并不是一个 concurrent mode,而是一种并发渲染的机制。本质上来说,并发意味着 React 有能力在同一时间更新多项状态。比如你在更新状态的时候,有些获取数据的状态更新会花费比较长的时间,渲染一个复杂页面的时间成本就会很高。如果使用了并发渲染,这些场景下 React 就不会阻塞住浏览器。这意味着页面在进行长时间的状态更新的同时,你还可以去输入文字,后台的状态更新不会阻塞你的交互。基本上,你可以把并发渲染想象成是在后台跑 setState()
。这确实是我们一直想在 React 中实现的功能,但重要的是它不再是一个单独的模式了。当你需要使用并发能力的时候,我们提供了一个叫 Transition 的功能,你可以用 Transition 包裹 setState()
的操作,这样在调用 API 的时候 React 就会进行并发渲染。所以这个在将来会是一个可以随时调用的特性,而不是作为一个 Web 应用的全局的模式,他只针对于你包裹的操作生效。就挑战而言,我们确实遇到了不少挑战。因为我们是从理论出发的,我们得出了很多理论性的想法,比如我们认为「并发是好的」,「长时间阻塞浏览器是不合理的」,「渲染应该是可以被打断的」等等。然后我们基于这些想法着手去做,在 2015 年完成了一个原型,之后又重写了 React 来让它能够真正实现这些想法,这就是 React 16 版本。之后我们确实花费了几年的时间来思考如何能够将这些功能应用到实际生产中。全新的 Facebook 官网 facebook.com 就使用了 React 的并发特性。诸如 「渲染应该是可中断的」,「setState()
应该是并发的」以及「渲染应该在后台完成」这些小小的想法实际上带来了很多的后果,这也是我们花了一段时间才意识到的。这些特性带来了更好的服务器端渲染(server-rendering),更好的代码分割 (code-splitting) 和更好的数据获取(data-fetching),在将来我们还会用相同的思路去实现更好的动画效果。这个想法在很多地方都得到了体现,因为它本质上为 React 提供了让渲染能够「异步」的能力,这个世界上有很多东西天生就是异步的,比如我上面提到的那些功能。梳理这些功能,搞清楚怎么让他们一起运作起来确实花了我们不少时间。但我们现在基本上都已经搞定了,这些特性都会集成到 React 18 里面。所以,我再一次建议感兴趣的朋友去 React 官网的博客查看 React 18 的发布计划,点击相关的讨论区查看相关的细节。
体验 React 18 Alpha
主持人:所以听众们现在就可以使用 React 18 Alpha 版来体验相关功能了对吗?
Dan:是的,但有两点需要注意。其一是要去博客看一下新版本的使用说明。比如项目升级了版本之后可能需要将 ReactDom.render()
方法替换成 ReactDom.createRoot()
方法,诸如此类会有一些新的 API 进来,如果你不做替换的话,程序可能会报 warning,但这就是你开始使用新特性的方式。另一点是 strict mode
的用户们需要注意的,新版的 strict mode
会更加严格,这就意味着原有的东西在新版本下可能会 break 掉。所以如果你的程序在新版 React 中完全跑不起来了,可以尝试移除 strict mode
,可能就是这个原因导致的。还有一点需要记住的是,alpha 版 React 主要是为库的开发者们提供的版本,有很多库,比如 Redux 等,本身还没有更新支持。所以如果你的项目依赖了大量的第三方库,那你的项目在更新 React 18 后很可能会崩溃,但这就这个发行版的目的,让库的作者有充足的时间去做升级。所以,如果你发现你的项目在新版 React 中完全运行不了也不要灰心,尤其是那些因为引入第三方库导致程序崩溃的用户们,等到 React 稳定版发布的时候,大多数流行的库应该都已经修复了问题并且做到了兼容。
React Lane
主持人:能简单介绍下 React Lane 是怎么设计的吗?
Dan:这是一个相当技术性的问题。我们其实不希望别人知道有 React Lane 这个东西,因为它不是一个公开的 API,而是一个涉及到 React 内部实现的东西。但我可以简要说明一下它是什么。正如我上面说的,我们为 React 添加了并发的功能,这意味着任意时刻都有可能有多个状态在同步更新。并发对于我们的 Transition 特性尤为重要。比如说你在输入框进行输入,回车后页面更新,这是一个常规的页面更新操作。但如果你想要在输入的同时实现自动补全,或者想要从网络上获取候选项列表需要连接后端,从前的做法是你使用 useEffect
自己去管理这些异步渲染的流程。但现在我们会在 React 内置这套流程,你可以用同步的代码写法来处理异步的数据操作。我一直认为 UI 是一个瞬间的状态(state in time),或者说瞬间的时刻(moment in time),所以要表达这一点,你应该为列表的结果创建一个 Transition,这样浏览器请求可能会花费一些时间,但是等数据回来的时候 React 会负责正确地去展示结果。Lanes 是一个 React 内部的机制,用来标记 React 当前正在处理哪项状态的更新。你可以想象当你每次调用 setState()
的时候,React 会把他加到一个 bitmask 中,类似于很多的开关(switch)或者是复选框(checkbox)。有点像是任务列表,如果你用过任务列表你就知道任务有高优先级和低优先级。或者是 Github 上的 Label,Label 可以用来标记是高优先级,也可以用来标记 bug,feature,discussion。同样对于 setState()
来说,React 也提供了一组标签,来标记它是不是 Transition,是否是一个紧急更新等等,这个标签就是 Lane。当 React 进行渲染的时候,它会根据 Lane 来选择哪些状态更新需要被执行。如果有一个紧急的状态更新,需要一次紧急的页面重新渲染,这时候就只会执行在 Urgent Lane 下的状态更新。之后如果所有紧急的更新操作都完成了,我再去检查 Transition Lane 中的状态更新,如果这个时候从网络上返回了数据,那就把它渲染出来,比如去加载自动补全的候选项列表。所以说 Lane 是 React 的一个内部机制,使用 bitmask 来将状态更新与优先级关联起来,之后 React 再根据它去更新状态。
React Server Component
主持人:Server Component 的 RFC 草案去年年底发布了,请问 Server Component 的主要目的是什么。
Dan:是的,我们去年 12 月发布了一个 Server Component 的技术预览,但它还处于比较早期的阶段。这应该算是一个还处在研究阶段的特性,与 React 18 相比更偏实验性,也不会包含在 React 18 的特性之中,可能会在其之后才发布。但广义上来说,它与 SSR 并不一样,这也是人们常常混淆的点。最大的区别是,Server Component 只运行在服务器上,它不会被下载下来。你可以把它们想象成某种 API,以往的客户端应用可能会请求 JSON API 来获取数据, Server Component 与之类似,但是 API 换成了在服务器端运行的组件。这样做的优势是你不需要去下载任何代码,可以达成性能上的优化。另一个优势是,因为这些组件运行在服务器端,它们可以直接与数据库、微服务、或者其他任何在这台服务器上的资源进行通信。你不需要把这些资源暴露给客户端,就把它们留在服务器上就好。
主持人:有些用户已经尝试过使用 Server Component 了,所以当我们需要在项目中使用服务器组件的时候,我们需要维护三个组件而不是一个,这带来了额外的复杂性不是吗?
Dan:好的,对于这个问题,我个人其实不太认同这种说法。我不想把这个问题定义为我们需要维护三个组件而非一个。我觉得更准确的说法是,在传统的 React 中,我们只有一种类型的组件;而在 Server Component 中,我们把它们分成了 Server Component,Client Component 和 Shared Component。这个问题就好比你原本只有一部手机,现在你既有手机、又有手表、又有电视,但这并不意味着你要同时去用这三个东西,你只是有了更多的选择。所以对于这个问题而言,不是说你的组件在将来都会以三种形态存在,而是你现在只能用 Client Component 的组件。如果只用 Client Component 能够满足需求的话固然很好,但是当这个 Server Component 这个特性落地的时候,你会有更多的选择来做相同的事。到那个时候,如果你想要使用 React 写博客,想要读取文件系统,这些操作不需要很多复杂的框架也能做到。如果你只是想要使用 PHP 或者 Rails 类似的传统 web 编程风格去做一些数据库的读取操作,Server Component 也能帮你做到。在大多数场景下,我们预期用户们会通过框架来使用 Server Component 功能,比如使用 Next.js
。所以幸运的话,你并不需要想太多,也不需要创建什么 API,你只需要在文件名中加上 .server.js,这个文件就可以在服务器端运行,你就可以使用所有 Server Component 的功能了。
主持人:所以我们可以直接通过一些框架来使用这个功能?
Dan:是的,我觉得大部分情况应该都是如此。因为框架就像是整个项目的基础设施一样,会为你连接并组织好项目的各个部分。虽然你也可以去手动配置,但是肯定不如使用框架来的方便。特别是框架还可以实现无配置的启用这些功能,如果你要自己动手,你就得自己把项目的各个部分串联起来。目前我们还没有针对如何使用 Server Component 给出使用的建议,但是鉴于 Next.js
已经支持了 Server Component,如果你想要部署自己的 Server Component,你可以去看看他们是怎么实现的,然后复制他们的做法。又或者你可以直接使用 Next.js
或者其他类似的框架。
React 与 Vue
主持人:人们常把 React 与 Vue 作比较。你个人是如何比较这二者的呢?希望能从设计、性能和使用目的来谈一谈。
Dan:首先我个人没有使用过 Vue,所以我可能做不了特别详细的比较。从技术上来讲,两者可能采取了不同的实现方式吧。Vue 建立在 mutability 的基础上,可以直接去修改 state。这样做带来了一些好处,它写起来更简单,你可以用 mutative style 去写代码,这种写码方式也受到了许多开发者的喜爱。但它也有一些缺陷,比如站在更高的层次来看,有些功能可能是出了名的难以实现,或者根本不可能实现。比如我们正在做的 Transition 功能,又或者是我们想要做的新的动画特性。二者之间的差异太大,就像被一道深深的技术鸿沟隔开了,Vue 在不断探索我们可以用 mutability 做什么,而 React 在不断探索 immutability 能够做到的事。但我们对自己的方向非常有信心,因为我们的背后有近 50 年的函数式编程的研究成果,所以我们明白虽然在落实到实践中可能会遇到一些困难,但是从理论分析的角度我们的方法还是合理的。Vue 和 React 只是在探索两个不同的方向,这件事很好,你只需选择自己喜欢的那个就好。谈到设计的话,我觉得二者最大的区别是 Vue 更倾向于去做一些妥协与折衷,也更关注能不能去解决实际的问题。比如它会提供一些人们呼声很高的功能,包括更加便利的动画,更加便利的条件渲染、组件模板等等。但 React 可能不太一样,我们希望确保 React 提供的每个解决方案都是完全可靠的、可信赖的。所以对于某些仅为了方便而存在的功能,我们抱着宁缺毋滥的态度,宁愿不去实现它们。比如 React 想要重做动画功能,我们最终交付给用户的功能一定比现有方案快上好几倍。我们不会去标准化一些现有的方案,因为我们对如何实现动画这个功能有自己的构想,这个构想会与 React 库深入结合,它会不同于我们现在看到的所有框架。所以说,在 Vue 中,你可以更快地得到自己想要的功能,但是在 React 这里你可能找不到自己想要的功能,只能依赖第三方库来实现,直到我们搞清楚解决问题的方法,知道如何把这个功能做对、做好,我们才会把它集成到 React 中来。这就有点像 Android 和 iOS 在设计理念上的区别。iOS 相对 Android 就经常会少一些功能,比如最初几版 iOS 连复制粘贴的功能都没有,直到出现了通知中心才有了复制粘贴功能。iOS 的特性可能确实会滞后一些,但是当它确定要做某件事的时候,它会把他实现的非常好,我想这也是我们的工作理念。我认为两种思路各有优劣吧。
Flow 与 TypeScript
主持人:Vue 3 已经不再使用 Flow 了,而 React 还在使用,对这一点你怎么看?
Dan:我不觉得这一点很重要啊。这更多还是取决于库本身是如何编写的,我们目前还是需要去做一些静态的类型检查。不过这与用户们使用的 React 类型没有关系,只影响 React 库自身的开发。我们使用 Flow 只是因为我们一开始就用了它,它也能满足我们后续的需求。我们也可以使用 Typescript,但我觉得这不是很关键。这完全不会影响到 React 的用户,因为我们内部的类型和我们暴露出去的类型也有差异。所以这件事不是很重要,我们并不 care。我们当然可以切到 Typescript,但迁移成本较高,不值得我们这样做。
React 的竞争力
主持人:最近有些新出现的前端框架,比如 Svelte 和 Solid-js,他们都不再使用 Virtual-DOM 了,并且声称在性能表现和 bundle 体积上都超越了 React,你怎么看待这一点?
Dan:还是一样,我没有深度地体验过这些框架,所以我不太好下论断。但还是那句话,看上去总有新的东西在出现,但其实并不是这样的。真正能够走通的设计思路其实并不多,React 在走 immutable 这条路,Vue 和 Svelte 在走 mutable 这条路,然后 Svelte 和 Vue 之间在具体实现上可能会有些差异,Solid-js 应该也是这条路上的一个分支。说到 Virtual-DOM,我其实不是很喜欢这个术语。我们尽量不去使用这个词,因为每个人对它都有不同的理解。但我觉得它不是一个与性能有关的东西。我想再重申一遍,我不愿去使用 Virtual-DOM 这个词,因为它是一个非常容易混淆的概念。当我们提到 “Virtual-DOM” 这个词的时候,我们说的其实是一种 UI 在内存中的表现形式。这应该是你期望得到的东西,因为它为开发者提供了更多的选择。比如 Server Component 是运行在服务器端的,但是它需要定义一个数据格式来传递服务器输出的结果,然后在客户端接收。如果你使用 Server Component 做了一个页面过渡的效果,你肯定希望将它与现有的 UI 合并起来。但这一点又跟 PHP 或者 Rails 那种传统的客户端渲染不一样,传统方法渲染的时候,老的页面会消失,然后新的页面逐渐加载,它并不保留任何的状态。如果你有一个搜索框,你输入一些东西,点击搜索跳转到新页面,此时搜索框会被清空。但使用 Server Component 不会发生类似的事情,我们把组件树发回给客户端,我们可以在客户端中将它和现有的 UI 做 diff ,然后渲染有差异的部分,所以搜索框的状态可以被保留下来。该被替换的组件替换了,这一点之所以能够实现,是因为 UI 在内存中有一种中间态的表现形式,它就是人们说到的 Virtual-DOM。这是用来说明 Virtual-DOM 作用的一个例子。另一个例子是,如果我们现在要将一个完整的动画系统集成到 React 中,我应该怎么做。比如我们现在要做一个手势拖动触发的动画,我们肯定不希望每一帧都去做渲染,我们希望只计算几个版本的 UI。假定最左边是 0% 的版本,最右边是 100% 的版本,我们计算好几个类似的关键帧,在拖动的时候就可以利用插值计算出当前 UI 样子。但问题又来了,你怎么去生成那些关键帧呢?你需要一个 UI 在内存中的表现形式,这样你才能在两者之间做插值。这个例子也说明了 Virtual-DOM 的意义,有些特性离开它就没法实现。所以 Virtual-DOM 与性能无关,它的存在只是为了让一些特性变得可行,我们认为这种 UI 在内存中的表现形式还是很重要的。尽管在一些综合性测试里面,我们可能会比其他方案慢 10% 左右,但你应该明白,这并不是一个问题。至少在我们的测试中,当我们分析 Facebook 一些复杂页面渲染的时候。我们亲眼看到的是,React 只花费了 10% 的时间,另外 90% 的时间开销是应用代码导致的。框架能够优化的,可能也就只有 2% 的速度。所以当你去看那些测试的时候,测试代码可能有 1000 行,但是只有一个 component。你可能就只关注到了那 2% 的差异,但它并不能反映应用的实际表现。所以真正影响性能的是整个 app 如何工作,以及我们如何能让用户代码运行的更好。这就是 Concurrent Rendering,Server Component 以及所有新特性存在的意义。我们要去支持更大规模的用户代码,而不是试着去赢下这些基准测试比赛,有些基准测试只有一些不到 10 行的小组件, 它们不值得我们付出时间和努力。
主持人:最近有很多新的库出来了,人们想知道 React 如何做到跟上潮流,保持竞争力。
Dan:是的,我想我已经从某种角度上回答了这个问题。我们正在努力从全局出发,用更加通用的方式去处理那些从单点很难以解决的问题,比如说 data-fetching,代码分割,未来还有动画以及其他的功能,React 在将来能让它们的实现变得更简单。我猜这就是 React 竞争力的来源吧。不过我个人觉得 React 并不需要某些特性来让它“保持”竞争力。比方说当我要用 React 做一些原型的时候,我要去画一些 UI,React 用起来会很自然,很顺手。即便 React 在将来五年没有新的 feature,我也会觉得使用嵌套的函数来表述 UI 是一件很自然的事情。我觉得这种开发方式很合理,我也很喜欢 React 现在的样子,所以我并不觉得 React 要变得更加 “有竞争力” (Dan 用两只手比划了引号)人们才会使用它。但我还是要重申一下,我们现在正在做的很多功能,可以让当前复杂的开发工作简单化,我对它们的存在感到兴奋。
SSR、CSR、NSR、ESR
主持人:Vue 和 React 都在解决网页渲染的问题,但是当前渲染网页有很多种方法,像是 SSR(Server Side Rendering), CSR(Client Side Rendering), NSR(Native Side Rendering), ESR(Edge Side Rendering),你是怎样描绘未来五年前端的发展的呢?
Dan:这个问题是说,你有很多种不同的方式来运行你的代码,你可以让它在客户端运行、在服务端运行,或者在其他地方运行,问题在于你如何去组织它们。但我觉得这里的大部分工作应该都会由框架来完成,所以再说一遍吧,我还是推荐你去使用框架。像 Next.js
就是一个不错的选择,它能让你对这部分如何实现有一个大体的印象。Next.js
可以通过对 React 现有概念的包装来简化这部分的操作,比如你要使用 Server Component 的话,Next.js
有他自己的 API,getServerSideProps() 或者是类似的方法,你不需要使用 React 原生的方式的去组织项目,Next.js
会通过它自己的 API 将你的项目编排好,让 Server Component 等类似的功能生效。所以我觉得未来... 哦我突然想到,如果你对 Server Component 感兴趣的话,不妨去看一下 Shopify 家最近新出的框架,Hydrogen
。他们最近放出了一个 Demo 演示,如果你搜索 「Shopify Hydrogen Demo」 或者类似的关键词就能看到,里面演示了他们是如何使用 Server Component 的,这也是对未来场景的展望吧。如果你只有一个渲染树,比方说你在写页面或是写个博客网站,你只需要去在服务器端的文件系统中读取一些 Markdown 文件,然后将它们渲染到组件的对应位置,最后把渲染好的组件传递给客户端,你只需要考虑组件就 OK 了。至于在哪里执行代码逻辑,这完全是由你自己决定的。有些可能是在构建的时候执行的,有些页面如果你愿意的话也可以在服务器端运行,只有那些与实际交互相关的代码会被下发到客户端,然后在客户端运行。理想情况下,这整个流程还是一个单一的渲染树,你不必去实时关注这部分的差异。你只需要给一些小提示,比如修改一下文件的扩展名,代码就会自动地在最合理的位置去执行。所以你不用想那么多,想自己既要做服务端渲染,还要做客户端渲染,有可能还要其他端的渲染。框架会帮你把这部分搞定的,你只需要使用同一个范式来写代码,它在所有位置都可以生效。
React 与框架
主持人:如何评价「React 更像是一个系统,而非一个框架」这种说法?
Dan:我不会说 React 自身是一个框架,我认为这个说法并不公平。首先我认为 React 只是一个库,因为它并没有去约束你工作的方式,没有去约束你项目的结构,它只是给你提供了工具,让你能够去构建组件。但我确实觉得 React 正在成为一种架构(Architecture),但这和框架又有所区别。因为想要新建一个 React 的框架其实有很多种方式,但是现在 React 对某些技术的实现也有了自己的「观点」,比如应该如何进行 data-fetching,如何去做路由,如何去做服务器端渲染,这些功能应该怎么组合起来。React 只是一种构建 UI 的方式,而不同的框架可以基于这点为用户提供更加上层的能力。
吸引前端开发者特质
主持人:你认为好的科技公司最吸引前端开发者的特质有哪些?能不能简单列举两三个?
Dan:嗯,我想一想。我觉得最重要的一点是「要能够从身边的人身上学到东西」。对我来说,有一个能够无时不刻学习新事物的环境是最重要的。当然这不是说你要去不断学习新的库,不要因为对前端这一领域全貌的不了解就说「前端一直在变化」这种话。我认为事实并不是这样的。正如我说的,大多数新事物都只继承了旧的思想,虽然层出不求,却只有相同的本质。如果你往更深的地方去探究,你就会发现它们都是同一个东西。但我觉得理想的环境就是,公司 / Team 鼓励你去学习新的知识,团队中有 10 年或者 15 年工作经验的前辈,你可以从他们那里获得收获。组内的高度自治也很不错,你不会被安排去做特定的工作,你可以从任务清单中完成自己想做的工作,这一点也会让工作更有趣。被动的激励自然很重要,但如果你能够发挥主观能动性,作出自己的选择,那也是非常有价值的。我不知道这是否回答了你的问题。
如何学习 React 代码
主持人:很多朋友对你的个人经历感兴趣,你刚加入 Facebook 的时候是如何学习 React 的结构、概念,并慢慢开始贡献代码的呢?
Dan:React 有一个相当复杂的 codebase,里面有很多复杂的上下文导致很难上手。一般来说当有新组员加入的时候,我们会花很多时间陪他们一起看一遍代码。刚开始他们可能会做一些小的 bug 修复或者相对独立的小功能点,来慢慢地熟悉代码。整个流程最关键是要熟悉架构,一旦你对整个项目的架构有了充分的了解,接下来的所有工作就都顺理成章了。有一个对我帮助很大的点,那就是去查看人们的 Issue,并去解决他们。在不同的时间点上,我都会过一遍现在处于 「Open」状态的所有 Issue,看看自己是不是能够解决这些问题,思考一下自己是不是能够理解他们在说什么,如果不懂的话就去学习上下文。我想我个人可能已经看了数千个 Issue 了,如果你想要快速上手项目的话,我认为这是一个非常不错的学习方式。你可以点击查看处于「Open」状态的 Issue,应该差不多有 500 个,你可以从头看,也可以跳转到最后一页从最老的那个开始看。通过看 Issue 你逐渐就能理解当前有哪些问题,慢慢地去理解代码,理解项目的更新历史等等,所以这就是通过 Issue 学习的方法。帮助提出 Issue 的人,在我看来是做贡献的最好方式,实话说我们并不需要人们去贡献那么多的代码,通常 Review 代码也需要花费我们很长时间,而且人们其实很难把功能写对。所以当我们收到社区提交的代码的时候,我们其实并没有从中收获很多。但替我们回答人们的 Issue 确实对我们很有帮助,有时候用户提交了一个 Bug 报告,但实际上是他们自己写的代码有问题。如果社区中有人帮助他们发现了问题,找到了 bug,我们就不用再在这些 Issue 上花费时间了,这也是我最欢迎的贡献方式。
维护 React
主持人:React 有一个非常庞大的 codebase ,请问 React 开发组和社区是如何维护如此复杂的代码仓库的?
Dan:代码仓库确实带来了一些挑战,但是有挑战的原因不是因为代码仓库大,而是因为一些其他的原因。首先你要知道怎么去进行开发,怎样把代码跑起来。举个例子,我们依赖了非常多的自动化测试,大概有数千个测试要跑吧,我觉得数量可能在 5000 个以上。目前我们写的测试代码可能比源代码还要多,我们非常依赖这一点。我们从中学到的一点是:一定要去针对 React 的 public API 写测试脚本。我们之前的做法是对独立的模块进行单元测试,但这对 React 这种项目来说是一个非常糟糕的想法,因为一旦你要重写 React,那些针对老代码的测试用例就没用了。在我们重写 React 的过程中,我们不断地意识到「哦,又有一堆测试代码不能用了,因为之前的代码不存在了」。所以我们把所有的测试用例修改成了仅对 public API 进行测试,来模拟用户的实际行为。测试用例只能用 ReactDom.render() 或者类似的方法,并不能访问内部的 API。在只针对 public API 进行测试的情况下,就算我们替换了原有的文件,测试样例也照样能够跑通,还能顺便测试我们的重写是否是正确的。这算是我们从实践中总结出的经验吧。另一个有趣的点,也可能是比较有争议的一个点,就是我们有很多文件都存在两个版本。如果你看源码的话,你会发现我们有 .old.js 或者 .new.js 这种文件。这些文件基本上是复制粘贴出来的,它们的内容也基本相同。我们用它们来测试一些可能会带来风险更新,对于 Facebook 网站,我们可以同时部署多个版本。我们有时候会将这些实验性的更新写到 .new 文件里面,然后在 Facebook 的实验环境里面进行回归测试,如果测试各项指标没有劣化的话,再将这些更新复制到 .old 文件中去,所以说我们的网站在任何时刻都有两个版本。在实验测试的过程中,其他人也可以去做其他部分的代码提交。这听上去可能比较奇怪,但实际用起来效果还是不错的。
主持人:所以这就是你们在开发中保持 React 代码质量的秘诀吗?
Dan:是的,我觉得是 Facebook 的测试环境真的非常好,我们不仅有针对 React 仓库本身的测试,还有很多针对 Facebook 进行的测试。有时候我们在 React 的开发过程中把功能搞坏了,我们可以在测试环节发现问题。在生产环节中,甚至是生产环节之后,我们都可以部署实验测试,然后去观察哪些指标出现了下降。比如我们去年十月进行了一次重构,那之后我们不得不将手头的工作暂停了两个月,因为我们看到某些指标下降了 1%,我记得好像是网站的评论数之类的少了 1%。我们就要查清楚到底是由性能问题导致的,还是因为 bug 或者其他什么原因。你要知道,其他的框架往往做不到这一点,它们往往需要先发布一个版本,然后可能一年之后才会有人发现里面的 bug,然后上报。但我们不会这样做,我们要确保 React 的高质量,所以我们花费了数月的时间来查这个问题。用二分法不断地分割 commit 提交,不断地去做实验测试,甚至要精确到每个 commit。最终,我们找到了 bug,我们部署的测试环境中确实有一个 bug。当我们将其修复之后各项指标就再次回归正常了,这个结果也给了我们信心。你要知道,在 Facebook 这种体量的公司中,即便只有 1% 的指标劣化,也会影响数百万人。所以如果有大的问题的话会比较容易发现。
阅读 React 源码?
主持人:作为一个使用 React 的前端开发者,我们需要去阅读 React 库的源码吗?如果需要的话,有没有好的阅读代码的方式?
Dan:我觉得没有必要,这可能会是一项相当困难的工作,因为我们没有在任何其他地方提及 React 自身的架构是怎样设计的。如果你上来就开始读代码,你可能会感到非常困惑,不明白为什么这样设计。这可能也是我们将来需要改进的一点,将来到了某个时间点,我们可能会去解释这里面的实现原理。但我觉得如果你只是想玩玩的话,这个过程应该也算不上痛苦。比如我自己就很喜欢做一件事,用 debugger 的步进功能(step-in) 来一行行跑代码,看看代码会跑到哪个函数,运行代码会用到哪些不同的文件。另一件你可以做的事是使用 Chrome Performance Tools,你可以打开 Chrome 的 Performance Tab,点击录制,然后在你的应用中进行一些操作,之后点击停止,你会看到一张火焰图或者火焰表。这个分析结果非常有用,它就像是某种堆栈,你可以看到函数的调用顺序,看到代码中正在发生的事。它常用来测试性能,你可以看到你代码中的哪一部分运行的比较慢,但你也可以用它来做一个当前函数总览,因为上面展示了函数的名称。你可以看到状态变化会引发哪些不同的事情。你可能会发现「哦?这个函数在所有的地方都被调用了,它是做什么的?」。之后你可以点进去,看看里面到底执行了什么。沿着这种性能测试来一步步探索代码我觉得也是一个不错的学习方式,可以了解到 React 在哪部分花了时间,哪些函数是其核心之类的。
保持对 React 热情
主持人:你是如何保持对 React 的热情的?
Dan:我就是很喜欢,不知道为啥,仔细想想的话,可能是因为 React 非常符合我对 UI 代码的看法吧。在进入 Facebook 之前,我就开始使用 React 了,那时候我还在一家小的创业公司。我们当时在开发一个非常复杂的应用,试着将它从 Backbone 迁移到 React。我们迁移不是因为当时 React 是大趋势,而是因为当时用 Backbone 开发一个复杂的 UI 真的非常的困难,相比起来用 React 来实现真的是太简单了。其中心思想就是写一个状态的函数,来表述当前屏幕上应该展示哪些东西。这也是我常常问自己的问题,我的组件应该长什么样子,哪些内容应该展示在屏幕上。这种思想和我的编程思路天然就是契合的。但确实有些东西不太好归纳到这个范式里边,比如 data-fetching 就是一个典型。你真正想要考虑的是屏幕上有哪些内容,但一涉及 data-fethcing,你就要去思考如何与服务端通信、怎么等待服务端返回结果、等待的时候可能还要设置某个状态,这样用户再次发起请求的时候才能忽略掉上次请求的结果。我原本只想考虑哪些东西应该展示在页面上,但这些东西却将问题复杂化了。这也是我们想要为用户提供 data-fetching 能力,比如 Suspense 功能的原因,它可以帮助你减少思考问题的复杂性。我只要考虑想要看到什么,从哪里获取,即便是外部 URL 也不用去考虑时间。我只想要表达目前屏幕上存在哪些东西,之后交由 React 决定如何去展示它们。我觉得让我非常激动的一点是,现在 React 已经能够很好地实现 UI 的组合与嵌套了,但是还有 data-fetching,动画,代码分割,data-asynchronous 这些目前难以实现的东西,我希望这些功能能变得更加简单易用。我希望在五年以后,我们能够用更加简单的方式构建复杂的应用,而这份简单来源于 React 帮助用户处理了这些复杂性。这就是我的想法。
如何像你一样优秀
主持人:如果我想要变得和你一样优秀,有没有什么好的前端学习资料可以推荐的?
Dan:我不确定自己算不算优秀,我其实在很多方面都没有跟上时代的脚步。比如,如果你让我去做一个好看的应用,可能很难去完成。因为我对 CSS 的知识还停留在 2010 年,我对于 CSS Grid 和 Flexbox 也并不是非常了解,我不知道这是不是你希望听到的。但是如果你需要我推荐学习资源的话,我觉得一个很有帮助的点是你可以去挑选一些 UI 的样例,然后从零去实现它们。这个过程中不要去使用 React,也不要去使用其他任何库。比如试着去实现一个带自动补全的输入框,或者是对话框中的一个 tab 之类的,体验一下完成这些操作的复杂度。另一点我很喜欢的是做一些小游戏,这也很有帮助,比如做一个井字棋,或者是贪吃蛇。做游戏会推动你去思考,思考程序如何去设计,思考如何去解决问题,而这一点是你平时写表单、写界面所训练不到的。总结来说,我推荐你做一些体量小,但是有深度的东西,然后从中获得收获吧。
如何度过闲暇时光
主持人:请问工作之余,你是如何度过你的闲暇时光的?
Dan:我工作之余并没有做太多有意义的事。我过去非常爱玩堡垒之夜,我玩的其实并不是很好,但还是玩了很久。我建筑建的很烂,如果有人打我,我就建一堵墙,但通常我都会被吓一跳,然后手忙脚乱地溜走。但我其实也有一阵子没玩了。现在的话一般就听听歌,散散步,做一些业余项目之类的。
justjavascript.com
主持人:你写了一个 「Just Javascript」 的系列文章,我个人也非常喜欢这个系列,已经等不及要看下一期了,想问一下新文章的进展。
Dan:好的,有些朋友们可能还不知道,这其实也是我个人的业余项目,叫做 justjavascript.com
。它像是一个 JavaScript 的课程,它现在还是免费的,但是再过几周可能就不免费了。这个课还是很特别的,它不像其他课程一样用传统的方式讲 JavaScript 的知识,它更多是去教你怎么理解代码,课程里面有很多可视化的东西,比如动画呀、图表呀之类的。它也会教你去从零实现一些东西。我想要通过它告诉人们如何去正确地阅读代码,如何正确地理解代码的运作方式。当然我们也在制作一些新的内容。现在我们正在将整个课程打包,然后上传到网站上,完成后这将是一个付费课程,你购买后可以看到里面的内容,所有的课程、绘图与测试问题都会呈现在网站上。我们还不确实是否会有人买这个课程,如果这样做能够赚钱的话,我们可能会更新更多的内容。这个项目之前一直是免费的,已经有一年半的时间了,我们想要看看它是不是一个可行的商业化产品,之后再决定如何去运营它。这个项目几周后就会正式上线,想要支持的朋友可以关注一下。
对中国开发者说点什么
主持人:有没有什么想要对中国开发者们说的话?
Dan:我不知道该说点儿啥。我不确定在中国有多少人在用 React,我只知道 Vue 在中国非常的流行。但我觉得有更多的选择是件好事,我非常感谢那些 React 文档的翻译者们,以及很多 React 库的中国开发者们。我不知道 React 能不能在中国流行起来,这个远在异国的我们可能影响不了。但是如果你对 React 非常感兴趣,你们有机会改变周围的环境,让它流行起来。如果 React 流行起来,以后找工作可能会更容易吧(笑)。我们很高兴看到人们翻译博客文章,传播知识,举办会议,真心地感谢为此付出的每一个人。
主持人:你今后有没有想要在中国的 React 社区中更活跃一点,我们其实很想跟你多多交流。
Dan:当然了,我其实也很想,但是不知道怎么做。正好有你们邀请了我,我感觉这次活动很酷,之后我们可以更经常地交流。
主持人:好的 ,我这边问完了所有观众的提问,非常感谢 Dan 陪我们进行了这次漫长的在线对话。
Dan:感谢你们邀请我。
主持人:感谢,我们下次再见!
Dan:再见!
附录
英 | 中 |
---|---|
State Management | 状态管理 |
Single Page Application | 单页面应用 |
Immutability | 不变性 |
Spread Operator | 扩展操作符 |
Funtional Programming | 函数式编程 |
Funtional-lite programming | 轻函数式编程 |
Data-fetching | 数据获取 |
server-rendering | 服务器端渲染 |
Flame gragh | 火焰图 |
Tic-tac-toe | 井字棋 |
Snake game | 贪吃蛇 |
Fortnite | 堡垒之夜 |