6.7K Star 的知名開源項(xiàng)目源碼,該怎么看?
源碼解讀??讀懂開源??精選好文
大家好,我是皮湯。最近程序員巴士學(xué)習(xí)交流群里有小伙伴想要了解一下如何看源碼,正好最近有一點(diǎn)心得感悟,之前也寫過一篇實(shí)際跑通 NaiveUI 源碼的文章:尤大都推薦的組件庫是如何開發(fā)出來的?[1] 源碼的經(jīng)驗(yàn),來給大家分享一下。
心理認(rèn)知要到位
首先要認(rèn)識到,看源碼是一個開始比較枯燥、同時時間跨度相對比較長的一個過程。所以看源碼的第一步是找到自己想要了解領(lǐng)域、或者自己所在業(yè)務(wù)領(lǐng)域高度相關(guān)的項(xiàng)目,并且在這個領(lǐng)域比較出名,且維護(hù)活躍。
打個比方,皮湯我因?yàn)槭且幻岸耍岸诉@個領(lǐng)域有很多新興的內(nèi)容,如 Unbundled 方案 Vite,新興框架 Svelte,新匯編語言 WebAssembly,CSS 工程化方案 TailwindCSS,組件庫如抖音很火的開源庫 Semi Design[2]、或者社區(qū)比較火的 Vue3 組件庫 NaiveUI 等。
而皮湯我一直對組件庫、CSS 方向比較癡迷,且是組內(nèi)最近負(fù)責(zé)前端工程化 CSS 方面基建的負(fù)責(zé)人之一,所以讓我去研究一個組件庫的源碼,如 NaiveUI,那么我是很有興趣、動力和理由的,而這也是驅(qū)動你啃下一個源碼的核心驅(qū)動力之一。

理解高潮 MVP
其次我們看源碼要有一定的技巧,復(fù)雜如 React[3],可以算作一個簡單的操作系統(tǒng)了,如果你上來通過比較簡單粗暴的從代碼入口開始,一路打斷點(diǎn)了解源碼,那你再怎么堅(jiān)持也會想吐的。

那這里的技巧是什么呢?就像我們互聯(lián)網(wǎng)創(chuàng)業(yè)一樣,如果你有一個大而全的點(diǎn)子,但是你的第一步肯定不是找一個空曠的屋子,備好半年的糧食,準(zhǔn)備幾臺電腦,然后高強(qiáng)度開發(fā)幾個月,然后祈求一問世就驚艷世人。一個是這種情況非常少見,二個是你也得有堅(jiān)持幾個月的資本和耐心。

而在創(chuàng)業(yè)領(lǐng)域一個比較知名且流傳深遠(yuǎn)的技巧就是 MVP,即最小可行性產(chǎn)品,你需要先做出一個非常小的,剛好能夠使用以及能夠測試你想法的產(chǎn)品,然后快速推向市場,接收用戶反饋,接著跟進(jìn)用戶反饋,不斷迭代產(chǎn)品,滿足用戶需求,直至達(dá)到 PMF(產(chǎn)品與市場匹配),這個時候你基本上就可以找投資,進(jìn)行規(guī)模化,然后就是融資、去納斯達(dá)克敲鐘。

所以對應(yīng)到我們看源碼這個領(lǐng)域,第二個需要注意的,就是你需要找到一個開源項(xiàng)目能跑起來的最小 MVP,去除其他繁雜的依賴,最最核心的流程與機(jī)制。這個能夠幫助你理解項(xiàng)目核心的 MVP,我稱之為 高潮 MVP -- 即如果你能夠跑通一個項(xiàng)目這樣的 MVP,那么你內(nèi)心會異常幸福,感覺自己成就慢慢,興奮達(dá)到了高潮,而接下來其他內(nèi)容、分支基本上就是復(fù)用這套 MVP,往上面添磚加瓦,補(bǔ)齊一些兼容細(xì)節(jié)等。

開始之前
那么對于 NaiveUI 來說,它的高潮 MVP 是什么呢?
我們首先打開它的官網(wǎng):

點(diǎn)開開始使用:

作為一個組件庫來說,它一般需要談?wù)撟约旱膬r值觀、設(shè)計(jì)原則、定制方式,ICON 圖標(biāo)相關(guān)的內(nèi)容,但是這對于你搞懂這份源碼其實(shí)沒有多大幫助,所以需要略去這些干擾項(xiàng)。
讓我們再來看看它的源碼倉庫:

確保這個庫使用何種語言編寫,這樣你在看源碼之前可以先衡量你當(dāng)前的知識儲備是否能夠支撐你看懂這份源碼,當(dāng)然如果你沒有對應(yīng)的支持儲備,但是又堅(jiān)持想要看這份源碼,那么你首先應(yīng)該考慮根據(jù)它使用的語言,提前進(jìn)行語言學(xué)習(xí)儲備。
看透本質(zhì)
讓我們回到 NaiveUI 的官網(wǎng):

可以看到,對于一個 “組件庫” 來說,實(shí)際上最最基礎(chǔ)的其實(shí)就是 “組件”,而組成 “組件” 的背后則需要一系列更加基礎(chǔ)的元素,如顏色、間距、邊框、背景、字體等。
那么我們的目標(biāo)是不是很明確了呢?把一個 “按鈕” 組件拿下,理解能夠完整使用到這樣一個按鈕背后所需要的所有必須的流程、知識、細(xì)節(jié),那么針對其他的組件,基本上 90 % 的邏輯可以復(fù)用,只需要理解剩余 10% 特定功能需求就可以搞懂。
類似下面這張冰山圖:

冰山之下就屬于那 90%,我們基于一個看似簡單的 “按鈕” 組件,來梳理整個組件庫的核心流程,就可以幫助我們快速、精準(zhǔn)的搞懂整份源碼,所以我們的高潮 MVP 就是搞懂一個 “按鈕” 組件的全流程。
了解上下文
理解我們的高潮 MVP 目標(biāo)是什么了之后,接下來就是帶著這個目標(biāo)先詳細(xì)讀一下文檔中關(guān)于 Button 的所有相關(guān)說明,可以看到這個按鈕包含如下內(nèi)容:

通過右側(cè)的目錄,了解到一個按鈕首先會有基礎(chǔ)內(nèi)容,包含 default、primary、info、success、warning 和 error 這幾類,然后需要處理:
邊框相關(guān):虛線 尺寸相關(guān):尺寸、行政 顏色:自定義顏色 狀態(tài):文本、禁用、加載中 事件

上述表明了這個 Button 可以達(dá)到的效果,可以完成的操作,了解之后,接著可以了解按鈕相關(guān)的使用 API,通過 API 以及可以達(dá)到的效果,我們大致可以理解這個按鈕接收的輸入和輸出有哪些。
一個一個使用 Button 的例子長什么樣子:
??
????Default
????type="primary">Primary
????type="info">Info
????type="success">Success
????type="warning">Warning
????type="error">Error
??
了解如何開啟項(xiàng)目
通常開源項(xiàng)目比較方便的一點(diǎn)是它會有詳細(xì)的文檔,同時它非常渴望有貢獻(xiàn)者加入,所以會有完善的 貢獻(xiàn)指南,比如 NaiveUI 的貢獻(xiàn)指南[4]如下:

通過貢獻(xiàn)指南,你能夠了解如何安裝依賴、處理一些啟動項(xiàng)目的問題,能夠把項(xiàng)目跑起來進(jìn)行調(diào)試,這通常是你了解整個代碼運(yùn)行過程的初次體驗(yàn)。
理解目標(biāo)項(xiàng)目的項(xiàng)目結(jié)構(gòu)
通常你到這個步驟時,你應(yīng)該需要知道如下內(nèi)容:
你已經(jīng)理解了你的目標(biāo),高潮 MVP 是什么 你理解了你目標(biāo)內(nèi)容作為一個功能特性,它的輸入和輸出是什么 你理解此項(xiàng)目的技術(shù)棧是什么,如何把項(xiàng)目跑起來
對應(yīng)到 NaiveUI 我們的這三點(diǎn)分別如下:
高潮 MVP:跑通一個 Button 并能夠使用,保有和現(xiàn)有 Button 一樣的特性,接收一樣的輸入,產(chǎn)生一樣的輸出 Button 包含邊框、尺寸、顏色、狀態(tài)、事件等相關(guān)的內(nèi)容,輸入這些參數(shù),產(chǎn)出對應(yīng)條件下的輸出 項(xiàng)目的技術(shù)棧是 Vue3、TypeScript,構(gòu)建工具是 Vite,同時使用了 CSS BEM 框架 CSS Render[5],同時包管理工具使用 pnpm
理解這三點(diǎn)之后,接下來我們就需要對照著源碼來理解一下整份文件目錄,了解各個目錄之前的依賴關(guān)系,見下圖。

我們可以先了解一下大致每個文件夾是干什么的:
src:這個是主要放組件庫相關(guān)的組件代碼,以及導(dǎo)出一些國際化、樣式、主題定制相關(guān)的內(nèi)容,一般是一個開源項(xiàng)目的核心開發(fā)目錄scripts:一些運(yùn)行代碼、構(gòu)建、發(fā)版相關(guān)的腳本邏輯theme:則為 NaiveUI 內(nèi)置的默認(rèn)主題,類似這種組件庫一般都允許用戶自定義主題,整個 NaiveUI 各個組件在使用各種 UI 屬性時都是遵從這套主題進(jìn)行設(shè)置的,也就是可以修改 theme 里面的內(nèi)容,或者自己完全自定義一套主題.github、.husky等都是一些配置,無需過多關(guān)注,可以直接加入到你的 MVP 模板工程里playground是用于此時代碼在各種環(huán)境下運(yùn)行的支持代碼,如 SSR 等demo則是引入src相關(guān)內(nèi)容用于展示組件實(shí)際效果的網(wǎng)站例子,實(shí)際上對于 NaiveUI 也就是我們之前看到的文檔官網(wǎng)其他的如 build、design-notes等是構(gòu)建產(chǎn)物,或者一些主題設(shè)計(jì)的筆記等,基本上不屬于本次源碼需要閱讀的部門,看興趣的同學(xué)可以看看
然后就是一些用于各種工程化配置的文件如:
.prettierrc:Prettier 相關(guān).gitignore:Git 相關(guān).eslintrc.js:ESLint 相關(guān)babel.config.js:Babel 相關(guān)jest.config.js:Jest 測試相關(guān)postcss.config.js:處理 CSS 相關(guān)tsconfig.xx.json處理 TypeScript 相關(guān)vite.config.js:Vite 構(gòu)建工具相關(guān)的配置
以及一些和項(xiàng)目強(qiáng)相關(guān),用于了解整個想法發(fā)展上下文的 CHANGELOG.xx.md ,還有我們之前提到的用于跑通代碼的 CONTRIBUTING 貢獻(xiàn)指南。

有點(diǎn)看懵了。??
創(chuàng)建你的高潮 MVP 項(xiàng)目
了解了整個 NaiveUI 的項(xiàng)目目錄結(jié)構(gòu)之后,我們就可以著手創(chuàng)建我們的高潮 MVP 項(xiàng)目了,但在這之前我們可以再進(jìn)行一波簡化,即我們有些內(nèi)容可以不要:
針對目錄的來說
.github、.husky、playground、scripts這種我們可以不要,我們只需要測試最基礎(chǔ)的環(huán)境,以及在開發(fā)時可以跑通即可theme這種只是整個 NaiveUI 遵循的設(shè)計(jì)體系,在其他部分會遵循這個體系,但是不會直接引用,所以我們也可以不要這樣我們只剩下 demo和src,而更近一步,我們可以把 demo 做到 src 里面,整個 src 我們將其職責(zé)變?yōu)楦叱?MVP 網(wǎng)站入口,然后原剩下的 src 下面的代碼則用于導(dǎo)入到 src 入口文件里面使用針對配置文件來說:
測試相關(guān)的,Jest 等我們并不需要 TypeScript 相關(guān)的,我們后續(xù)可以迭代,不用引入不必要的復(fù)雜度以及類型體操 ESLint 和 Prettier 等我們也可以不需要,依賴于編輯器默認(rèn)的格式化就可,當(dāng)然引入這兩個到我們初始的高潮 MVP 項(xiàng)目里也不礙事
經(jīng)過簡化之后,我們的高潮 MVP 項(xiàng)目就只需要如下幾個文件了:
構(gòu)建項(xiàng)目和提供開發(fā)服務(wù)器的 Vite 相關(guān)內(nèi)容: vite.config.js用于提供語法轉(zhuǎn)譯的 babel.config.js項(xiàng)目依賴文件 package.json用于跑通項(xiàng)目的主要代碼 src以及index.html入口模板
目錄結(jié)構(gòu)如下:
.
├──?babel.config.js
├──?index.html
├──?node_modules
├──?package.json
├──?public
├──?src
├──?vite.config.js
└──?yarn.lock
很精簡,沒有多余繁雜的內(nèi)容對吧?同時也非常易懂。

這些剩下要創(chuàng)建的文件內(nèi)容,從 NaiveUI 的工程目錄里面 Copy 過來,然后安裝對應(yīng)的依賴即可。
跑通流程
當(dāng)我們根據(jù)源碼庫創(chuàng)建了我們的高潮 MVP 項(xiàng)目之后,現(xiàn)在應(yīng)該可以跑起來了,只不過內(nèi)容只是一個簡單的 Button,因?yàn)闉榱丝焖倥芷饋眄?xiàng)目,我們的入口文件 src/App.vue 會如下:
??hello?tuture
而對應(yīng)的 src/components/TButton.vue 如下:
??

接下來我們就嘗試一遍了解 NaiveUI 的代碼,一遍將這些主干代碼遷移到我們的高潮 MVP 項(xiàng)目中來,然后確保遷移過程中能夠持續(xù)跑起來,雖然我們可能會遇到有時候一個依賴需要大量的前置依賴,所以需要遷移一大段代碼才能將項(xiàng)目跑起來。

找到核心入口
我們要完成一個 Button 的所有前置依賴,只需要去到 NaiveUI 對應(yīng)的工程目錄文件里面,找到 Button 對應(yīng)的代碼,如下:

其實(shí)解析一下組件文件的代碼,就是下面幾部分:
前置的 import 依賴
定義組件
defineComponent組件里面處理 props 傳入與使用、自身狀態(tài)的定義與使用 模板代碼 導(dǎo)出組件
而上圖代碼中的所有和 TS 定義相關(guān)的內(nèi)容我們都是不需要的,所以可以刪除 ButtonProps 、NativeButtonProps 、MergedProps 、XButton 這些類型定義相關(guān)的內(nèi)容。
而導(dǎo)入部分涉及到類型定義相關(guān)的我們也可以刪除掉:
import?type?{?ThemeProps?}?from?'../../_mixins'
import?type?{?BaseWaveRef?}?from?'../../_internal'
import?type?{?ExtractPublicPropTypes,?MaybeArray?}?from?'../../_utils'
import?type?{?ButtonTheme?}?from?'../styles'
import?type?{?Type,?Size?}?from?'./interface'
刪除完這些無關(guān)的代碼之后,我們的代碼還剩下那些內(nèi)容呢?
導(dǎo)入依賴部分:
import?{
??h,
??ref,
??computed,
??inject,
??nextTick,
??defineComponent,
??PropType,
??renderSlot,
??CSSProperties,
??ButtonHTMLAttributes
}?from?'vue'
import?{?useMemo?}?from?'vooks'
import?{?createHoverColor,?createPressedColor?}?from?'../../_utils/color/index'
import?{?useConfig,?useFormItem,?useTheme?}?from?'../../_mixins'
import?{
??NFadeInExpandTransition,
??NIconSwitchTransition,
??NBaseLoading,
??NBaseWave
}?from?'../../_internal'
import?{?call,?createKey?}?from?'../../_utils'
import?{?buttonLight?}?from?'../styles'
import?{?buttonGroupInjectionKey?}?from?'./ButtonGroup'
import?style?from?'./styles/button.cssr'
import?useRtl?from?'../../_mixins/use-rtl'
組件聲明部分:
const?Button?=?defineComponent({
??name:?'Button',
??props:?buttonProps,
??setup(props)?{
????//?定義組件狀態(tài)
????const?selfRef?=?refnull>(null)
????const?waveRef?=?ref null>(null)
????const?enterPressedRef?=?ref(false)
????
????//?使用?Props?或注入全局狀態(tài)
????const?NButtonGroup?=?inject(buttonGroupInjectionKey,?{})
????const?{?mergedSizeRef?}?=?useFormItem(...)
????const?mergedFocusableRef?=?computed(()?=>?{...})
????
????//?定義組件事件處理
????const?handleMouseDown?=?(e:?MouseEvent):?void?=>?{...}
????const?handleClick?=?(e:?MouseEvent):?void?=>?{...}
????const?handleKeyUp?=?(e:?KeyboardEvent):?void?=>?{...}
????const?handleKeyDown?=?(e:?KeyboardEvent):?void?=>?{...}
????const?handleBlur?=?():?void?=>?{...}
????
????//?處理組件的主題,獲取該?Button?組件在整個全局設(shè)計(jì)系統(tǒng)中的對應(yīng)樣式
????const?{?mergedClsPrefixRef,?NConfigProvider?}?=?useConfig(props)
????const?themeRef?=?useTheme(...)
????const?rtlEnabledRef?=?useRtl(...)
????
?????//?將自身狀態(tài)、全局狀態(tài)相關(guān)的主題樣式、各個?CSS?屬性的值、事件相關(guān)的內(nèi)容處理之后返回給模板使用
?????return?{
??????selfRef,
??????waveRef,
??????mergedClsPrefix:?mergedClsPrefixRef,
??????mergedFocusable:?mergedFocusableRef,
??????mergedSize:?mergedSizeRef,
??????showBorder:?showBorderRef,
??????enterPressed:?enterPressedRef,
??????rtlEnabled:?rtlEnabledRef,
??????handleMouseDown,
??????handleKeyDown,
??????handleBlur,
??????handleKeyUp,
??????handleClick,
??????customColorCssVars:?computed(()?=>?{...}),
??????cssVars:?computed(()?=>?{...})
????}
???},
???render()?{
???//?處理各種組件相關(guān)的樣式渲染、事件處理相關(guān)的內(nèi)容,這里的樣式渲染對應(yīng)著在文檔里提到的?Button?可以呈現(xiàn)的狀態(tài)和能處理的操作
????const?{?$slots,?mergedClsPrefix,?tag:?Component?}?=?this
????return?(
??????<Component
????????ref="selfRef"
????????class={[
??????????`${mergedClsPrefix}-button`,
??????????`${mergedClsPrefix}-button--${this.type}-type`,
??????????{
????????????[`${mergedClsPrefix}-button--rtl`]:?this.rtlEnabled,
????????????[`${mergedClsPrefix}-button--disabled`]:?this.disabled,
????????????[`${mergedClsPrefix}-button--block`]:?this.block,
????????????[`${mergedClsPrefix}-button--pressed`]:?this.enterPressed,
????????????[`${mergedClsPrefix}-button--dashed`]:?!this.text?&&?this.dashed,
????????????[`${mergedClsPrefix}-button--color`]:?this.color,
????????????[`${mergedClsPrefix}-button--ghost`]:?this.ghost?//?required?for?button?group?border?collapse
??????????}
????????]}
????????tabindex={this.mergedFocusable???0?:?-1}
????????type={this.attrType}
????????style={this.cssVars?as?CSSProperties}
????????disabled={this.disabled}
????????onClick={this.handleClick}
????????onBlur={this.handleBlur}
????????onMousedown={this.handleMouseDown}
????????onKeyup={this.handleKeyUp}
????????onKeydown={this.handleKeyDown}
??????>
????????{$slots.default?&&?this.iconPlacement?===?'right'???(
??????????<div?class={`${mergedClsPrefix}-button__content`}>{$slots}div>
????????)?:?null}
????????<NFadeInExpandTransition>NFadeInExpandTransition>
????????{$slots.default?&&?this.iconPlacement?===?'left'???(
??????????<span?class={`${mergedClsPrefix}-button__content`}>{$slots}span>
????????)?:?null}
????????{!this.text???(
??????????<NBaseWave?ref="waveRef"?clsPrefix={mergedClsPrefix}?/>
????????)?:?null}
????????{this.showBorder???(?...)}
????????{this.showBorder???(...)}
?????Component>
???)
??}
})
進(jìn)一步簡化代碼
從上述還剩下的代碼,我們可以看到,其實(shí)對于理解組件庫來說,我們其實(shí)絕大部分內(nèi)容是在做定制主題,然后如果根據(jù)各種傳入的 props,展示不同的主題的工作,所以你會看到 Button 組件里充斥著大量的 CSS 變量,如 this.color 、this.ghost 、this.text 、this.cssVars ,所以我們的核心就是理解這些主題是如何定制的,包含哪些變量和依賴,這些變量和依賴是如何影響 Button 可以承載不同樣式和功能的。
所以上述代碼中,有一些內(nèi)容其實(shí)我們就可以刪掉了:
我們只需要看一個獨(dú)立的 Button 是如何運(yùn)作的,所以 NButtonGroup 部分,按鈕組部分就可以不要了 我們也不需要處理一些獨(dú)特的適配,如 RTL(從右向左排版)等
所以我們需要近一步刪除這些代碼:
import?{?buttonGroupInjectionKey?}?from?'./ButtonGroup'
import?useRtl?from?'../../_mixins/use-rtl'
const?NButtonGroup?=?inject(buttonGroupInjectionKey,?{})
以及其他使用到 buttonGroup 相關(guān)的內(nèi)容。
理解輸入
通過上一步,我們基本上去除了所有無關(guān)的內(nèi)容,達(dá)到了我們最終高潮 MVP 項(xiàng)目里需要的 Button 的所有的、最精簡的內(nèi)容,也就是說我們核心入口代碼自身和依賴的部分已經(jīng)確定了,那么接下來就需要處理全部的輸入,以及刪除這些輸入中相關(guān)的依賴與 Button 處理無關(guān)的邏輯。
我們可以看到 Button 主要有如下一種輸入:
文件頂部的 import 輸入 使用鉤子 useFormItem、或全局狀態(tài)注入inject(...)相關(guān)的輸入
我們可以看到,import 相關(guān)的輸入主要分為兩類:
某些庫,如 vue的導(dǎo)入:這個我們只需要查詢對應(yīng)庫的文檔就可了解對于 API 的作用直接依賴于自身項(xiàng)目的其他相對路徑導(dǎo)入:這個我們就需要繼續(xù)探究 NaiveUI 源碼庫的其他部分
而鉤子 useFormItem 、或全局狀態(tài)注入 inject(...) 相關(guān)的輸入則也依賴于 import 里自身項(xiàng)目的其他相對路徑引入。
我們需要順著如下的這些依賴,進(jìn)行依賴分析:
import?{?createHoverColor,?createPressedColor?}?from?'../../_utils/color/index'
import?{?useConfig,?useFormItem,?useTheme?}?from?'../../_mixins'
import?{
??NFadeInExpandTransition,
??NIconSwitchTransition,
??NBaseLoading,
??NBaseWave
}?from?'../../_internal'
import?{?call,?createKey?}?from?'../../_utils'
import?{?buttonLight?}?from?'../styles'
import?{?buttonGroupInjectionKey?}?from?'./ButtonGroup'
import?style?from?'./styles/button.cssr'
這些依賴?yán)锩嬗行┳约罕揪褪侨~子依賴,并無其它依賴,如:
import?{?createHoverColor,?createPressedColor?}?from?"../../_utils/color/index";
//?其中某幾項(xiàng)
import?{?useFormItem?}?from?"../../_mixins";
//?下面的某幾項(xiàng)
import?{
??NFadeInExpandTransition,
??NIconSwitchTransition,
}?from?"../../_internal";
import?{?call,?createKey,?getSlot,?flatten?}?from?"../../_utils";
這些葉子依賴可以直接對照著原倉庫建立對應(yīng)的目錄結(jié)構(gòu)和文件命名,然后把代碼拷貝過來。
對于那些非葉子依賴,我們需要再下一番功夫繼續(xù)解析其依賴,重復(fù)之前的兩項(xiàng)操作:
刪除 TS 或者其他和 Button 不相干的代碼和依賴 尋找其依賴的依賴,繼續(xù)上面的過程
最后就是對照著源碼的目錄結(jié)構(gòu)創(chuàng)建一樣的結(jié)構(gòu),將處理完無關(guān)內(nèi)容的代碼拷貝過去。
打個比方,對于非葉子依賴 style :
import?style?from?"./styles/button.cssr.js";
我們需要去到對應(yīng)的文件下,查看其依賴:
import?{?c,?cB,?cE,?cM,?cNotM?}?from?'../../../_utils/cssr'
import?fadeInWidthExpandTransition?from?'../../../_styles/transitions/fade-in-width-expand.cssr'
import?iconSwitchTransition?from?'../../../_styles/transitions/icon-switch.cssr'
發(fā)現(xiàn)其依賴了用于進(jìn)行 BEM 規(guī)范定義的 cssr 庫(自建)、以及處理動畫的一些 fadeInWidthExpandTransition 和 iconSwitchTransition 依賴,那么接著要繼續(xù)進(jìn)入這些依賴,如:
import?{?c,?cB,?cE,?cM,?cNotM?}?from?'../../../_utils/cssr'
它的依賴如下:
?/*?eslint-disable?@typescript-eslint/restrict-template-expressions?*/
import?CSSRender,?{?CNode,?CProperties?}?from?'css-render'
import?BEMPlugin?from?'@css-render/plugin-bem'
發(fā)現(xiàn)沒有其他再需要繼續(xù)遞歸尋找的依賴了,都是引入的第三方庫,那么就可以去查閱一下對應(yīng)的第三方庫的文檔,了解 API 的含義即可。
如此往復(fù)進(jìn)行上述的依賴分析,直至收斂,最后我們會得到一個如下的文件組織圖:
.
├──?App.vue
├──?_internal
│???├──?fade-in-expand-transition
│???│???├──?index.js
│???│???└──?src
│???│???????└──?FadeInExpandTransition.jsx
│???├──?icon
│???│???├──?index.js
│???│???└──?src
│???│???????├──?Icon.jsx
│???│???????└──?styles
│???│???????????└──?index.cssr.js
│???├──?icon-switch-transition
│???│???├──?index.js
│???│???└──?src
│???│???????└──?IconSwitchTransition.jsx
│???├──?index.js
│???├──?loading
│???│???├──?index.js
│???│???└──?src
│???│???????├──?Loading.jsx
│???│???????└──?styles
│???│???????????└──?index.cssr.js
│???└──?wave
│???????├──?index.js
│???????└──?src
│???????????├──?Wave.jsx
│???????????└──?styles
│???????????????└──?index.cssr.js
├──?_mixins
│???├──?index.js
│???├──?use-config.js
│???├──?use-form-item.js
│???├──?use-style.js
│???└──?use-theme.js
├──?_styles
│???├──?common
│???│???├──?_common.js
│???│???├──?index.js
│???│???└──?light.js
│???├──?global
│???│???└──?index.cssr.js
│???└──?transitions
│???????├──?fade-in-width-expand.cssr.js
│???????└──?icon-switch.cssr.js
├──?_utils
│???├──?color
│???│???└──?index.js
│???├──?cssr
│???│???├──?create-key.js
│???│???└──?index.js
│???├──?index.js
│???├──?naive
│???│???├──?index.js
│???│???└──?warn.js
│???└──?vue
│???????├──?call.js
│???????├──?flatten.js
│???????├──?get-slot.js
│???????└──?index.js
├──?assets
│???└──?logo.png
├──?button
│???├──?src
│???│???├──?Button.jsx
│???│???└──?styles
│???│???????└──?button.cssr.js
│???└──?styles
│???????├──?_common.js
│???????├──?index.js
│???????└──?light.js
├──?components
│???└──?Button.jsx
├──?config-provider
│???└──?src
│???????└──?ConfigProvider.js
└──?main.js
32?directories,?45?files
一個簡單的 Button 竟然要包含 45 個文件,32個目錄來進(jìn)行支撐,我們基本上可以確定組件庫中 90% 的內(nèi)容是共通的,只需要理解了一個 Button 需要的所有底層依賴和設(shè)計(jì)理念,理解這個組件庫只需要再努力一步,了解剩下 10 % 的各組件特殊設(shè)計(jì),就可以弄懂整個組件庫的源碼。
上述核心整理的一個 Button 的全部依賴代碼可以進(jìn)入我的 Github 倉庫查閱:https://github.com/pftom/naive-app。

抽絲剝繭
當(dāng)我們能夠拿到一個 Button 能夠完美運(yùn)行背后所需要的所有 “必要” 和 “最簡” 的依賴之后,我們就可以邊運(yùn)行這個項(xiàng)目,邊通過查閱資料,畫思維導(dǎo)圖理解這份最簡必要代碼了。

我們首先把代碼跑起來,然后逐層理解代碼邏輯,如前置的幾個鉤子函數(shù)是干嘛的:

核心的 useTheme 鉤子是干嘛的:

用戶自定義相關(guān)的鉤子函數(shù)又是干嘛的,它包含哪些 CSS 變量:

Vue3 組件里面的 setup 返回值有哪些:

最終用于渲染的 render 函數(shù)邏輯是干嘛的:

通過查閱 Vue3 文檔、梳理整個代碼流程,然后了解各個分支是如何運(yùn)作的,我們就能慢慢理解 Button 組件是如何跑起來的。得益于我們進(jìn)行了代碼的最精簡化處理,所以整個看代碼的流程雖然會慢一點(diǎn),但是整體需要理解的內(nèi)容相比之前我們拿到一整份源碼,幾百上千個文件來一股腦從入口開始打斷點(diǎn)調(diào)試會好很多。

寫在最后
相信大家在看皮湯的這篇源碼閱讀文章之前,應(yīng)該也看過各種大牛的源碼解讀文章,但是相信每個人都有自己比較獨(dú)特的看源碼技巧,雖然我這里是拿如何看懂 NaiveUI 的源碼舉例子,但是相信所有看源碼的過程都是如此,遵循如下步驟:
樹立好的心理認(rèn)知 理解高潮 MVP,又包含定位源碼最小可行性代碼需要的內(nèi)容,在看源碼之前先梳理結(jié)構(gòu),確保 MVP 能夠跑起來 然后再在最小、最核心的源碼上進(jìn)行打斷點(diǎn)、畫思維導(dǎo)圖、查閱文檔等方式幫助自己啃下源碼
這是皮湯在看 Vite、NaiveUI 源碼過程中總結(jié)出來的經(jīng)驗(yàn),相信能夠?yàn)榕腔苍诳丛创a路上卻沒有方法的同學(xué)提供一點(diǎn)指引,你完全可以應(yīng)用這個技巧去看其他的源碼,如 Webpack?qiankun?Ant Design?或者抖音最近發(fā)布的 Semi Design。共勉 ??

參考資料
尤大都推薦的組件庫是如何開發(fā)出來的?: https://mp.weixin.qq.com/s?__biz=MzkxMjI3OTA3NQ==&mid=2247485296&idx=1&sn=61b6de490f9d437215cadde9502d75df&chksm=c10e143cf6799d2afe13b5aecebc2b6608bd0fab3e61149b80910ce87757ceb2219651ed9a07&token=586597170&lang=zh_CN#rd
[2]Semi Design: https://github.com/DouyinFE/semi-design
[3]React: https://github.com/facebook/react
[4]貢獻(xiàn)指南: https://github.com/TuSimple/naive-ui/blob/main/CONTRIBUTING.md
[5]CSS Render: https://github.com/07akioni/css-render
- END -
后臺回復(fù):typescript,獲取我寫的 typescript 系列文章,絕對精品 后臺回復(fù):電子書,自動獲取我為大家整理的大量經(jīng)典電子書,省去你篩選以及下載的時間 后臺回復(fù):不一樣的前端,自動獲取精選優(yōu)質(zhì)前端文章。 后臺回復(fù):算法,自動獲取精選算法文章。另外也可關(guān)注我的另外一個專注算法的公眾號力扣加加。 后臺回復(fù):每日一薦,自動獲取我為大家總結(jié)的每日一薦月刊,內(nèi)含精品文章,實(shí)用技巧,高效工具等等

