技术不革新带来的问题负担-剖析vue2.5源码的bug
最近对一个项目接入Cypress,项目使用了比较旧的框架,在接入过程中遇到了挺多坑的。感慨技术不革新,没有跟着社区发展演进,会给开发带来问题和负担,影响开发。本篇文章就讲一下其中一个跟Vue2.5源码有关的坑。
问题背景
- 团队已经在一个A项目中落地实践了一套Cypress前端测试方案,A项目使用的Vue框架是
2.6.12
, - 现在有一个B项目,我需要把这套测试方案从A项目搬运到B项目,
- 虽然A项目和B项目都是使用Vue框架,但是B项目使用的是基于
Vue@2.5.2
魔改的@sxf/vue
, - 在方案落地B项目过程中遇到了很多之前在A项目中没有遇到的问题,
- 运行测试用例时,控制台飘红,
[Vue warn]: The setup binding preperty 'xxx' is already declared.
排查
找出直接原因
先在打印报错的地方打个断点,分析下代码的执行逻辑,找出代码报错的直接原因。
查看Call Stack
调用栈,如下图:
看asVmProperty
这个函数是怎么写的,如下图:
从asVmProperty
这个函数(主要看if语句的判断),我们可以找到直接原因:前置判断如果propName
在vm中已经有了,直接报错不允许重复定义。
为什么会重复定义
我们可以在if语句这里打一个条件断点,证明我们的思路是不是正确的。条件断点为propName === 'cardList'
重新执行后会发现条件断点确实触发了两次,第一次vm上没有cardList,第二次vm上有cardList,所以第一次没有报错,第二次的时候控制台报错了。
第一次触发条件断点的截图:
第二次触发条件断点的截图:
我们查看这两次触发断点的调用栈情况,发现不同点在于wrappedData
这里:
我在这一步卡了挺久的,一直在看这个wrappedData
是怎么执行的,这里其实我们把断点打在1952行$options.data = function wrappedData
,就会容易分析很多,因为wrappedData
里面用到的data其实是来自于函数外部的var data = $options.data
(如果你不懂,要复习下js的作用域链面试题)。
断点打在1952行,重新执行,你会发现,第一次data是undefined
,第二次data是function
。
因为同样的用例、同样的配置在A项目上跑是正常的,我当时还去A项目看了,发现A项目functionApiInit
只会调用一次,data只会是undefined
。
也就是说问题出在functionApiInit
这个函数应该只执行一次,但是它被执行了两次。
我们从调用栈上看是谁调用了functionApiInit
,可以看到是callHook
这个函数,hook参数传的是beforeCreate
,然后handlers是一个长度为13的数组。
看handlers数组是具体哪13个回调就可以发现端倪了,handlers数组中有两个functionApiInit
函数。
从这里,我们可以看出,vue的生命周期钩子其实是一个handlers数组,vue会遍历数组来调用每一个handler。
而这里很显然,vue在合并生命周期钩子生成handlers数组的过程中,合并了重复的钩子函数,导致相同的函数被重复执行。
我们接下来的目的很明确,就是要找出是在哪里合并出错的。
对vue源码熟悉的话,就会知道vue可以通过Vue.extend
和Vue.mixin
进行合并选项配置,而这两个实现逻辑都会去依赖mergeOptions
这个函数,所以我们直接看mergeOption
这个方法。mergeOptions
主要功能就是把 parent
和 child
这两个对象根据一些合并策略,合并成一个新对象并返回。
要找到生命周期钩子的合并策略,得看mergeField
这个函数,当key传参是beforeCreate
,strats[key]
就是对应的合并策略实现(合并策略就是mergeHook函数的实现)。
我们在最后打一个条件断点,options.hasOwnProperty('beforeCreate') && options['beforeCreate'].length > 6
。
当触发断点时,我们就可以从调用栈看到,是Cypress-vue2.esm.bundler.js
中进行了合并操作。从这里就可以得知Cypress-vue2.esm.bundler.js
的合并操作导致合并了重复的生命周期钩子。
找到根因
为什么在A项目中没有报错,而在B项目中报错了呢?为什么Vue@2.6.12
没有问题,Vue@2.5.2
有问题呢?我们得对比vue@2.5.2
跟vue@2.6.12
的源码在mergeHook上的实现差异,如下图:
我们可以看出Vue@2.6.12
和Vue@2.5.2
对于mergeHook的实现是有一些不一致的,Vue@2.6.12
会对合并后的生命周期钩子做dedupeHooks
函数调用,确保最终return的handlers数组里面是不会包含重复的生命周期钩子。
根因就是vue@2.5.2
的合并生命周期钩子策略有bug!而这个bug在vue框架的演进中修复了。问题总算是排查出来了,整个排查过程还是十分耗时的,走了许多弯路,从上面的排查截图可以看到,有非常多的库/插件会去执行合并生命周期钩子的操作,比如vite-plugin-vue2
、@vue/composition-api
、cypress-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
打开上面截图打印的文件路径,路径内容是@sxf/vue
的源码拷贝,直接对源码进行修改,修改完成后保存。
然后执行pnpm patch-commit xxx
,xxx是你刚刚访问修改的路径。
patch-commit执行成功后,会在package.json
中生成pnpm.patchDependencies
配置,如下图
项目根路径下会生成一个patches文件夹,如下图
至此,我们对vue源码打patch就大功告成了。
转载自:https://juejin.cn/post/7177566751792562233