likes
comments
collection
share

技术不革新带来的问题负担-剖析vue2.5源码的bug

作者站长头像
站长
· 阅读数 10

最近对一个项目接入Cypress,项目使用了比较旧的框架,在接入过程中遇到了挺多坑的。感慨技术不革新,没有跟着社区发展演进,会给开发带来问题和负担,影响开发。本篇文章就讲一下其中一个跟Vue2.5源码有关的坑。

问题背景

  1. 团队已经在一个A项目中落地实践了一套Cypress前端测试方案,A项目使用的Vue框架是2.6.12
  2. 现在有一个B项目,我需要把这套测试方案从A项目搬运到B项目,
  3. 虽然A项目和B项目都是使用Vue框架,但是B项目使用的是基于Vue@2.5.2魔改的@sxf/vue
  4. 在方案落地B项目过程中遇到了很多之前在A项目中没有遇到的问题,
  5. 运行测试用例时,控制台飘红,[Vue warn]: The setup binding preperty 'xxx' is already declared.

技术不革新带来的问题负担-剖析vue2.5源码的bug

排查

找出直接原因

先在打印报错的地方打个断点,分析下代码的执行逻辑,找出代码报错的直接原因。

技术不革新带来的问题负担-剖析vue2.5源码的bug

查看Call Stack调用栈,如下图:

技术不革新带来的问题负担-剖析vue2.5源码的bug

asVmProperty这个函数是怎么写的,如下图:

技术不革新带来的问题负担-剖析vue2.5源码的bug

asVmProperty这个函数(主要看if语句的判断),我们可以找到直接原因:前置判断如果propName在vm中已经有了,直接报错不允许重复定义。

为什么会重复定义

我们可以在if语句这里打一个条件断点,证明我们的思路是不是正确的。条件断点为propName === 'cardList'

重新执行后会发现条件断点确实触发了两次,第一次vm上没有cardList,第二次vm上有cardList,所以第一次没有报错,第二次的时候控制台报错了。

第一次触发条件断点的截图:

技术不革新带来的问题负担-剖析vue2.5源码的bug

第二次触发条件断点的截图:

技术不革新带来的问题负担-剖析vue2.5源码的bug

我们查看这两次触发断点的调用栈情况,发现不同点在于wrappedData这里:

技术不革新带来的问题负担-剖析vue2.5源码的bug

技术不革新带来的问题负担-剖析vue2.5源码的bug

我在这一步卡了挺久的,一直在看这个wrappedData是怎么执行的,这里其实我们把断点打在1952行$options.data = function wrappedData,就会容易分析很多,因为wrappedData里面用到的data其实是来自于函数外部的var data = $options.data(如果你不懂,要复习下js的作用域链面试题)。

技术不革新带来的问题负担-剖析vue2.5源码的bug

断点打在1952行,重新执行,你会发现,第一次data是undefined,第二次data是function。 因为同样的用例、同样的配置在A项目上跑是正常的,我当时还去A项目看了,发现A项目functionApiInit只会调用一次,data只会是undefined

也就是说问题出在functionApiInit这个函数应该只执行一次,但是它被执行了两次。

技术不革新带来的问题负担-剖析vue2.5源码的bug

技术不革新带来的问题负担-剖析vue2.5源码的bug

我们从调用栈上看是谁调用了functionApiInit,可以看到是callHook这个函数,hook参数传的是beforeCreate,然后handlers是一个长度为13的数组。

技术不革新带来的问题负担-剖析vue2.5源码的bug

看handlers数组是具体哪13个回调就可以发现端倪了,handlers数组中有两个functionApiInit函数。

从这里,我们可以看出,vue的生命周期钩子其实是一个handlers数组,vue会遍历数组来调用每一个handler。

而这里很显然,vue在合并生命周期钩子生成handlers数组的过程中,合并了重复的钩子函数,导致相同的函数被重复执行。

技术不革新带来的问题负担-剖析vue2.5源码的bug

我们接下来的目的很明确,就是要找出是在哪里合并出错的。

对vue源码熟悉的话,就会知道vue可以通过Vue.extendVue.mixin进行合并选项配置,而这两个实现逻辑都会去依赖mergeOptions这个函数,所以我们直接看mergeOption这个方法。mergeOptions 主要功能就是把 parent 和 child 这两个对象根据一些合并策略,合并成一个新对象并返回。

要找到生命周期钩子的合并策略,得看mergeField这个函数,当key传参是beforeCreatestrats[key]就是对应的合并策略实现(合并策略就是mergeHook函数的实现)。

技术不革新带来的问题负担-剖析vue2.5源码的bug

技术不革新带来的问题负担-剖析vue2.5源码的bug

我们在最后打一个条件断点,options.hasOwnProperty('beforeCreate') && options['beforeCreate'].length > 6

技术不革新带来的问题负担-剖析vue2.5源码的bug

当触发断点时,我们就可以从调用栈看到,是Cypress-vue2.esm.bundler.js中进行了合并操作。从这里就可以得知Cypress-vue2.esm.bundler.js的合并操作导致合并了重复的生命周期钩子。

技术不革新带来的问题负担-剖析vue2.5源码的bug

找到根因

为什么在A项目中没有报错,而在B项目中报错了呢?为什么Vue@2.6.12没有问题,Vue@2.5.2有问题呢?我们得对比vue@2.5.2vue@2.6.12的源码在mergeHook上的实现差异,如下图:

unpkg.com/browse/vue@…

技术不革新带来的问题负担-剖析vue2.5源码的bug

unpkg.com/browse/vue@…

技术不革新带来的问题负担-剖析vue2.5源码的bug

我们可以看出Vue@2.6.12Vue@2.5.2对于mergeHook的实现是有一些不一致的,Vue@2.6.12会对合并后的生命周期钩子做dedupeHooks函数调用,确保最终return的handlers数组里面是不会包含重复的生命周期钩子。

根因就是vue@2.5.2的合并生命周期钩子策略有bug!而这个bug在vue框架的演进中修复了。问题总算是排查出来了,整个排查过程还是十分耗时的,走了许多弯路,从上面的排查截图可以看到,有非常多的库/插件会去执行合并生命周期钩子的操作,比如vite-plugin-vue2@vue/composition-apicypress-vue2等等,最终的beforeCreate的handler都有十个左右。

还有就是技术不革新的问题,如果你团队的技术不做革新,一直停留在使用非常旧版本的vue,最终只会是被社区抛弃,现在开源技术基本都不会去考虑,去测试,去适配vue@2.5.2。使用旧技术引发的问题只能你自己去解决,如果求助社区,社区大概率会建议你升级版本。所以不要等问题发生了,再去排查、升级,等问题发生的时候,那时候是亡羊补牢,问题有可能是大问题,影响项目进度。我们要跟紧社区主流,及时革新,才不会被bug挨打。

给vue源码打补丁

根因找到了,那么对症下药,我们的解决方案就是给@sxf/vue打一个补丁。pnpm从v7.4开始内置了patch功能,我们可以直接用pnpm patch打补丁。

第一步执行pnpm patch @sxf/vue@1.0.2

技术不革新带来的问题负担-剖析vue2.5源码的bug

打开上面截图打印的文件路径,路径内容是@sxf/vue的源码拷贝,直接对源码进行修改,修改完成后保存。

然后执行pnpm patch-commit xxx,xxx是你刚刚访问修改的路径。

技术不革新带来的问题负担-剖析vue2.5源码的bug

patch-commit执行成功后,会在package.json中生成pnpm.patchDependencies配置,如下图

技术不革新带来的问题负担-剖析vue2.5源码的bug

项目根路径下会生成一个patches文件夹,如下图

技术不革新带来的问题负担-剖析vue2.5源码的bug

至此,我们对vue源码打patch就大功告成了。