33

[译] 以 Vue 为例,解释 JavaScript 的反应性

 5 years ago
source link: https://mp.weixin.qq.com/s/Wm5-3hsqre7ft_f0YBnoeg?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

qQn6je.jpg!web

作者|Gregg Pollack

译者|张卫滨

很多前端 JavaScript 框架(如 Angular、React 和 Vue)都有自己的反应性(Reactivity)引擎。理解反应式是什么以及如何运行能够提升你的开发水平,同时能够更高效地使用 JavaScript。在本文中,我们构建了与 Vue 源码相同的反应性功能。

反应性系统

当你第一次见到 Vue 的反应性系统时,你可能会感觉有些神奇。以下面这个简单的 Vue 应用为例:

iM32yun.jpg!web

qAbyEzV.jpg!web

不知道基于什么原因,Vue 能够知道 price 的值是否发生了变化,并且在变化的时候能够完成如下三件事情:

  • 更新 Web 页面 price 的值;

  • 重新计算乘法表达式 price * quantity ,并更新页面;

  • 再次调用 totalPriceWithTax 函数并更新页面。

但是,稍等,我似乎听到你想问,Vue 如何知道在 price 发生变化的时候都要更新哪些值,它又是如何跟踪所有的内容的呢?

VRfI3ye.jpg!web

这并不是 JavaScript 编程通常的运行方式。

如果这对你来说不那么直观,那么我们需要明白程序通常并不是按照这种方式来运行的。例如,如果我运行下面的样例代码:

Zv2Qnq3.jpg!web

你猜将会打印出什么内容呢?因为我们没有使用 Vue,它将会打印出 10 :

在 Vue 中,我们想要在 pricequantity 更新的时候, total 也进行更新。我们希望的输出是:

但令人遗憾的是,JavaScript 是过程性的,并不是反应式的,所以在现实代码并这并不可行。为了让 total 具有反应性,我们必须让 JavaScript 语言按照不同的方式来运行。

问题

我们需要记住如何计算 total ,这样才能在 pricequantity 发生变化的时候重新运行。

解决方案

首先,我们需要有某种方法告诉我们的应用,“我将要运行的代码是什么, 将它存储起来 ,在稍后某个时间点我可能需要你运行它”。然后,我们运行代码,在 pricequantity 变量发生变化的时候,再次运行存储的代码:

q2qe6bV.jpg!web

我们想到的办法可能就是将函数的内容记录下来,这样就能再次运行了:

2EZjeab.jpg!web

需要注意,我们在 target 变量中存储了一个匿名函数,然后调用了 record 函数。如果采用 ES6 的箭头语法的话,我还可以写成如下的形式:

record 的定义非常简单:

zEjMBb2.jpg!web

我们将 target 存储了起来(我们的示例中也就是 { total = price * quantity } ),这样的话,我们就能在随后运行它,可能会借助一个 replay 函数运行我们记录下来的所有内容。

aauIFz6.jpg!web

这样会遍历我们在 storage 数组中存储的所有匿名函数,并运行它们。

那么在我们的代码中,只需:

qa2eAjQ.jpg!web

非常简单,对吧?如果你想要通读代码并再次尝试的话,下面给出了完整的代码。

JFjQbu3.jpg!web

问题

我们可以按需继续记录 target,但是更好的方式是有一种健壮的方案,能够扩展我们的应用。我们可以使用一个类,让这个类维护一个 target 的列表,当需要它们重新运行的时候,这个类会得到通知。

解决方案:依赖类

要解决这个问题,我们将这些行为封装到单独的类中,使用一个依赖类( Dependency Class )来实现标准的观察者模式编程。

如果我们创建 JavaScript 类来管理依赖的话(类似于 Vue 的处理方式),它看起来可能会如下所示:

EJFV3iZ.jpg!web

需要注意,我们这里不再使用 storage ,而是使用 subscribers 来存储匿名函数,也不再使用 record 函数了,而是调用 depend ,同时使用 notify 代替了 replay 。要让它运行起来,只需:

ey2YVnu.jpg!web

它依然可以运行,而且我们的代码看上去具备了一定的可重用性。唯一感觉尤其诡异的地方就是设置和运行 target

问题

未来,每个类都会有一个 Dep 类,如果能将创建匿名函数观察更新的行为封装起来就更好了。接下来, watcher 函数将会出场来负责这种行为。

所以,我们将不会再调用:

uq22MjY.jpg!web

(这就是上面示例的代码)

相反,我们只需这样调用:

NN7fm2M.png!web

解决方案:Watcher 函数

在 Watcher 函数中,我们可以做几件很简单的事情:

3meU733.jpg!web

可以看到, watcher 函数接受一个 myFunc 变量,将其作为我们的全局 target 属性,调用 dep.depend() ,将会以订阅者的形式添加我们的 target,调用 target 函数并重置 target

现在,我们可以运行下面的代码:

bUnEnuF.jpg!web

你可能会想,我们为什么要将 target 实现为全局变量,而不是将其传递给所需的函数。这里有一定的原因,在本文结束的时候,相信你就明白了。

问题

我们现在有了一个 Dep 类,但是我们真正想要实现的是每个变量都有自己的 Dep。在进行下一步讲解之前,我们先将它们放到属性中。

我们先假设每个属性( pricequantity )都有其自己的内部 Dep 类。

BnuIZ3U.jpg!web

现在,当我们运行:

Qba63eV.jpg!web

因为访问到了 data.price 的值,所以我希望 price 属性的 Dep 类要将我们的匿名函数(存储在 target 中)放到它的订阅数组中(通过调用 dep.depend() )。因为 data.quantity 也被访问到了,所以我希望 quantity 属性的 Dep 类要将该匿名函数(存储在 target 中)放到它的订阅数组中:

Ena2U3z.jpg!web

如果我还有其他的匿名函数只访问 data.price 的话,我希望要将这个函数放到 price 属性的 Dep 类中。

jimY7jM.jpg!web

那么,我该在何时为 price 的订阅者调用 dep.notify() 呢?答案是为 price 赋值的时候。在本文结束的时候,我希望能够在命令行中实现如下的效果:

zy22YzY.jpg!web

我们希望能有某种方式嵌入到数据属性中( pricequantity ),这样的话,当属性被访问的时候,能够将 target 存储到订阅者数组中,当属性变更时,能够运行存储在订阅者数组中的函数。

解决方案:Object.defineProperty()

我们需要学习 ES 5 JavaScript 所提供 Object.defineProperty() 函数。它允许我们为属性定义 getter 和 setter 函数。在展示如何与 Dep 类协作之前,我们看一下它的基础用法。

VvErQbQ.jpg!web

73MVJ3i.png!web

可以看到,这里只是打印了两条日志。但是,它并没有实际 getset 值,这是因为我们将功能覆盖掉了。现在,我们将功能添加回来。 get() 预期要返回一个值,而 set() 依然要更新值,所以我们添加一个 internalValue 变量来存储当前的 price 值。

r2UFNv3.jpg!web

我们的 getset 都能正常运行了,你觉得控制台的打印信息会是什么呢?

Eju2Yj3.png!web

所以,当取值和设置值的时候,我们有了一种得到提醒的方法。借助一些递归,我们就可以将其用到数据数组的所有条目中了。

值得一提的是, Object.keys(data) 能够返回对象中 key 所组成的数组。

Vjm6Fri.jpg!web

现在,所有的属性都有 getter 和 setter 了,我们来看一下控制台:

7NnYjy7.jpg!web

将这两个理念组合在一起

当这样的代码运行并尝试 get price 属性的值时,我们希望 price 能够记住这个匿名函数( target )。通过这种方式,如果 price 发生了变化,或者被 set 了一个新的值,这个函数就能重新运行,因为它能够知道这行代码依赖该属性。所以,你可以按照如下的方式来思考。

Get=>记住该匿名函数,当值发生变化的时候我们会重新运行。

Set=>运行保存的匿名函数,我们的值就会发生变化。

或者,在 Dep Class 的场景下:

Price 访问 (get)=>调用 dep.depend() 保存当前的 target ;

Price set=>调用 price 的 dep.notify() ,重新运行所有的 target

接下来,我们将这两个理念组合起来,并看一下最终的代码。

mimyei3.jpg!web

在我们运行的时候,看一下控制台的输出:

u6NVneQ.jpg!web

完全符合我们的预期!现在 pricequantity 都是反应式的了。当 pricequantity 的值更新时,我们的代码完全重新运行了。

Vue 文档中的图示对你来说应该就非常清晰了。

iaQZb2j.jpg!web

看到漂亮的 Data 圆圈中的 getters 和 setters 了吗?它看起来似曾相识!每个组件实例都有一个 watcher (蓝色圆圈),它会从 getter 中收集依赖(红线)。当 setter 随后被调用时,它会 通知 watcher,从而会导致组件的重新渲染。如下的图片添加了一些我自己的注释。

UZvaiey.jpg!web

现在,是不是感觉一目了然了呢?

当然,Vue 底层的处理要更复杂,但是你现在已经掌握了它的基础。

我们学到了什么呢?

  • 如何创建 Dep 来收集依赖(depend)并重新运行所有的依赖(notify);

  • 如何创建 watcher 来管理我们正在运行的代码,这些代码可能需要作为依赖添加进来(target);

  • 如何使用 Object.defineProperty() 来创建 getter 和 setter。

原文链接

https://medium.com/vue-mastery/the-best-explanation-of-javascript-reactivity-fea6112dd80d

课程推荐

不管你愿不愿相信,对于互联网技术人来说,“带团队”不再是一个可选项,而是迟早都要面对的事儿。推荐极客时间《技术管理实战 36 讲》专栏,前百度最佳经理人刘建国为你解惑团队 leader 的 36 个场景,传授打造高效团队的 13 种方法,分享十年管理的“战地笔记”。

现在订阅,立享限时福利:

福利一: 限时优惠价¥45,原价¥68,8 月 25 日恢复原价

福利二: 每邀请一位好友购买,你可获得 12 元现金返现,好友可返 6 元。多邀多得,上不封顶,随时提现(提现流程:极客时间 App - 我的 - 分享有赏)

e6zyqu3.jpg!web

阅读原文 ,免费试读或订阅!


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK