如何閱讀源碼 —— 以 Vetur 為例
作者:范文杰
簡介:字節(jié)跳動前端工程師
來源:SegmentFault 思否社區(qū)
我很早就意識到,能熟練、高效閱讀開源前端框架源碼是成為一個高級前端工程師必須具備的基本技能之一,所以在我職業(yè)生涯的最早期,就已經(jīng)開始做了很多次相關(guān)的嘗試,但結(jié)果通常都以失敗告終,原因五花八門:
缺乏必要的背景知識,猶如閱讀天書
不理解項目架構(gòu)、設計理念,始終不得要領
目標不夠聚焦,閱讀過程容易復雜化
容易陷入細節(jié),在不重要的問題上糾結(jié)半天
容易追著分支流程跑,分散注意力
沒有及時記錄筆記和總結(jié),沒有把知識碾碎、重組、內(nèi)化成自己的東西
沒有處理過特別復雜問題的經(jīng)歷,潛在的不自信心理
個人毅力、韌性不足,或者目標感不夠強烈,遇到困難容易放棄
等等
弄清楚目標
為了增進對框架的認知深度,提升個人能力
為了應對面試
為了解決當下某個棘手的 bug 或性能問題
基于某些原因,需要對框架做二次改造
反正閑著,也不知道該學點啥,試試唄。。。
好奇
當下確實需要以閱讀源碼的方式增進自己對框架的認知深度嗎?有沒有一些更輕量級,迭代速度更快的學習方式?
你所選定的框架,其復雜度、技術(shù)難度是否與你當下的能力匹配?最好的狀態(tài)是你自認為踮踮腳就能夠到,過高,不具有可行性;過低,ROI 不值當。
閱讀技巧
了解背景知識
優(yōu)質(zhì)參考資料 —— 收集一波質(zhì)量較高的學習資料,收集過程可以同步通讀一遍
框架是如何運行的 —— 也就是所謂的入口
IO —— 框架如何與外部交互?它通常接受什么形態(tài)的運行參數(shù)?輸出什么形式的結(jié)果?
生態(tài) —— 優(yōu)秀的框架背后通常都帶有一套成熟的生態(tài)系統(tǒng),例如 Vue,框架衍生品如何補齊框架本身的功能缺失?它們以何種方式,以什么樣的 IO 與主框架交互?遵循怎么樣的寫法規(guī)則?
如何斷點調(diào)試 —— 這幾乎是最有效的分析方法,斷點調(diào)試能夠幫助你細致地了解每一行代碼的作用。

怎么寫插件:通過 package.json 文件的 contributes 、main 等屬性,聲明插件的功能與入口
怎么運行:開發(fā)階段使用 F5 啟動調(diào)試
怎么編寫語言特性:使用 詞法高亮、Language API、Language Server Protocol 三類技術(shù)實現(xiàn)
六步循環(huán)分析

理解項目結(jié)構(gòu)
尋找合適的切入點
就著切入點查閱文章資料
就著切入點分析代碼流程
局部深入研究
及時總結(jié)
之后,再繼續(xù)設定切入點,重復執(zhí)行上述流程直到透徹地理解了問題
理解項目結(jié)構(gòu)
分析項目入口
分析項目依賴了哪些基礎工具,包括編譯工具,如 webpack、Typescript、babel;基礎庫,如 lodash、tapable、snabbdom。
將項目中重要文件夾、文件逐一列舉出來,理解它們?nèi)绾伟凑找蕾囮P(guān)系組成一個整體的架構(gòu)。
入口分析
contributes.languages 指定語言配置文件
contributes.grammars 指定語法配置文件
"main": "./dist/vueMain.js" 指定插件執(zhí)行入口
探索 contributes.languages 配置
{
// ...
"contributes": {
"languages": [
{
"id": "vue",
"configuration": "./languages/vue-language-configuration.json"
},
{
"id": "vue-html",
"configuration": "./languages/vue-html-language-configuration.json"
}
// ...
]
}
// ...
}這里回過頭翻一下 VS Code 對 [contributes.languages] 的解釋:
{
"comments": {
// symbol used for single line comment. Remove this entry if your language does not support line comments
"lineComment": "http://",
// symbols used for start and end a block comment. Remove this entry if your language does not support block comments
"blockComment": [
"/*",
"*/"
]
},
// ...
}
翻閱參考資料,理解 contributes.languages 配置的作用
打開對應入口文件,猜測各個配置項的作用
繼續(xù)翻閱參考資料,或者修改配置,驗證猜想
探索 contributes.grammars 配置
{
"contributes": {
"grammars": [
{
"language": "vue",
"scopeName": "source.vue",
"path": "./syntaxes/vue-generated.json",
"embeddedLanguages": {
"text.html.basic": "html",
// ...
}
},
{
"language": "vue-postcss",
"scopeName": "source.css.postcss",
"path": "./syntaxes/vue-postcss.json"
}
// ...
]
}
}
language:語言的名稱
scopeName:語言的分類,與 TextMate scopeName 同義,可用于嵌套語法定義
path:語言的詞法規(guī)則文件
{
"name": "Vue HTML",
"scopeName": "text.html.vue-html",
"fileTypes": [],
"uuid": "ca2e4260-5d62-45bf-8cf1-d8b5cc19c8f8",
"patterns": [
// ...
{
"name": "meta.tag.any.html",
"begin": "(<)([A-Z][a-zA-Z0-9:-]*)(?=[^>]*></\\2>)",
"beginCaptures": {
"1": {
"name": "punctuation.definition.tag.begin.html"
},
"2": {
"name": "support.class.component.html"
}
}
}
],
"repository": {
// ...
}
}
探索 main 配置
"main": "./dist/vueMain.js"
import vscode from 'vscode';
export async function activate(context: vscode.ExtensionContext) {
// ... 啟動邏輯
}
調(diào)用 registerXXXCommands 方法注冊一系列命令
調(diào)用 initializeLanguageClient 方法初始化 LSP Client 對象
小結(jié)
Vetur 本質(zhì)上是一個 VS Code 插件,所有配置 —— 包括入口都記錄在 package.json 文件中
Vetur 包含三種啟動入口:
contributes.languages:定義一些簡單的語言基本配置,包括怎么折疊,怎么注釋
contributes.grammars:定義了一套基于 TextMate 引擎的詞法規(guī)則,用于實現(xiàn)代碼高亮
main:定義了插件的啟動入口,入口中注冊了一系列命令,同時創(chuàng)建了基于 LSP 協(xié)議的 Language Client 對象,而 LSP 協(xié)議用于實現(xiàn)如代碼補全、錯誤診斷、跳轉(zhuǎn)定義等高級特性
基礎依賴分析
VS Code 插件配置信息,大體上在上一節(jié)都有描述,這里不展開
工程化命令,核心有:
watch:對應命令為 rollup -c rollup.config.js -w ,由此可以推斷 Vetur 基于 Rollup 實現(xiàn)構(gòu)建
compile:功能與 watch 相似
lint:對應命令為 tslint -c tslint.json **.ts ,由此可以推斷 Vetur 基于 tslint 實現(xiàn)代碼檢查
項目的 devDependencies 依賴,主要包含 typescript、tslint、rollup、vscode-languageclient、husky、mocha、vscode-test、prettier
Vetur 使用 Rollup + typescript 等工具執(zhí)行構(gòu)建工作,按常理執(zhí)行 yarn watch 命令應該就能啟動一個持續(xù)的構(gòu)建工作進程
Vetur 使用 tslint 實現(xiàn)代碼檢查,配合 huscky + prettier 完成格式化工作
Vetur 使用 mocha + vscode-test 實現(xiàn)自動化測試
文件結(jié)構(gòu)
vetur
├─ .vscode
│ ├─ ...
├─ build
│ ├─ ...
├─ client
│ ├─ client.ts
│ ├─ commands
│ │ ├─ ...
│ ├─ grammar.ts
│ ├─ ...
├─ languages
│ ├─ vue-html-language-configuration.json
│ ├─ ...
├─ scripts
│ ├─ build_grammar.ts
│ └─ tsconfig.json
├─ server
│ ├─ .gitignore
│ ├─ .mocharc.yml
│ ├─ .npmrc
│ ├─ bin
│ │ └─ vls
│ ├─ package.json
│ ├─ rollup.config.js
│ ├─ src
│ │ ├─ ...
├─ syntaxes
│ ├─ markdown-vue.json
│ ├─ pug
│ │ ├─ ...
│ ├─ ...
│ └─ vue.yaml
├─ test
│ ├─ ...
├─ vti
│ ├─ README.md
│ ├─ bin
│ │ └─ vti
│ ├─ package.json
│ ├─ rollup.config.js
│ ├─ src
│ │ ├─ ...
│ ├─ tsconfig.json
│ └─ yarn.lock
├─ tsconfig.options.json
├─ package.json
├─ ...
└─ yarn.lock
client:VS Code 插件的入口代碼,package.json 文件中 main 字段會指向這個目錄的產(chǎn)物
server:LSP 架構(gòu)中的 Server 端,上述 client 會通過 LSP 協(xié)議與這個 server 目錄通信
syntaxes:Vetur 的詞法規(guī)則文件夾,內(nèi)部包含許多 JSON 格式,符合 TextMate 規(guī)則的詞法聲明
languages:Vetur 提供的語言配置信息,規(guī)則比較簡單,了解作用即可,不必深入
vti:按 vti/bin/vti 文件可以推斷,這里是 Vetur 的命令行工具,不在主流程內(nèi)可以先忽略
docs:按內(nèi)容可以推斷這是 Vetur 的介紹文檔,此處可忽略
build:構(gòu)建命令,package.json 文件的 script 命令有一些會指向這個目錄,可以忽略
一系列基礎配置文件,包括 tsconfig.json 、package.json 等,可先忽略
小結(jié)
Vetur 是一個語言插件,所以必然是使用 詞法高亮、Language API、Language Server Protocol 三類技術(shù)實現(xiàn)核心邏輯的,而 package.json 文件中的 contributes 配置項的內(nèi)容也恰好驗證了這一點
詞法高亮 相關(guān)的代碼集中在 syntaxes 文件夾
Language Server Protocol 相關(guān)的代碼集中在 client 與 server 文件夾
可以用 yarn watch 命令持續(xù)構(gòu)建,配合 F5 快捷鍵啟動調(diào)試
設定切入點
善用搜索引擎
谷歌 and 百度一類的搜索引擎,體感上谷歌的搜索質(zhì)量會好很多,不過有一定的英語門檻
開源項目的官網(wǎng)、社區(qū)、wiki、github 等官方渠道,通常都會有比較不錯的資料
Segmentfault、知乎、掘金、公眾號等垂直社區(qū)
國外的 Medium/StackOverflow 社區(qū),質(zhì)量極高,很多大佬在上面活躍
Xxx 源碼解析
Xxx 原理
如何實現(xiàn) xxx
分析關(guān)鍵流程

啟動階段,vls 類型會初始化化 projectService 對象,之后再監(jiān)聽各類 LSP 事件
執(zhí)行階段,LSP 事件觸發(fā)時,vls 會將事件直接委托給 projectService 對象處理,而 projectService 會做兩件事情:
針對 SFC 文件做 region 切割,解析出 template、script、style 等區(qū)塊
針對不同區(qū)塊,調(diào)用 modes/xxx 對象的 doComplete 函數(shù)處理
對于 template 的格式化請求,最終會流轉(zhuǎn)到 modes/template/index.ts 文件的 format 函數(shù)做處理
對于 style 的格式化請求,則流轉(zhuǎn)到 modes/style/index.ts 文件的 format 函數(shù)
同理可以推導出包括代碼補全、hover 提示、跳轉(zhuǎn)到定義、錯誤診斷等等高級特性上

局部深入
靜態(tài)猜想:“讀”源碼,從面上理解代碼邏輯并作出猜想
動態(tài)驗證:“運行”源碼,借用 debug 工具逐行跟蹤代碼執(zhí)行過程,必要時可以改動原有代碼,驗證猜想
靜態(tài)分析 —— 做猜想
函數(shù)層面,關(guān)注輸入輸出及副作用:
函數(shù)接受什么結(jié)構(gòu)的參數(shù),這些參數(shù)經(jīng)過函數(shù)內(nèi)部的每一條語句之后會發(fā)生什么變化,或者如何影響語句的執(zhí)行
函數(shù)執(zhí)行完畢之后,會返回什么結(jié)構(gòu)的結(jié)果,這些結(jié)果下一步會被誰消費,影響誰的執(zhí)行邏輯
特別的,有不少庫的函數(shù)實現(xiàn)有明顯的“副作用”,不是那么“純”,包括 Webpack、Vetur、Eslint 等 —— 這會急劇提升理解成本,所以閱讀的時候多留個心眼
分支語句中,優(yōu)先關(guān)注主流程,分支流程很容易增加心智負擔,到后面就不認得誰是誰了
對于循環(huán)語句,通常可以關(guān)注循環(huán)之前的狀態(tài)與之后的狀態(tài),通過這些變化推斷循環(huán)的作用
對于變量與子函數(shù),根據(jù)命名推斷作用,通常不必過度細究
跳過參數(shù)校驗、錯誤處理等分支邏輯,抓主流程!抓重點!
謹記你要研究的切入點,遇到特別復雜的子模塊,先大致理解功能,點到為止,記下這個硬骨頭回頭再作為一個新的切入點繼續(xù)研究
學點常用的設計模式,工廠、裝飾器、代理等等,這些模式的使用率非常高
動態(tài)分析 —— 驗證猜想
如果框架已經(jīng)接入了一些工程化工具,需要弄清楚如何將源碼編譯為運行產(chǎn)物,例如 Vetur 項目接入了 tsc + rollup,對應的命令為 yarn watch/compile
如何啟動調(diào)試模式,例如 Vetur 場景下需要借用 VS Code 的 .vscode/launch.json 配置文件 + F5 命令啟動調(diào)試;而對于前端框架如 Vue、React,通常打開瀏覽器的 DevTool 面板即可
如何插入調(diào)試語句,前端或 Node 場景下通常添加 debugger; 語句即可
及時總結(jié)
下一個切入點
最佳實踐
設定好具體、可衡量的目標,不要為了學習而學習,如果有切實的強訴求,那就別由于彷徨,馬上去做
磨刀不誤砍柴工,不要上來就對著源碼瘋狂輸出,一定要花點時間站在高層視角去看框架的背景和生態(tài)
抓大放小,忽略哪些還不熟悉的概念、語句、工具、分支邏輯,你要認識到復雜事物的學習模型往往螺旋上升,逐步深入的,不可能過一遍就能掌握所有細節(jié)和精髓,如果一開始就過度關(guān)注細節(jié),通常會讓整個學習周期拉到無限長。要弄清楚啥時候,什么情況下應該忽略細節(jié),什么時候應該抓住不放 —— 這與你的目標和切入點有很大的關(guān)系
隨時筆記:一旦有任何新發(fā)現(xiàn)、新問題,做好筆記,記錄下來,這些都會成為繼續(xù)探索的重要線索
隨時總結(jié):
筆記記錄當下的、零碎的發(fā)現(xiàn),總結(jié)則將這些線索串聯(lián)形成知識點。
總結(jié)過程你會發(fā)現(xiàn)更多認知漏洞,提出更多問題,可以反過來繼續(xù)挖掘
好記性不如爛筆頭,探索的結(jié)果落到紙面上才會真正成為你自己的東西,極端一點看,沒有形成輸出的學習過程往往會隨著時間的流逝,變成徒勞

