Vue.js中不容易的数据响应实现

579 阅读7分钟

Vue.js中不容易的数据响应实现

引言

如果你对Vue.js中数据响应的简单实现还不熟悉的话,请在参看本文之前先起身看看我昨天写的读书笔记Vue.js响应式数据的简单实现,因为今天的内容可以说是在昨天内容的基础上对实现数据响应所作的进一步探究与完善,今天所有的代码演示也都是基于昨天的代码。

分支切换Bug

首先,我们来看看在数据响应中,什么叫做分支切换:

carbon (48).png

如上图所示,当obj.ok布尔值变化时,副作用函数effectFn所依赖的数据也会随之变化,即代码的执行会发生变化,这就是本文所指的分支切换

回顾一下昨天所实现的简单数据响应,你会发现分支切换会产生遗留的副作用函数。以上图代码举例,当data.ok == true时,副作用函数effectFn会与响应式数据对象objtext属性建立明确的联系:每次修改obj.text都会重新执行一次effectFn。到现在为止,好像还没发现什么问题。但如果,此时执行代码obj.ok = false修改了obj.ok的值时,意外发生了!

理论上,因为此时obj.ok的值为false,所以obj.text值的修改不应该再触发副作用函数effectFn的执行;但在实际测试中,函数effectFn却执行了。原因很简单:早在obj.ok == true的时候,effectFn就与obj.text建立了明确的联系,虽然后来将obj.ok的值修改为false,但是effectFnobj.text的联系并没有因此被切断。所以要想修复这个漏洞,应该在每次obj.ok被修改为false的时候,切断effectFnobj.text的联系。那,如何实现呢?

其实解决思路很简单:在每次副作用函数执行前,把它从所有与之关联的依赖集合中删除;而每当副作用函数执行完毕后,再与依赖项建立新的联系。顺着这个思路,我们需要重新定义副作用函数注册函数effect,在其中实现为当前的副作用函数关联一个数组用来储存所有包含该副作用函数的依赖集合,代码如下:

  • 重新定义effect

carbon (50).png

  • cleanup函数实现

carbon (51).png

  • track 函数修正-收集当前副作用函数的依赖集合

carbon (52).png

至此,我们实现的响应系统已经成功解决了分支切换导致的副作用函数遗留问题了,是不是感觉大功告成了?试一试运行测试一下?嘿,炸了,无限循环了!在前文中,我们修改了函数effecttrack,添加了函数cleanup,但是trigger函数我们没有碰过。请注意,每次对代码的修改都请考虑到是否会对其他部分造成影响。

这次的问题就出在trigger函数上,请看源码:

carbon (53).png

forEach遍历集合effects本来是没有问题的(这个effects就是前面的deps),但是,前文不是说了嘛:要想解决分支切换的问题,就要在当前副作用函数执行前先调用cleanup函数清除,即执行deps.delete(effect);而在副作用函数执行完毕后,当前副作用函数又会与依赖项重新建立联系,即执行了deps.add(effect)。这一删一添,就没完没了了,forEach永远都遍历不完,当然死循环啦~简单直观的代码体现如下:

carbon (54).png

如果你有经验的话,你会马上想到解决办法:另外构造一个集合Set()用来遍历就行了,trigger函数修改后代码如下:

carbon (55).png

副作用函数嵌套Bug

如果我在一个副作用函数里再嵌套一个副作用函数,如下所示:

carbon (57).png

理想情况下,我们希望每次修改obj.foo都会执行effectFn1以及effectFn2,而当修改obj.bar时只执行effectFn2。但是事实上是这样吗?读者大可自己试一试。事实上,当再次修改obj.foo的值时,只有effectFn2执行了,这显然是不符合预期的。

那问题出在哪里呢?不知道读者是否还记得我们先前是怎么储存当前副作用函数的,我们用的是activeEffect,显然,同一时刻activeEffect只能储存一个副作用函数,那当副作用函数发生嵌套时,内部的副作用函数就会覆盖掉外部的副作用函数,这会使原本应该与外层副作用函数建立联系的响应式数据收集到的副作用函数变成了内层的副作用函数。

怎么解决这个问题呢?我们可以定义一个模拟栈的数组effectStack,至于activeEffect则保持不变,这样,当发生副作用函数嵌套时,会先将外层副作用函数压入栈底,而最内层副作用函数会被压入栈顶,activeEffect则永远指向栈顶元素代码实现如下:

carbon (58).png

无限递归循环bug

我承认,我就是来找茬的,就看你能不能接住了,示例代码如下:

carbon (59).png

这种情况会直接导致栈溢出,分析如下:obj.foo++既会读取obj.foo的值触发track,从而将当前副作用函数收集到bucket中;紧接着又设置obj.foo的值,触发trigger执行刚放入bucket中的副作用函数。而很不巧的是,此时导致该现象的副作用函数本身都还没有执行完毕呢,这就导致了副作用函数无限调用自己,产生栈溢出。就好比用递归或者迭代写个算法题,结果没设置return出口,直接玩完。

解决思路也不难,有点类似节流:当trigger触发的副作用函数与当前正在执行的副作用函数相同,则不触发执行。修改后的trigger函数如下:

carbon (60).png

添加调度执行功能

可调度指的是当trigger动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。可调度性是响应式系统一个非常重要的特性,比如在Vue.js中要想实现“连续多次修改响应式数据但只触发一次更新”这个功能,支持可调度就是关键所在。可以直白地说,可调度就是让副作用函数受到用户的主动控制,根据用户的意愿来决定如何执行。

为了实现这个功能,我们可以在副作用函数注册的时候为每一个副作用函数添加一个options属性用来接受用户传入的额外值,从而实现让用户自己控制副作用函数执行的时机、次数以及方式。重新设计的effect函数如下:

carbon (61).png

trigger做相应的修改如下:

carbon (70).png

至此,我们就可以自行控制副作用函数执行的时机、次数以及方式,示例如下;

原代码:

carbon (71).png

现在,假设需求有变,需要先输出字符串'结束了',然后再输出数字2。为了实现这个需求,我们可以利用选项参数options设计一个调度器函数scheduler,示例代码如下:

carbon (77).png

这个scheduler调度器函数就是实现可调度性的精髓所在,用户通过重构scheduler函数,可以按照自己的需求来调度副作用函数的执行。

总结

至此,数据响应的简单实现及基本完善已经介绍完毕,在此附上完整代码:

carbon (69).png

当然,细心的你可能已经发现:此代码只能实现对对象类型数据的简单代理。事实上,要想实现Vue.js中完完整整的数据响应,只用简单的getset夹子是完全不够的,这是后话啦,以后再谈~

后话

我最近写的读数笔记都贴了不少《Vue.js设计与实现》书上的代码,其实感觉在文章里代码图片的量一多确实不讨喜。但是,我觉得这是有必要的。因为我觉得代码比文字表现的更加直观,甚至考虑到读者在看不懂或者还不熟悉的地方应该对着我附上的代码多敲几遍,毕竟,模仿学习在某种程度上是最高效的、入门速度最快的学习方式。

声明

  • 本文属于读书笔记一类,是作者在拜读 霍春阳 大佬的新作《Vue.js设计与实现》途中,以书中内容为蓝本,辅以个人微末的道行“填写”完成,推荐购书阅读,定有收获
  • 欢迎大佬斧正
  • 日更