前幾天遇到了一個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
vm
也就沒法建立wrapper
{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的關鍵。
看到這裏你可能仍是很迷糊,這說的是個啥,跟這個bug有啥關係嘛?啊,到目前爲止關係確實不大,前面都是鋪墊,接下去咱們來說真正的問題。這個問題要從VTU的findComponent
提及,這個API其實很簡單,他從App
的vnode
開始向下遍歷,遍歷一遍從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.children
和node.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>
複製代碼
在這種嵌套關係中,CompB
是CompA
的children
。
那麼<div><slot /></div>
去哪了呢?他其實只是CompA
的渲染內容,因此他叫作CompA
的subTree
,即子樹。而最終咱們把全部的節點列出來(包括組件節點和HTML節點)的話,咱們的應用實際上是這樣的:
<App>
<CompA>
<div>
<CompB>
<span>Hello</span>
</CompB>
</div>
</CompA>
</App>
複製代碼
好,知道了children
和subTree
的區別以後,接下去咱們就能夠來說講bug的緣由了。
可能明眼人已經看出問題來了,源碼中遍歷的方式是:
aggregateChildren(nodes, node.children)
aggregateChildren(nodes, node.component.subTree.children)
複製代碼
對於上面的demo,咱們從App
開始。App
沒有children
,因此直接看subTree
,而這裏直接遍歷的是node.component.subTree.children
,而這種狀況下,其實CompA
正是App
的subTree
,因此這裏的代碼直接無視了CompA
,而轉向了他的children
。 因此解決這個問題方法很是簡單,增長一句代碼:
aggregateChildren(nodes, [node.component.subTree])
複製代碼
到這裏呢,其實問題已經解決了。那麼有同窗要問了,我上面說了一大堆slots
想關的跟這個有關係嗎?從結論出發,好像確實沒啥關係,可是這個問題的解決過程當中,卻免不了對於slots
的理解和思考。
vue3爲了渲染性能因此在編譯模板的時候,把slots
編譯成函數,因此在上面的例子中,其實CompA
的children
是:
{
default: () => CompA
}
複製代碼
因此咱們從CompA
的children
出發,是找不到CompB
的vnode
的。那咱們的代碼是否是又變得有問題了呢?不會的,由於CompB
咱們又能夠從CompA
的div
的children
中找到,整個鏈路就是:
App -> subTree -> CompA -> subTree -> div -> children
複製代碼
你再看一下實現的代碼,你能看出這個鏈路是怎麼造成的麼?
而這個問題其實才是一直困擾維護者lmiller老哥的,他一直不清楚在slots
被優化編譯以後如何獲取其vnode
。
不得不說這老哥真的熱情 在我表示我沒寫註釋是由於我英語通常般以後,他說若是我想寫這個bug相關的文章或者一些vue原理相關的文章的話,他能夠幫我修正,有機會的話我還真想試試。
從去年開始我漸漸喜歡在github參與開源項目,前先後後也給挺多項目提過PR,有成功合併的也有由於一些緣由沒有合併的,在這個過程當中讓個人能力提高不少,可是最重要的是我對於代碼實現細節拿捏有了更多的思考,再也不僅僅侷限於實現一個功能,更可能是怎麼更好得實現一個功能,以及思考將來可能的擴展需求併爲之作好準備。在這個過程當中鍛鍊了我不少能力,包括但不限於:
我建議各位都多去github逛逛,畢竟這仍是目前世界上最好的開源項目社區,即使有英語這個門檻,但早晚你是要克服,我能夠這麼說,至少10年內,世界最好的技術相關的文檔仍然會是以英文爲主。若是你但願本身成爲一個全面優質的技術人員,這個門檻早晚是要邁過去的。
另外參與開源項目真的很鍛鍊能力,可是你在國內的環境要參與這樣的項目太難了,國內沒有一個很好的開源環境,阿里系是國內在開源社區最活躍的,可是阿里的開源卻又是很是功利性的,由於是KPI掛鉤的,今年搞個開源項目,PPT好看拿了3.75,明年這個項目可能就不維護了,由於可能要換個項目撐KPI了。因此國內的環境仍是比較逐利的,更別說小公司可能連業務都來不及開發,哪有資源給你搞開源呢。
說了這麼多,也就是小小感慨一下,最近拉了個羣同窗愈來愈多,討論多了也很但願羣裏的同窗能快速提高。這種感情慢慢變成了但願國內的技術氛圍能變得更好,而不要太浮躁,太趨利,只有技術能幫你作出一些有意思的東西的時候,你才能一直保持興趣並進而提高本身。
共勉!