[译]JavaScript响应式的最佳解释

4,403 阅读7分钟

原文地址:The Best Explanation of JavaScript Reactivity

许多前端JavaScript框架(例如Angular,React和Vue)都有自己的Reactivity引擎。通过了解响应式及其工作原理,您可以提高开发技能并更有效地使用JavaScript框架。在视频和下面的文章中,我们构建了您在Vue源代码中看到的相同类型的Reactivity。

💡响应式系统

当你第一次看到它时,Vue的响应式系统看起来很神奇。拿这个简单的Vue应用程序来说:

不知何故Vue只是知道如果price改变,它应该做三件事:

  • 更新网页上price的值。
  • 重新计算乘法表达式price * quantity,并更新页面。
  • 再次调用函数totalPriceWithTax并更新页面。 但是等等,你是否会好奇,Vue如何知道price更改时所要更新的内容,以及它如何跟踪所有内容?
    这并不是JavaScript通常的工作方式

我们解决一个大问题是通常不会这样编程。例如,如果我运行如下代码:

你觉得它打印什么?由于我们没有使用Vue,它将打印出来10

在Vue中,我们希望total随着pricequantity更新而更新。我们想要:

不幸的是,JavaScript是程序性的,而不是响应式的,所以这在现实中不起作用。为了实现total响应,我们必须使用JavaScript来使事情表现得与众不同。

⚠️ 问题

我们需要保存如何计算得到total,这样可以在pricequantity更新时重新运行它,

✅ 解决方案

首先,需要一些方法告诉我们的应用程序,“我即将运行的代码,存储它,我可能需要在其他时间运行它。”然后我们开始运行代码,如果pricequantity变量得到更新,再次运行存储的代码。

我们可以通过record函数来执行此操作,以便我们可以再次运行它。
请注意,我们在target变量中存储了一个匿名函数,然后调用一个record函数。使用ES6箭头语法我也可以这样写:
这个record函数定义很简单:
我们正在存储target(在我们的例子中{ total = price * quantity }),所以我们可以稍后运行它,可以通过一个replay函数来运行存储的所有内容。
这将遍历执行storage 数组中存储的所有匿名函数。 然后在代码中,我们可以:
很简单吧?如果您需要阅读并尝试再次理解它,这里有完整的代码,仅供参考,如果您想知道原因,我会以特定的方式对此进行编码。

⚠️ 问题

我们可以根据需要继续记录target,但是有一个更强大的解决方案可以扩展我们的应用程序。一个负责维护target列表的类,当需要它们重新运行时,这些target列表会得到通知。

✅ 解决方案:依赖类

我们解决这个问题的一种方法是将这种行为封装到它自己的类中,这是一个实现标准观察者模式的依赖类。

因此,如果我们创建一个JavaScript类来管理我们的依赖项(它更接近Vue的处理方式),它可能看起来像这样:

请注意,我们现在存储匿名函数是subscribers而不是storage。我们现在调用的函数是depend而不是record,我们现在使用notify而不是replay。为了让这个运行:
它仍然有效,现在的代码感觉更可重用。唯一仍然感觉有点奇怪的是设置和运行target

⚠️ 问题

我们将为每个变量设置一个Dep类,并且很好地封装了创建需要监视更新的匿名函数的行为。也许一个watcher函数可能是为了处理这种行为。

所以不要这样调用:

(这只是上面的代码) 我们可以改为:

✅ 解决方案:观察者函数

在我们的Watcher函数中,我们可以做一些简单的事情:

如您所见,该watcher函数接受一个myFunc参数,将其设置为全局target属性,调用dep.depend()target添加到订阅者subscriber,执行target函数,然后重置target函数。

现在,当我们运行以下内容时:

您可能想知道为什么我们将target设为全局变量,而不是将其传递到我们需要的函数中。这将在我们的文章结尾处解释。

⚠️ 问题

我们有一个单独的Dep class,但我们真正想要的是每个变量都有自己的Dep。在继续之前,将数据设为对象属性。

假设每个属性(pricequantity)都有自己的内部Dep类。
现在我们执行时:
由于访问了data.price的值,我希望price属性的Dep类将存储在target的匿名函数推送到其subscriber 数组(通过调用dep.depend())。由于data.quantity被访问,我还希望quantity属性Dep类将存储在target的匿名函数推送到其subscriber 数组中。
如果我有另一个匿名函数,只是data.price被访问,我希望它只是推送到price属性Dep类。
pricesubscribers什么时候调用dep.notify()?我希望在price设置时调用它们。在文章的最后,我希望能够进入控制台并执行:
我们需要一些方式来挂钩数据属性(如pricequantity),所以当它被访问时,我们可以保存target到我们的 subscribers数组中,当它被更改时,运行存储在 subscribers数组中的函数。

✅ 解决方案:Object.defineProperty()

我们需要了解Object.defineProperty()函数,它是简单的ES5 JavaScript。它允许我们为属性定义gettersetter函数。在我向您展示如何在Dep类中使用它之前,将向您展示最基本的用法。

如您所见,它只记录两行。但是,它实际上没有getset任何值,因为我们过度使用了该功能。我们现在加回来吧。get()期望返回一个值,set()仍然需要更新一个值,所以让我们添加一个internalValue变量来存储我们当前的price值。

既然我们的getset工作正常,您认为将打印到控制台的是什么?

因此,当我们getset值时,我们可以获得通知。通过一些递归,我们可以为数据数组中的所有项运行它,对吧?

Object.keys(data)返回对象键的数组。

现在一切都有gettersetter,我们在控制台上看到了这一点。

🛠 将两种想法放在一起

当像这样的一段代码运行并getprice值时,我们想要price记住这个匿名函数(target)。这样,如果price被更改,或者set为新值,它将触发此函数以重新运行,因为它知道此行依赖于它。

Get =>记住当前匿名函数,当我们的值发生变化时,会再次运行它。

Set =>运行保存的匿名函数,我们的值随之改变。

或者就我们的Dep Class而言

Price accessed (get) =>调用dep.depend()以保存当前target

Price set =>调用dep.notify()price,重新运行全部targets

让我们结合这两个想法,并完成我们的最终代码。

现在看看我们执行时会发生什么。

正是我们所希望的!pricequantity确实都响应了!每当值pricequantity更新时,全部的代码都会重新运行。

Vue文档中的这个插图现在应该开始有意义了。

你看到那个美丽的紫色数据圈getters and setters了吗?看起来应该很熟悉!每个组件实例都有一个watcher实例(蓝色),它从getter(红线)收集依赖项。稍后调用setter时,它会通知watcher导致组件重新渲染。注释之后的图如下。

是的,这现在不是更有意义吗?

显然,Vue如何做到这一点更复杂,但你现在知道了基础知识。

⏪ 那么我们学到了什么?

  • 如何创建一个Dep类来收集依赖项(depend)并重新运行所有依赖项(notify)。
  • 如何创建一个Watcher程序来管理正在运行的代码,这些代码可能需要作为依赖项(target)被添加。
  • 如何使用Object.defineProperty()创建gettersetter

扩展

可以看看reactivity and proxies