魔力在于文字--“阿布拉卡达布拉”、“芝麻开场”等--但一个故事中的魔术词语在下一个故事中并不具有魔力。真正的魔力在于理解哪些词语有效,什么时候起作用,什么时候起作用;诀窍在于学会诀窍。
自从钩子在React 16.8中正式稳定以来,已经有很长一段时间了,随之而来的是理解我们应用程序工作方式的一种根本不同的方式。这既是好事,也是坏事:钩子更接近于Reaction编程模型,有助于避免某种微妙而令人困惑的错误,但一些开发人员也表示担心,Reaction正在变成一个黑匣子。这些担忧是完全有道理的;钩子通常看起来很“神奇”,因为大多数复杂性都隐藏在React的内部。
这种“神奇”的感觉在很大程度上是因为Hooks是基于许多开发人员根本不熟悉的一些现有技术和编程语言研究。理解Hooks的一些动机和灵感可以帮助为幕后发生的事情建立一个心理模型。虽然Hooks最初的提议有几个影响来源,但可以说最重要的是代数效应的概念。
❗❗请注意,本文并不是介绍如何使用钩子或钩子如何在内部工作,这只是一种思考钩子的方式。有关如何使用它们的更多信息,我建议从文档开始。
在深入到代数效果的细节之前,让我们先退一步。
类组件似乎工作得很好,为什么要添加另一种编写组件的方式,至少从表面上看,这样做是一样的呢?
React的核心原则之一是,应用程序的用户界面是该应用程序状态的纯函数。在这里,“状态”可以指本地组件状态和全局状态的任意组合,例如Redux存储。当该状态更改并在组件树中传播时,输出表示该状态更改后的新UI。当然,这是对更新实际发生方式的具体细节的抽象,因为React处理必要的实际协调和DOM更新,但这个核心原则意味着,至少在理论上,我们的UI始终与我们的数据同步。
当然,这并不总是正确的。类组件公开了某些场景,如果我们在生命周期方法中没有有效地处理这些状态更改,那么这些场景允许我们忽略状态更改。Dan Abramov就与此相关的一些常见陷阱写了一篇出色的文章,值得阅读更多详细信息。简而言之,类组件使用不同的生命周期方法来处理副作用,但是这会将副作用映射到DOM操作,而不是状态更改。这意味着,虽然UI的可视元素可能会响应状态更改,但副作用可能不会。
由于类组件必须在道具更改时执行这些内部更新以同步其内部状态,因此根据定义它们是不纯净的。但是等等,你说,我想我们说过UI是一个纯粹的状态函数。
钩子代表了一种不同的思考效果的方式。钩子不是考虑组件的整个生命周期,而是允许我们将焦点缩小到当前状态。然后,我们可以声明我们希望效果在哪些状态下运行,确保那些状态更改反映在我们的效果中。当然,“效果”可以是很多事情,从使用useState处理状态、使用useEffect发出网络请求或手动更新DOM,或者使用useCallback计算昂贵的回调函数。
但是,我们如何在一个纯函数中解释这些副作用呢?我很高兴您问到了这一点!
代数效果是在纯上下文中通过定义效果、一组操作和负责处理如何实现效果的语义的效果处理程序来推理计算效果的通用方法。1代数效果概括了一系列潜在的用途,如输入和输出、处理状态、异步/等待等等。
这有点抽象,所以让我们编写一些代码来看看它在实践中是如何工作的。不幸的是,JavaScript实际上并不支持代数效果,尽管Reaction可能会在内部模仿它们。虽然有几种不同的语言2支持代数效果,但我们将使用EFF,这是一种专门围绕代数效果设计的函数式编程语言。别担心,大多数人不会知道EFF,所以我将在学习3的过程中解释一些语法。
代数效果的一个常见用例是处理有状态计算。请记住,效果只是在具有一组操作的接口中。在EFF中,我们使用Effect关键字和类型签名定义了效果:
(*state.eff*)(*a user with a name and age*)type user=string*interffect get:userEffect set:user-&>unit。
一旦我们定义了效果的外观,我们就可以通过使用HANDLER关键字来定义如何处理我们的效果。
WET STATE=HANDLER|y-&>;FUN currentState->;(y,currentstate)|Effect Get k->;(FUN currentstate->;(Continue K Currentstate)currentState)|Effect(Set Newstate)k->;(FUN_->;(Continue k())Newstate);;
嗯,这看起来有点棘手-让我们把它分解一下。我们有一个有三个分支的处理程序,它们都返回一个函数。这个函数将用来处理一些效果(或缺少效果)。
第一个分支y->;Fun currentstate->;(y,currentstate)表示没有效果,当我们到达正在处理的块的末尾时就会发生这种情况(我们很快就会看到)。这里是函数的返回值,所以这只返回内部返回和状态的元组。
第二个和第三个分支与我们的效果相匹配,但是有一个可疑的参数kk,这里是一个延续,它表示在我们执行效果之后的其余计算。
因为延续代表了整个运行中的过程,所以它们本质上是调用堆栈在效果发生时的快照。当我们达到效果时,几乎就像我们在计算上按了一个巨大的暂停按钮,直到我们正确地处理了效果。调用Continue k 4就像是再次按下了Play按钮。
好了,我想我们已经准备好看到我们的效果处理程序在运行了。现在,我们有一个用户处于状态;让我们祝他们生日快乐:
让CREARY=WITH状态句柄let(name,age)=Perform Get in print_string";Happy Birthday,";;print_string name;print_endline";!";;Performance(Set(Name,age+1));Perform Get;;CREARY((";Henry";,39));;
当我们开始计算时,我们首先从state获取用户,它运行处理程序中的第二个分支。此时,我们已经点击了暂停按钮,所以当我们从state获取时,函数已经停止运行。处理程序返回一个函数,该函数调用Continue k currentstate,使用currentstate值继续我们的计算。
每次我们执行效果时,都会发生同样的流程。暂停,做一些工作,点击播放。
亲爱的读者,这里才是代数效果真正闪耀的地方。你看,我们如何保持状态其实并不重要。当然,现在它只是内存中的一个对象,但如果它在数据库中呢?如果它存储在浏览器的localStorage中呢?就CRESTARY所知,这些都是相同的。如果我们愿意,我们可以用在键值存储中存储状态的redisState处理程序来交换我们的状态处理程序。
在JavaScript中,您的代码必须知道哪些是同步的,哪些是不同步的。如果这在将来发生变化,并且状态是异步处理的,我们将需要开始处理承诺,这将需要对涉及此函数的所有内容进行更改。但是,使用代数效果,我们可以简单地完全停止当前进程,直到我们的效果结束,而不是维护一个包含对不同进程的引用的正在运行的进程。
当然,状态不是我们唯一可以用代数效应来处理的东西,比方说我们有一些网络请求想要执行,但我们只想在函数完成之后执行,我们称之为延迟效应。
Effect DEFER:(unit->;unit)->;unit let defer=HANDLER|y->;Fun()->;(Effect(DeferEffect Func)k->;(Fun()->;Continue k();Effect Func());;
请注意,Continue k()不必是处理程序的最后部分,就像在我们的状态处理程序中一样,我们可以随时调用Continuations,想调用多少次就调用多少次-记住,它们只是一个流程的表示。
为确保此功能按预期工作,让我们快速概述一下此功能在实践中的工作原理:
让runWithCleanup=WITH DEFER句柄print_endline";开始我们的计算;Perform(DEFER FUN()->;print_endline";Running Cleanup";);(*做一些工作*)print_endline";正在完成计算";runWithCleanup();;
在这一点上,我相信你在想:“太好了,我们可以随时暂停执行。这与Hooks有什么关系?“嗯,我们在EFF中列出的两个效果存在于React中,只是用了其他名称:状态处理程序(不出所料)映射useState,我们的延迟处理程序的工作方式与简化的useEffect非常相似。前面的示例与用户界面没有直接关系,但是暂停和恢复进程的心理模型,以及延续之后的调度效果,是理解Hooks和Reaction的未来的核心。
那么让我们把注意力转回到React上。前面我们讨论了为什么需要Hooks,但是出现了我们如何看待Hook的问题。回想一下我们对代数效果的原始定义,即一组操作和一组效果处理程序。这里的操作是我们的Hooks(即useState、useEffect等等),Reaction在渲染期间处理这些效果。
我们知道,由于钩子的某些规则,效果处理程序是Reaction呈现周期的一部分。例如,如果您试图在Reaction组件外部调用useEffect,则很可能会收到与无效钩子调用类似的错误。钩子只能在函数组件的主体内调用。同样,如果在EFF中执行效果时没有正确处理它,您将看到运行时错误:未捕获效果延迟。虽然我们必须在EFF中自己设置处理程序,但在Reaction中,它们被设置为呈现周期的一部分。
那么为什么这么重要呢?了解Reaction负责实现效果运行的时间和方式,这一点很重要,因为它允许我们在React中隐藏巨大的复杂性。例如,useEffect的主要用途之一是作为调度器。特别是对于计算昂贵的UI(如复杂动画),调度工作单元极其复杂,Reaction需要能够决定哪些工作是最优先的。在更高级别,Reaction可以暂停和恢复单个组件的渲染,它还可以确定屏幕组件或响应用户输入的组件的优先级。Andrew Clark写了一篇出色的概述,介绍了Reaction Fibre的工作原理及其设计目标,但下面这段关于计划的花絮在这里特别重要:
基于推送的方法需要应用程序(您,程序员)决定如何安排工作,基于拉动的方法使框架(反应)变得智能,并为您做出决定。
通过允许Reaction来分离效果和渲染,我们可以让它减轻一些复杂性。随着Reaction越来越多地向悬念和并发模式等功能发展,这一点将变得越来越重要。
通常,最痛苦的错误来自于我们对工具的心理模型与它的工作方式不完全一致的时候。对于许多Reaction开发人员来说,我认为我们很难理解当我们调用useState时发生了什么。我希望理解代数效应至少为Hooks在幕后的工作提供了一个稍微好一点的模型。当然,值得重申的是,这并不是说这就是Hooks的实际工作方式-它只是尝试并理解它们。
这篇文章没有太深入地研究Reaction的字面内部工作原理,但希望它能更好地提供有关钩子和效果的更一般的直观信息。代数效果是编程语言研究的一个相当新的领域,至少我自己知道,需要大量阅读才能更好地理解它们是什么。如果您想深入了解代数效果背后的研究,我在下面提供了一些建议阅读。
尽管社区中有些人抱怨Reaction变成了一个黑匣子,但重要的是要记住,新工具的存在是有原因的,Hooks和更广泛的Reaction的一个主要目标是保护我们免受我们不想处理的一定程度的复杂性的影响,使我们能够专注于构建更好的UI并取悦我们的用户。
Daan Leijen的演讲“与代数效果的异步”很好地概述了代数效果如何推广到更多的用例,如迭代器、异步/等待等等。如果你更喜欢阅读论文,他还写了“函数式编程的代数效应”,其中列出了一些细节。Daan是Koka的创建者,Koka是另一种专注于代数效应的研究语言。
如果你想看看代数特效在另一种语言中是如何工作的,你可以在这里找到一个关于多核OCaml中的代数特效的教程。还有利奥·怀特关于在OCaml中实施效果系统的演讲。
为了概述Reaction光纤、代数效果、调度和Reaction的未来,Brandon Dail在几年前做了一次演讲,题目是“代数效果、光纤、协程哦,我的天!”(Algebraic Effects,Fibers,Coroutines On My!)。关于代数效果是如何在Reaction中实现的。
这种效应与其处理程序之间的分离是代数效应自引入以来获得越来越多兴趣的原因之一。对于熟悉Monad的读者来说,代数效果是对Monad的限制,这在某些情况下使它们成为“较弱”的抽象,但在实践中,接口和实现之间的区别更加清晰。
↩。
多核OCaml在Reaction文档中被特别提到是一种灵感,但像EFF或Koka这样的语言-尽管它们主要是研究型语言,还没有准备好成为生产焦点-是专门考虑到效果的,这使得它们对于还不懂该语言的人来说更容易阅读。
↩
需要注意的是:在EFF中继续实际上只是一个标识函数(在EFF的pervasives.eff中提供)。它是由EFF创建者推荐的,作为区分延续函数和常规函数的一种方式,当然,如果您愿意,也可以忽略它。
↩。
Smalltalk社区甚至有一个大量使用延续的Web框架,称为Seside。
↩