原文做者:Sebastian Markbåge
前端
译者:UC 国际研发 Jothyreact
写在最前:欢迎你来到“UC国际技术”公众号,咱们将为你们提供与客户端、服务端、算法、测试、数据、前端等相关的高质量技术文章,不限于原创与翻译。git
编者按:本文摘自 React Hooks issue,由 React 做者 Sebastian Markbåge 编写,本文内容丰富,因此翻译上也有难度,若是有翻译不许确的地方欢迎指正反馈。github
看完全部相关的评论以后,我想总结一下个人感想。算法
不得不说,React Hooks 的反响很是强烈。它很受欢迎而且表现不错。 你们广泛承认它,并把它应用到生产中。它的名声和使用规范貌似传播得很好,还被其余库直接采用了。 固然这不是说不存在其余可能的改变了,我想表达的是当前的设计并不彻底失败。
设计模式
围绕该机制的主要讨论是 hooks 实际实现的注入和持久调用顺序的依赖。 有些人但愿其一,或者两个都能改改。 但“最纯的”模型就像 Monad(译者注:一种程序设计模式)同样。
数组
本质上,(传入 hooks 的)参数是为了替换掉 hooks 的实现代码。这有点像通常的依赖注入和控制反转问题。 React 没有本身的依赖注入系统(与 Angular 不一样),它也并不须要,由于大多数入口点是(主动) pull 而不是(被动) push 的(译者注:可参照 Rxjs 中关于 push&pull 概念的解释)。至于其它代码,模块系统已经提供了良好的依赖注入边界。就测试而言,咱们比较推荐其它技术,好比在模块系统级别进行 mock(例如使用 jest)。
安全
与之不一样的是 setState,replaceState,isMounted,findDOMNode,batchedUpdates 等 API。事实上,React 已经用依赖注入将 updater 插入到 Component 基类中,做为构造函数的第三个参数。Component 实际上啥都没干。这也正是 React 在 React ART 或 React Test Renderer 等相同环境中的不一样版本中,具备多种不一样类型的渲染器的原理。自定义渲染器也是这么干的。
服务器
理论上,相似 React-clones 这样的第三方库可使用 updater 来注入它们的实现。在实践中,大部分人倾向于经过 shim 模块来替换整个 react
模块,由于可能存在某些权衡或者他们想要实现某些 API(例如,移除开发模式内容,或者合并基类及其实现细节)。
闭包
在 Hooks 的世界里,这两个选项仍然保留。 Hooks 其实并非在 react 包中实现的。它只是调用当前的调度程序(dispatcher)。正如我上面所说,你能够随时将其临时重载为任何你想要的实现,React 渲染器正是经过这一点,实现多个渲染器共享同一个 API。例如。你能够专门为单元测试 hooks 建立一个 hooks 测试调度程序。“调度程序”这个名字有点吓人,但咱们能够随时改变它,这不是设计缺陷。如今,你能够把调度程序移到用户空间中,但这会给那些与该组件的做者几乎毫无关联的用户带来额外的干扰,正如这个 issue 中的大部分人并不了解 React 的 updater 同样。
总的来讲,咱们可能会引入更多的静态函数调用,由于它们更适合摇树优化技术,能够更好地优化及内联。
另外一个问题是,hooks 的主要入口是 react
而不是第三方包。极可能未来 react
会移除许多现有的东西,而 hooks 会被保留,因此 hooks 的膨胀并非问题。惟一的问题是,这些 hooks 隶属于 react,而不是其它更通用的东西。例如 Vue 也曾考虑过实现 hooks API。然而,hooks 的关键是它的原语(primitives)很明确。在这一点上,Vue 有彻底不一样的原语,而咱们已经对原语进行了迭代。其余库可能会提出略有不一样的原语。在这一点上,过早地使这些过于笼统是没有意义的。咱们选择把第一次迭代在 react 上实现,只是为了代表这就是咱们对原语的愿景。若是其它库有雷同,那么咱们将建立第三方包以整合这些库,并将 react
的库重定向到该包。
要明确的是,咱们想谈的并非执行顺序的依赖。 先使用 useState 仍是先使用 useEffect 之类的问题并不重要。
React 中有不少依赖于执行顺序的模式,正是因为容许在渲染(render)中变异(mutation)(这仍然保持渲染自己的纯净)。
我不能更改代码中 children 和 header 的顺序。
Hooks 不关心你按什么顺序使用它们,它只关心顺序是否持久,每次都按照同一个顺序。这与调用之间隐含的依赖性截然不同。
最好不要依赖持久顺序 - 全部事情都是平等的。可是,你得作权衡 - 好比说 - 语法干扰或其余使人困惑的东西。
有些人认为就算只是为了纯粹主义,那也该这么作。可是,有些人也考虑得比较实际。
有人担忧它会变得使人困惑,这可能发生在许多层面。我认为这一点并不使人困惑,由于人们彻底无能为力或只能放弃。但事实上,基础知识很是容易掌握。
更有甚者,担忧一旦出现问题,咱们很难弄清楚哪里出了问题。即便你了解它的工做原理,你仍然可能犯错误,在这种状况下,你必须得轻松找出问题并修复它。咱们发现了很多这类问题。通常来讲它会被 lint 规则捕获,报错信息足以解释缘由。可是咱们能够作得更多。咱们能够制造编译硬错误,在开发模式中跟踪 hooks 的相关信息,在顺序切换时发出警告。在这些状况下,咱们能够优化错误消息,以显示更改的堆栈,而不只仅是显示 something changed。类型系统中逐渐有模拟效果的趋势,例如 Koka。我敢打赌 JavaScript 确定会应用它,只是时间问题。
另外一个问题是,这些约束是否使编码更加困难。对于普通的条件代码,状况彷佛并不是如此。它通常比较容易重构。缺少早期响应有点恼人,但也没什么大不了的。 Hooks 还有其余与顺序无关的难题。
可是,重构循环中的 Hooks 可能会很是烦人。解决方案一般是将循环体分解为单独的函数。这也挺不方便的,由于你须要把全部数据经过 props 传递,不然将会抱闭包问题。这是 React 中更难的问题了,不只限于 Hooks。这么作是最佳实践,有助于优化。例如,在更改列表中的单个项目时保持低渲染成本,确保每一个子项均可以独立。使用 Suspense 意味着每一个子项均可以并行获取并渲染而不是按顺序获取,报错边界具备相似的要求。所以就算是单独解决 Hooks 问题,将循环体拆分为单独的组件仍然是最佳实践 - 这也解决了 Hooks 的循环问题。
也就是说,Hooks 的最初实现能够建立一个用做编译器目标的键控嵌套做用域。它们确实建立了一种以嵌套方式支持 Hook 的机制。但它不是很是符合人体工程学或易于解释,而且不管如何都会遇到上述问题。若是须要,咱们能够在未来添加它。这如今它不该该是常见状况。
咱们考虑的各类替代方案各自都存在许多缺点。
大多数方案不支持循环。这是关于 Hooks 无条件性的最大限制因素,彷佛许多提案都忽略了这一点。只是让它在本身的条件下可用并无价值。
大多数方案并不解释自定义 hooks 做为常见状况的缘由。咱们认为这是实现性能和语法轻量级的重要目标。
一旦你容许 hooks 用于条件语句,有些地方会变得奇怪起来。例如,你可能会在条件中看到 useState,但这意味着什么?这是否意味着它的做用范围只在该块中,仍是说它的生命周期随之变化? if (c) useEffect(...)
是什么意思?这是否意味着当条件为真时该 effect 会触发,仍是说每次 effect 为真时它都会触发?当条件为否是卸载仍是继续组件的生命周期?
对于像在 body 外声明 hooks 的提议,屡次调用 hooks 意味着什么?仅仅由于技术上能够实现,不会让它变得不那么混乱。
大多数方案使用大量的间接和虚拟调度,很难进行静态分析,这使得死代码消除,内联和其余类型的优化变得更加困难。当前的 hooks 提议是很是有效的,由于它只有索引属性,能够轻松 minify,而且具备可预测的 O(1) 查找成本。请记住,文件大小很重要,这是当前设计真正的亮点。
此为旁注:有人提到你们都关注并发的全局状态。 若是未来 JS 支持线程,或者若是咱们编译出某些支持线程的东西,咱们但愿可以支持多个组件的并行执行。 可是,这在实践中不是问题,由于咱们存储的用于跟踪当前正在执行的组件和当前 hook 索引的小状态能够轻易进入线程本地存储 - 不管如何,这在某些形式的解决方案中始终是必需的,不管是可变域(mutable field)仍是代数效应(algebraic effects)。
你们广泛关注调试会是什么样的。咱们从如下几个角度来看。
首先是错误信息。为了带来更好的错误消息,咱们下了些工夫。当在 DEV(开发环境)中检测到违反 hook 规则时,咱们至少能很好地处理错误。
断点调试变得很是简单,由于它只是使用正常的执行堆栈,这点不像 React 一般的作法。有些替代方案使用了更多的间接性,会让断点调试更难。
另外一个问题是树的反射。 React DevTools 容许你检查树中任何内容的当前状态。在生产包中,咱们常会将类名等进行 minify。极可能在咱们添加更多的生产优化,例如内联和删除没必要要的 props 对象以后,若是没有 source map,更多这样的事情将不会自动进行。咱们没有为了生产调试而将元数据添加到 API 设计中的信仰。可是咱们能够在开发模式时,进行辅助元数据(如 source map)等操做。
也就是说,咱们已经证实了咱们可使用 Debug Tools API 提取大量反射元数据。咱们还计划添加更多,以便让库具备良好的扩展点,以便为调试提供更丰富的反射数据,或者解析源代码行以将名称添加到单个 useState 调用。
你们都知道测试很重要,因此咱们得在更普遍的发布以前清楚地记录它。这一点毋庸置疑。
至于技术细节,我想上文提到的依赖注入点已经告诉了你能够如何作到。
我认为 API 设计中有一种感受,就是存在“可测试”的 API。当听人这么说时,我以为他们会想到纯函数之类的东西,只有一些输入变量能够单独测试。 React API 很是简单,你可能只会想直接调用 render 函数或直接调用单个 hook。
惋惜 API 的丰富性也带来了些微妙差异。你不老是依赖它,因此你能够常常在特殊状况,或者在简单的测试中一步使用它。可是,随着代码库的增加,你会遇到愈来愈多这样的状况,而且你不但愿在每一个代码库中都从新实现 React 运行时的全部细微差异。因此你想要一个测试框架。
好比说,咱们为这个用例构建了浅层渲染器类。它容许你“使用”或者“遵循”(诸如此类的动词)正确的语义来调用全部生命周期。测试 Hooks 原语的全部细微差异也挺有必要的。
然而在实践中,咱们发现你们不怎么使用浅渲染器(shallow renderer)。使用深层渲染更为常见,由于你正在测试的工做单元一般依赖于更低的几个级别,它们已经经过了测试。
也就是说,咱们还将包含一种与组件隔离的直接测试自定义 hooks 的方法。咱们要作的就是添加一些模拟调度程序的东西,并保持原语的语义一致。
这会取代 Redux 吗?它会增长必须学习全部 Flux 知识的负担吗?与通常的 Flux 框架相比,Reducer 的使用范围要窄得多。它很简单。可是,若是你看一下框架/语言的方向,好比 Vue,Reason,Elm。这种调度并归集逻辑,以在更高级别的状态之间转换的模式彷佛取得了巨大成功。它还解决了 React 中回调的许多奇怪问题,为复杂的状态转换带来了更多直观的解决方案。特别是在并行的世界中。
在膨胀性方面(In terms of bloat),它并未给 React 添加其不须要的任何代码。在概念方面,我认为这是一个值得学习的概念,由于相同的模式不断以各类形式出现,最好有一个中央 API 来管理它。
因此我更多地把 useReducer 当成中心 API,而非 useState。 useState 仍是很棒的,由于它对于简单用例来讲很是简洁而且易于解释,但人们应该尽早研究 useReducer 或相似的模式。
也就是说,useReducer 并无作 Redux 和其余 Flux 框架所作的许多事情。我通常认为你不会须要它,因此它可能不像如今那样广泛存在,但它仍然存在。
有人说过,当你只想暴露一种消费 Context 的方法时,理想状况下你不该该从模块中暴露 Context Provider。看似 useContext 鼓励你暴露 Context 对象。我认为这样作的方法是暴露一个自定义 hooks 以供消费。好比 useMyContext = () => useContext(Private),这一般会更好些,由于你能够自由添加自定义逻辑、将其更改成全局逻辑或再行添加弃用警告。它彷佛不是须要框架进一步抽象来执行的东西。
咱们能够考虑让 createContext 直接返回一个 hooks ,咱们也鼓励使用这个常见的模式。 [MyContextProvider, useMyContext] = createContext()
Context Provider 的另外一个怪异点是没法用 hooks 提供新的上下文,你仍然须要一个包装器组件。相似的问题还有你没法经过 Hooks 或相似 findDOMNode 的方式将事件监听器附加到当前组件。
这么作的缘由在于,Hooks 在设计上要么是独立的,要么只是观察值。这意味着使用自定义 Hook 不会影响组件中未明确传递给该 Hook 的任何内容,它从不钻入任何抽象层次。这也意味着顺序依赖可有可无。惟一的例外是在处理相似遍历 DOM 的全局可变状态时。它是一个逃生口,但不是你能在 React 世界中滥用的东西。这也意味着使用 Hooks 不依赖于顺序。像 useA(); useB();
或者 useB(); useA();
这样调用都行。除非你经过共享数据的方式显式建立依赖项。let a = useA(); useB(a);
到目前为止,最怪异的 Hook 是 useEffect。须要明确的是,它预计是迄今为止最难使用的 Hook,由于它使用较难管理的命令式代码,这就是咱们试着保持声明式的缘由。可是,从声明式变为命令式很难,由于声明式能够处理更多不一样类型的状态和每行代码的转换。当你实现某个效果时,理想状况下也应处理全部随之而来的 case。这么作的部分目的是鼓励处理更多 case,这样的话有些怪异点也是能够解决的。
毫无疑问,第二个参数挺古怪的。把它做为第二个而不是第一个参数是由于对于全部这些方法,你能够先编写代码,而后再进行添加。该属性的好处是你能够在 IDE 中使用 linter 或代码重构工具,或者让编译器根据你的回调自动添加它。这是从 C# 中吸取的经验,其中语法顺序旨在支持自动完成等功能。
也许它该有个比较函数。我尚未看到不能将它重写为输入的状况,但不管咱们是否添加比较函数,咱们均可以放到以后作。那也须要一个输入数组,来让咱们知道要存储及传递什么给比较函数。
如今不容许使用异步函数做为 effect,意思是你必须费尽心思来作异步清理。很难保证异步 effect 的正确,由于这些步骤之间一切皆有可能。在初始化新 effect 以前不能进行清理,不然可能会影响 effect 的属性。以后咱们有可能放宽这种约束,但我怀疑它是一个糟糕的模式,也许咱们不该该鼓励在第一个版本中使用它。
useEffect 最奇怪的状况是在使用闭包时。这会在咱们想要跟踪某个取值的时候混淆视听。由于闭包的值实际上不是 reactive 的,它们会捕捉当前的状态,实际上这是一个不错的优势。因为批处理和并发模式,多数状况下,事物以意想不到的方式交织。因为违反直觉的闭包,捕获的值确实会致使错误,但一旦修复,会大大减小它们的竞争条件问题。
另外一个是内存使用问题。我想说 React 的内存使用量通常都是不可预测的,由于咱们基本上都记得树中的全部东西。可是,因为闭包共享执行环境,它可能致使额外的反直觉延长。这些能够经过使它成为一个自定义 hooks 来解决,但它不老是那么明显,因此有时候你必须这么作。了解此模式的优化编译器也能够轻松解决此问题。
针对这个问题,有个解决方案是咱们能够引入 useEffect 做为相同的函数,将全部输入参数传递为函数的参数,而且鼓励挂起它们。但这是有问题的,由于闭包的好处是方便引用计算值。其余模式咱们也鼓励使用闭包。因此这彷佛破坏了「一切都进入方法体」的思想。这反过来又解决了其余问题,例如默认 props,计算值等。我不肯定这对于剩下的少数状况来讲是否值得去作,但不作的话会遗留更多。
有几我的指出咱们缺乏了一些 API。
setState
的第二个参数在此模型中不能很好地运行。一样,咱们在 ReasonReact 中尚未相似 UpdateWithSideEffects 的东西。咱们已经考虑好了如何使用它,并会在以后作补充。例如,在 reducer 中调用 emitEffect。
因为状态转换,咱们没有办法改动单个组件。若是有方法,那么咱们可能反过来须要像 forceUpdate
那样绕过它来进行改变。
咱们尚未 getDerivedStateFromError 和 componentDidCatch 的替代方法,但咱们有 HOC 来提供此功能。好比 catch((props, error) => { if (error) setState(...); useEffect(() => { if (error) ... }); })
。咱们会在以后添加。
一直以来总有人问,是否能够有更底层的 API 来实现其余语言(如 ClojureScript 或 Reason)的特定语义。这是咱们确定想要支持的,但我不肯定是否该使用相似公共 API 的机制来完成。好比说,React 的优化编译器须要一个不一样的入口点来定位该格式。该机制要能够用于各类较底层的操做,没必要进行易用性优化。所以,咱们可能会将其与公共 API 分开添加。
我相信大多数 JavaScript 类型的问题都已获得解决,由于 Flow 和 TypeScript 如今都已定义。
有个有趣的问题尚未被说起:即便发生了调用顺序错误的状况,是否仍能够在其余语言(例如 Rust 或 Reason)中正确地编码。 这尚未获得证明 - 至少在非运行时没有。
有些人担忧这些非纯函数会影响编译器的优化。对此我有异议:咱们也想,而且已经作了许多运行时或静态执行的优化。
事实上咱们努力地想将 Hooks 拿出来,由于它很是适合优化。它谨慎地鼓励许多静态的解决模式产生。
有两个优化是关于合并组件的。对于由父组件无条件渲染的组件来讲,这是普通 hooks,你能够直接调用。你能够在用户空间中执行此优化。即便是对于循环和条件,咱们也知道如何为它们添加相同做用的做用域。即便是动态渲染的组件,咱们也能够跳过额外 Fibers 的建立,例如当一组父组件渲染一个也是函数组件的子组件时。咱们只须要在函数类型改变的状况下,跟踪切换发生的顺序就行了。
这对于基于记忆(memoization)的优化一样有效。在具备代数 effect 的语言中,记忆功能只须要同时记住 effect 就像。这里也同理。记忆只须要跟踪调用期间发出的 hooks 。
许多使用对象或传递头等函数的替代方案,须要以某种方式展开,这种方式因为其间接性而加大了实际优化的难度,其中尤以 Generator 的难度最甚。
有人提到关于 setState 中的重载 API,它接受函数或函数的返回值做为参数。 这是一个艰难的设计决定,咱们进行了诸多权衡。 确实,重载的 API 有时会致使难以预料的安全问题。 咱们就曾有过一例因子组件同时接受字符串或元素而致使的问题。 但也有许多重载的 API 是很是有用的抽象,不会致使安全问题。 我会更深刻地研究这一点,以确保对风险作合理的评估。
还有一个还没有提出但我想说明的问题。 若是你认为第三方 Hook 不可信,由于它能够有条件地添加/删除其状态 hooks ,而后从其 hooks 的末尾开始读取。 你容许它从外部组件读取状态。 若是你能够执行代码,一般你就输了,但在相似 Caja/SES 的环境中,这多是相关的。 这是比较不幸的状况。
为何全部特殊的 hooks 都是核心?因为全部机制都必须存在,所以它们中的大多数并无真正增长体积,而更多的是概念开销。其中大多数是没法在用户空间中实现或经过描述意图提供重要价值的原语。
例如,你如今能够在用户空间中使用 useMemo,但咱们但愿,未来在低内存状况或窗口组件中丢弃记忆值的状况下,状态仍能被保留。
useCallback 只是一个围绕 useMemo 的简单包装器,可是咱们已经想好将来如何进一步优化它们,不管是静态仍是使用运行时技术。
useImperativeMethods 提供了一个能够在用户空间中构建的 API,但因为咱们有几种不一样的方式与 refs 交互,所以以单一规范方式维护它们更好一些。咱们已经两度更改了 refs。
我一直听到的一个争论点是这次改变的动机不足,由于“类(class)挺好的呀”。听说那帮试图学习的用户奔溃了。我认为这个论点过于片面,有人还强调 class 对于新手来讲难比登天呢——因此这不是重点。
主要动机是像闭包这样的模式天然会建立值的副本,这使得编写并发代码更加简单,由于你能够在任何给定点存储 n 个状态,而不是在可变类的状况下只存储一个状态。这避免了许多类的坑,由于类看起来很直观但实际结果难以预料。
类(class)彷佛是保持状态的理想方式,由于它本就为此而生。可是,React 更像是一个声明函数,它不断被反复执行以模拟反应状态。两者有一个阻抗不匹配,当咱们把这些看做是类时会不断泄漏。
另外一个是 JS 中的类在同一命名空间中合并方法和值的问题。这是的优化难以进行,由于有时方法的行为相似于静态方法,有时又相似于包含函数的值。 Hooks 模式鼓励对辅助函数使用更静态可解析的调用。
在类中,每一个方法都有本身的做用域。它致使像咱们这样的问题必须从新发明默认 props,以便咱们能够建立一个单独的共享解析对象。咱们还鼓励你使用类中的可变字段在这些方法之间共享数据,由于惟一共享的是 this
。这对于并发性来讲也是有问题的。
另外一个问题是,React 的概念心智模型只是递归调用其余函数的函数。用这些术语表达它有不少价值,有助于创建正确的心智模型。
我很是同情的一个问题是,这只会增长学习 React 的脑力成本 - 短时间内。那是由于你可能要在可预见的将来学习 Hooks 和 class。要么是由于两者你都使用,你的代码库中有以前的类或者其余人编写的类、你在 stackoverflow 上读过的一个例子或教程使用的类,或者你正在调试的一个库使用了它。
虽然须要不少年,但我打赌其中一种方法将取得胜利。要么咱们必须回滚 Hooks,要么逐渐减小 class 的使用,直到它们彻底消失为止。
我认为你们的批评很公正。咱们的确没有明确的答复,或给出「能够合理地将 class 移出核心并外化为 compat 层」的预计路线图。我认为短期内咱们都不会给出明确答复,直到实际看到 Hooks 变得更好。到那时候,咱们会看是否给出下降类重要性的时间线。
英文原文:
https://github.com/reactjs/rfcs/pull/68#issuecomment-439314884
好文推荐:
React 16.x 路线图公布,包括服务器渲染的 Suspense 组件及Hooks等
React 官方发布 V16.6.0,新增支持 lazy,memo 和 contextType
“UC国际技术”致力于与你共享高质量的技术文章
欢迎关注咱们的公众号、将文章分享给你的好友