兩行代碼下面的冰山,記一次給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了。因此國內的環境仍是比較逐利的,更別說小公司可能連業務都來不及開發,哪有資源給你搞開源呢。

說了這麼多,也就是小小感慨一下,最近拉了個羣同窗愈來愈多,討論多了也很但願羣裏的同窗能快速提高。這種感情慢慢變成了但願國內的技術氛圍能變得更好,而不要太浮躁,太趨利,只有技術能幫你作出一些有意思的東西的時候,你才能一直保持興趣並進而提高本身。

共勉!

相關文章
相關標籤/搜索