
语言的魔力是咒语,芝麻开门,等等,但是在一个故事里的咒语在下一个故事里就没有魔力了。真正的神奇之处在于懂得哪些词语,什么时候起作用,为什么起作用;诀窍在于学会诀窍。
自从在React 16.8中正式稳定了钩子已经有一段时间了,伴随它们而来的是一种完全不同的理解我们应用程序工作方式的方式。这既是一件好事,也是一件坏事:钩子更接近于React编程模型,有助于避免某些微妙而令人困惑的bug,但一些开发人员也担心React会成为一个黑盒子。这些担忧完全有道理;钩子通常看起来很“神奇”,因为大多数复杂性都隐藏在React的内部。
这种“神奇”的感觉主要是因为hook是基于一些现有技术和编程语言研究,而许多开发人员只是不熟悉。理解hook的一些动机和灵感可以帮助建立一个心理模型来了解幕后发生了什么。虽然有几个来源的影响,最初的钩子建议,可以说最重要的是代数效应的概念。
注意,本文不介绍如何使用钩子或内部钩子是如何工作的。这只是考虑钩子的一种方式。关于如何使用它们的更多信息,我建议从文档开始。
在深入研究代数效应的细节之前,让我们先退一步。
我们为什么需要钩子?
类组件似乎工作得很好,为什么要添加另一种编写组件的方式,至少表面上,做同样的事情?
React的核心原则之一是,应用程序的用户界面纯粹是应用程序状态的函数。这里,“state”可以指本地组件状态和全局状态的任何组合,比如Redux存储。当状态更改并通过组件树传播时,输出表示状态更改后的新UI。当然,这是对实际更新如何发生的具体细节的抽象,因为React处理实际的协调和必要的DOM更新,但这个核心原则意味着,至少在理论上,我们的UI总是与数据同步的。
当然,这并不总是正确的。类组件暴露了某些场景,如果我们不能在生命周期方法中有效地处理这些状态更改,这些场景允许我们忽略状态更改。丹·阿布拉莫夫(Dan Abramov)写了一篇关于与此相关的常见陷阱的优秀文章,值得一读以了解更多细节。简而言之,类组件使用不同的生命周期方法来处理副作用,但这将副作用映射到DOM操作,而不是状态更改。这意味着,虽然UI的可视化元素可能会响应状态更改,但副作用可能不会。
因为类组件必须做这些内部更新,以同步它们的内部状态时,道具改变,它们的定义是不纯的。等等,你可能会说,UI是一个状态函数。
精确。这就是钩子发挥作用的地方。
钩子代表了看待效果的另一种方式。与其考虑组件的整个生命周期,挂钩允许我们将关注点缩小到当前状态。然后,我们可以声明要在其中运行效果的状态,确保这些状态更改反映在效果中。当然,一个“效果”可以是很多事情,从用useState处理状态,发出网络请求或者用useEffect手动更新DOM,或者用useCallback计算昂贵的回调函数。
但是我们如何在一个纯函数中推断这些副作用呢?我很高兴你问了!
代数效应的介绍
代数效应是通过定义一个效应、一组操作和一个效应处理程序(负责处理如何实现效应的语义)来在纯上下文中推理计算效应的一种通用方法。1代数效应泛化在许多潜在的用途上,如输入和输出、处理状态、异步/等待,以及更多。
这有点抽象,所以让我们写一些代码来看看这在实践中是如何工作的。遗憾的是,JavaScript实际上并不支持代数效果,尽管React可能在内部模仿它们。虽然有一些不同的语言2支持代数效果,但我们将使用Eff,这是一种专门围绕代数效果设计的函数式编程语言。不用担心,大多数人不会知道Eff,所以我将在后面解释一些语法。
代数效果的一个常见用例是处理有状态计算。记住,效果只是在具有一组操作的接口中。
让我们把它分解一下。我们有一个具有三个分支的处理程序,它们都返回一个函数。该函数将用于处理某些效果(或缺少效果)。
第一个分支,y ->有趣的currentState -> (y, currentState),表示没有效果,当我们到达我们正在处理的代码块的末尾时就会发生这种情况(我们很快就会看到)。这里的y是函数的返回值,所以它只返回内部返回值和状态的元组。
第二个和第三个分支匹配我们的效果,但是有一个可疑的论点,k,这里是一个延续,它表示在我们执行效果之后的剩余计算。
GOTO,但更好的在我的心里,我是一个像神谕的人;我的创建设置标签,我的方法执行跳转。然而,这是一种非常强大的goto指令。如果您的头发在这一点上变绿了,不要担心,因为您可能只处理延续的用户,而不是概念本身。
这个小gem来自于GNU Smalltalk延续文档。对于有些人来说,对GOTO的引用可能会让您感到有些厌恶,但是延续仍然作为控制流而存在是有原因的,这是关于上下文的。GOTO更危险的方面之一是被扔进了一个无效的上下文中,但是使用延续,您实际上是在存储一个动态进程,因此变量、指针等都将是有效的。
因为延续表示运行中的整个过程,所以它们本质上是效果发生时调用堆栈的快照。当我们得到一个效果时,就好像我们在计算中按下一个巨大的暂停键,直到我们正确地处理这个效果。呼叫continue k4就像再次按下播放按钮一样。
当我们开始这个计算时,我们首先从state获得用户,它运行处理程序中的第二个分支。在这一点上,我们已经按下了pause按钮,所以当我们从state得到这个函数时,函数已经停止运行。处理程序返回一个函数,该函数调用continue k currentState,用currentState的值恢复我们的计算。
每次执行一个效果时,都会发生相同的流。按暂停,做一些工作,按播放。
这里,亲爱的读者,是代数效应的力量真正闪耀的地方。你看,我们如何保持状态并不重要。当然,现在它只是内存中的一个对象,但如果它在数据库中呢?如果它存储在浏览器的localStorage中呢?据celebrate所知,这些都是一样的。如果需要,我们可以使用存储键值存储中的状态的redisState处理程序交换状态处理程序。
在JavaScript中,您的代码必须知道什么是同步的,什么不是。如果将来发生更改,并且状态是异步处理的,我们将需要开始处理承诺,这将需要对涉及此函数的所有内容进行更改。但是使用代数效果,我们可以完全停止当前进程,直到效果完成,而不是维护一个运行的进程,该进程包含对另一个进程的引用。
当然,状态不是我们能用代数效应处理的唯一东西。假设我们有一些网络请求,我们想要创建或清理,我们想要执行,但我们只想在函数完成后执行。我们称之为延迟效应。
此时,我相信你们会想"太好了,我们可以随时暂停执行。这和钩子有什么关系?好吧,我们在Eff中列出的两个效果存在于React中,只是它们的名字不同:state处理程序(意料之中)反映了useState,而我们的defer处理程序工作起来很像一个简化的useEffect。前面的示例与用户界面没有直接关系,但是暂停和恢复过程的心理模型,以及在延续之后的调度效果,是理解钩子和React未来的核心。
反应中的代数效应
让我们把注意力转回反应。之前我们讨论了为什么需要钩子,但问题是我们如何看待钩子。回想一下我们最初将代数效果定义为一组操作和一组效果处理程序。这里的操作是我们的钩子(例如useState, useEffect,等等),并在渲染期间处理这些效果。
由于钩子的一些规则,我们知道效果处理程序是反应呈现周期的一部分。例如,如果您尝试在一个React组件之外调用useEffect,您可能会在无效钩子调用的一行中得到一个错误。钩子只能在函数组件的内部调用。类似地,如果你在没有正确处理的情况下在Eff中执行一个效果,你会看到运行时错误:未捕获的效果延迟。虽然我们必须自己在Eff中设置处理程序,但在React中,它们是作为渲染周期的一部分设置的。
为什么这很重要呢?理解React对何时以及如何运行您的效果的实现负责是很重要的,因为它允许我们在React中隐藏大量的复杂性。例如,useEffect的关键用途之一是作为调度器。特别是对于计算开销较大的ui(比如复杂的动画),调度工作单元是非常复杂的,并且React需要能够决定什么工作是最高优先级的。在更高的级别上,React可以暂停和恢复单个组件的呈现,这同样可以对屏幕上的组件或响应用户输入的组件进行优先级排序。安德鲁·克拉克(Andrew Clark)写了一篇关于纤维如何起作用及其设计目标的精彩概述,但这篇关于日程安排的小文章在这里尤为重要:
基于推的方法要求应用程序(你,程序员)决定如何安排工作。基于拉的方法允许框架(React)更聪明地为您做出这些决定。
通过允许反应分离效果和渲染,我们允许它减轻我们的一些复杂性。随着React越来越多地转向悬念和并发模式,这一点将变得越来越重要。
结论
当我们对工具的思维模型与它的工作方式不一致时,通常会产生最痛苦的bug。对于许多React开发者来说,我认为我们很难看到grok在调用useState时发生了什么。我的希望是,对代数效应的理解至少为钩子在幕后所做的工作提供了一个稍微好一点的模型。当然,值得重申的是,这并不是说钩子就是这样工作的——只是试着去理解它们。
这篇文章并没有过多地探讨React的内部工作原理,但希望它能提供对钩子和效果更普遍的更好的直觉。代数效应是编程语言研究中最近才出现的一个领域,至少我自己知道,要更好地理解它们需要大量的阅读。如果你想深入研究代数效应的背后,我在下面列出了一些建议。
尽管一些社区的投诉反应成为一个黑盒,重要的是要记住,新工具的存在是有原因的,而更广泛的钩子和反应的主要目标是保护我们免受一定的复杂性,我们不想处理,让我们专注于构建更好的ui和取悦用户。
