两行代码下面的冰山,记一次给vue官方测试工具解决bug的过程

前几天遇到了一个vue-test-utils(后简称VTU)的bug,由于正处于vue3的rc阶段,VTU也仍然处于beta阶段,因此这是一个新bug,在issues列表里找到里好几个相似的问题,我在其中一个进行了回复。后面VTU的维护者发起了一次PR来讲明这个问题,详细能够看这里,他写了不少来讲明这个问题,以及这个问题可能的缘由,和这个问题不太好解决,并@咱们这些关注这个问题的人,但愿咱们一块儿讨论。html

正好是礼拜天,我闲着也是闲着,就默默的fork了代码来看看能不能解决一下。先说结果,我次日就提了解决的PR,而且最终在三天后这个PR被成功merge,而我也成为了VTU for vue3的第一个非主要维护者的贡献者。vue

Looks good! I will give this a little test myself and then merge it up. Thanks a lot, this is great first contribution!node

维护者老哥的原话。这老哥给我一顿猛夸,搞得我都有点很差意思: react

你如今确定在想,这确定是贡献了不少代码,解决了很复杂的问题,才会引来这样的夸奖吧。但事实可能会很是出乎你的意料,由于我解决这个bug的代码,只有两行!git

是的,我最终给VTU的贡献,只修改了两行代码,以及增长了一个测试用例。可能不少人要说了:“就这?”,这怕不是又是个凑数的PR吧,或者什么解决了拼写问题的PR,没什么技术含量。说实话我还真见过之前有公司用工具来刷拼写错误,来提高本身在开源项目中的PR占比的。github

固然,我这个确定不是的,因此接下去来说点有技术含量的。api

两行代码下面的冰山

咱们先来看一下这个问题。VTU又一个API,wrapper.findComponent,就是在当前wrapper节点下面找到某个组件,demo:数组

import { mount } from 'vue-test-utils'

const wrapper = mount(App)
const compWrapper = wrapper.findComponent({ name: 'YourComponentName' })
复制代码

这是VTU里面最经常使用的APi之一,可是这个API在以前倒是不能正常执行的,他不能正常执行的缘由维护者lmiller老哥在他的PR里面讲得很详细,原文看这里。归纳一下主要是两点:markdown

  1. 纯函数组件没有实例,因此没法获取到vm也就没法建立wrapper
  2. vue3的插槽会被编译成{defualt: () => [], other: () => []}这样的结构,没法直接获取子节点的vnode,进而没法获取vm也没法建立wrapper

对于第一点,由于不是我解决的主要问题,因此简单说一下,函数组件就是一个函数,其自己是没有this的,也没法使用composition api,因此自己是没有状态的,他自己就是一个render函数而已,因此天然没有vm对象。app

咱们重点关注第二点,我很早就写过一篇关于 vue3 中的 render 方法,你可能不知道这些来说解vue3中的关于createElement API的用法,只是看到的人很少,可能写得比较生僻吧。

这个问题跟上面说的文章里面的内容关联性很高,最主要的就是vue对于slots的表示,在vue3中,使用插槽的节点的API是这样的:

createElement(YourComponent, props, {
	default: () => [],
    othern: () => []
})
复制代码

也就是说YourComponent拿到的slots实际上是:

{
	default: () => [],
    othern: () => []
}
复制代码

这是一个很大的区别,为何这么说呢,由于正常的节点,不论是在vue2仍是react里面,你的组件拿到的children或者slots基本上都是已经建立好的节点(你强行本身传一个函数的除外),也就是说:

<YourComponent><Child /></YourComponent>
复制代码

正常来讲对于YourComponent来讲,他应该拿到的slots.default应该是一个对象。那么拿到对象和拿到函数有很大的区别么?是的,很是大。

createElement(YourComponent, props, {
	default: createElement(Child, props)
})

createElement(YourComponent, props, {
	default: () => createElement(Child, props)
})
复制代码

请你花30秒思考一下上面这两种方式的最大区别。

答案揭晓,那就是在这两个代码执行的时候,default指代的element是否已经建立。前者的default已是一个建立好的element,然后者的element并无建立,须要执行了函数以后才会建立。

换句话说,前者在执行建立YourComponent的节点的时候,他的children已经建立好了,然后者尚未。 这就是解决这个bug的关键。

vnode

看到这里你可能仍是很迷糊,这说的是个啥,跟这个bug有啥关系嘛?啊,到目前为止关系确实不大,前面都是铺垫,接下去咱们来说真正的问题。这个问题要从VTU的findComponent提及,这个API其实很简单,他从Appvnode开始向下遍历,遍历一遍从vue的vnode树中找到须要的vnode以及其对应的组件实例vm。而这其中的关键函数就是:

function findAllVNodes( vnode: VNode, selector: FindAllComponentsSelector ): VNode[] {
  const matchingNodes: VNode[] = []
  const nodes: VNode[] = [vnode]
  while (nodes.length) {
    const node = nodes.shift()!
    // match direct children
    aggregateChildren(nodes, node.children)
    if (node.component) {
      // match children of the wrapping component
      aggregateChildren(nodes, node.component.subTree.children)
    }
    if (node.suspense) {
      // match children if component is Suspense
      const { isResolved, fallbackTree, subTree } = node.suspense
      if (isResolved) {
        // if the suspense is resolved, we match its children
        aggregateChildren(nodes, subTree.children)
      } else {
        // otherwise we match its fallback tree
        aggregateChildren(nodes, fallbackTree.children)
      }
    }
    if (matches(node, selector) && !matchingNodes.includes(node)) {
      matchingNodes.push(node)
    }
  }

  return matchingNodes
}
复制代码

咱们主要关心这个while循环,aggregateChildren大体也就是根据传入的类型把找到的节点塞入到nodes数组里面,这个循环会直到vue的vnode树的全部节点都被遍历一遍以后(至少指望是这样)才会结束。

咱们能够看到他这里主要关心的是两个值,node.childrennode.component.subTree.children,那么这里就有两个概念须要同窗们理解了。

  • children
  • subTree

咱们先来看一个例子:

const CompA = {
	template: `<div><slot /></div>`
}

const CompB = {
	template: `<span>Hello</span>`
}

const App = {
	template: `<CompA><CompB /></CompA>`
}
复制代码

注:以上代码并不能正常执行,仅为讲解用

在这个例子中,对于CompA来讲,CompB是他的children,而div则是他的subTree。对于站在children的视角上来看,整个应用的结构以下:

<App>
  <CompA>
    <CompB />
  </CompA>
</App>
复制代码

在这种嵌套关系中,CompBCompAchildren

那么<div><slot /></div>去哪了呢?他其实只是CompA的渲染内容,因此他叫作CompAsubTree,即子树。而最终咱们把全部的节点列出来(包括组件节点和HTML节点)的话,咱们的应用实际上是这样的:

<App>
  <CompA>
    <div>
      <CompB>
        <span>Hello</span>
      </CompB>
    </div>
  </CompA>
</App>
复制代码

好,知道了childrensubTree的区别以后,接下去咱们就能够来说讲bug的缘由了。

解析

可能明眼人已经看出问题来了,源码中遍历的方式是:

aggregateChildren(nodes, node.children)
aggregateChildren(nodes, node.component.subTree.children)
复制代码

对于上面的demo,咱们从App开始。App没有children,因此直接看subTree,而这里直接遍历的是node.component.subTree.children而这种状况下,其实CompA正是AppsubTree,因此这里的代码直接无视了CompA,而转向了他的children 因此解决这个问题方法很是简单,增长一句代码:

aggregateChildren(nodes, [node.component.subTree])
复制代码

到这里呢,其实问题已经解决了。那么有同窗要问了,我上面说了一大堆slots想关的跟这个有关系吗?从结论出发,好像确实没啥关系,可是这个问题的解决过程当中,却免不了对于slots的理解和思考。

vue3为了渲染性能因此在编译模板的时候,把slots编译成函数,因此在上面的例子中,其实CompAchildren是:

{
	default: () => CompA
}
复制代码

因此咱们从CompAchildren出发,是找不到CompBvnode的。那咱们的代码是否是又变得有问题了呢?不会的,由于CompB咱们又能够从CompAdivchildren中找到,整个链路就是:

App -> subTree -> CompA -> subTree -> div -> children
复制代码

你再看一下实现的代码,你能看出这个链路是怎么造成的么?

而这个问题其实才是一直困扰维护者lmiller老哥的,他一直不清楚在slots被优化编译以后如何获取其vnode

不得不说这老哥真的热情 在我表示我没写注释是由于我英语通常般以后,他说若是我想写这个bug相关的文章或者一些vue原理相关的文章的话,他能够帮我修正,有机会的话我还真想试试。

小小总结一下

从去年开始我渐渐喜欢在github参与开源项目,前先后后也给挺多项目提过PR,有成功合并的也有由于一些缘由没有合并的,在这个过程当中让个人能力提高不少,可是最重要的是我对于代码实现细节拿捏有了更多的思考,再也不仅仅局限于实现一个功能,更可能是怎么更好得实现一个功能,以及思考将来可能的扩展需求并为之作好准备。在这个过程当中锻炼了我不少能力,包括但不限于:

  • 写单测,我如今不少状况下甚至会先写单测
  • 插件思惟,在实现功能以前就先想好若是我须要自定义扩展该怎么作
  • 站在用户角度思考API的设计,再也不为了实现功能而写代码
  • 英语😄

我建议各位都多去github逛逛,毕竟这仍是目前世界上最好的开源项目社区,即使有英语这个门槛,但早晚你是要克服,我能够这么说,至少10年内,世界最好的技术相关的文档仍然会是以英文为主。若是你但愿本身成为一个全面优质的技术人员,这个门槛早晚是要迈过去的。

另外参与开源项目真的很锻炼能力,可是你在国内的环境要参与这样的项目太难了,国内没有一个很好的开源环境,阿里系是国内在开源社区最活跃的,可是阿里的开源却又是很是功利性的,由于是KPI挂钩的,今年搞个开源项目,PPT好看拿了3.75,明年这个项目可能就不维护了,由于可能要换个项目撑KPI了。因此国内的环境仍是比较逐利的,更别说小公司可能连业务都来不及开发,哪有资源给你搞开源呢。

说了这么多,也就是小小感慨一下,最近拉了个群同窗愈来愈多,讨论多了也很但愿群里的同窗能快速提高。这种感情慢慢变成了但愿国内的技术氛围能变得更好,而不要太浮躁,太趋利,只有技术能帮你作出一些有意思的东西的时候,你才能一直保持兴趣并进而提高本身。

共勉!

相关文章
相关标签/搜索