目前網上有很多「XX原始碼分析」這樣的文章,不過這些文章分析原始碼的範圍有限,有時候講的內容不是讀者最關心的。同時我也注意到,原始碼是在不斷更新的,文章裡寫的原始碼往往已經過時了。因為這些問題,很多同學都喜歡自己看原始碼,自己動手,豐衣足食。
這篇文章主要講的是閱讀大型的前端開源專案比如 React、Vue、Webpack、Babel 的原始碼時的一些技巧。目的是讓大家在遇到需要閱讀原始碼才能解決的問題時,可以更快的定位到自己想看的程式碼。授人以魚不如授人以漁,希望大家可以通過這篇部落格,瞭解到閱讀大型前端專案原始碼時的切入點。在之後遇到好奇的問題時,可以自己去探索。
問題驅動——不要為了看原始碼而看原始碼
首先我們要明確一點,看原始碼的目的是什麼?
我個人的意見是,看原始碼是為了解決問題。開源專案的原始碼並沒有什麼非常特殊的地方,也都是普通的程式碼。這些程式碼的數量級一般都挺大,如果想是從原始碼中學到東西,直接瀏覽整個 Codebase 無疑是大海撈針。
但如果是帶著問題去看原始碼,比如想了解一下 React 的合成事件系統的原理,想了解 React 的 setState 前後發生了什麼,或者想了解 Webpack 外掛系統的原理。也有可能是遇到了一個 bug,懷疑是框架/工具的問題。在這樣的情況下,帶著一個具體的目標去看原始碼,就會有的放矢。
看最新版的原始碼
之前看到一種說法,看原始碼要從專案的第一個 commit 開始看。如果是為了解決前文中對框架/工具產生的困惑,那自然要看當前專案中用到的框架/工具的版本。
如果是為了學習原始碼,我也建議看最新的原始碼。因為一個專案是在不斷迭代和重構的。不同版本之間可能是一次完全的重寫。比如 Vue 2.x 和 React 16。重構導致了程式碼架構上的一些變化,Vue 2.x 引入了 Vritual DOM,Pull + Push 的資料變化檢測方式讓整個程式碼的結構變的更清晰了,所以 2.x 的程式碼其實比 1.x 的更容易閱讀。React 16 重寫了 Reconciler,引入了 fiber 這個概念,整個程式碼倉庫結構也更清晰,所以更推薦閱讀。
前置條件
看原始碼怎麼看,當然不能一把梭了。
看原始碼之前需要對專案的原理有一個基本的瞭解。所謂原理就是,這個專案有哪些組成部分,為了達到最終的產出,要經過哪幾步流程。這些流程裡,業界主流的方案有哪幾種。
比如前端 View 層框架,要渲染出 UI,元件要經過 mount、 render 等等步驟。資料驅動的前端框架,在 mounted 之後,就會進入一個迴圈,當使用者互動觸發元件資料變化時,會更新 UI。其中資料的檢測方式又有分 Push 和 Pull 兩種方案。渲染 UI 可以是全量的字串模板替換,也可以是基於 Virtual DOM 的差量 DOM 更新。
又比如前端的一些工具,Webpack 和 Babel 這些工具都是基於外掛的。基本的工作流程就是讀取檔案,解析程式碼成 AST,呼叫外掛去轉換 AST,最後生成程式碼。要了解 Webpack 的原理,就要知道 Webpack 基於一個叫 tapable 的模組系統。
那我們要如何瞭解這些呢?要了解這些,可以去各大網站和部落格上的《XXX原始碼解析》系列。通過這些文章,我們可以對我們要看的框架/工具的原理有一個大致的瞭解。
本地build
不過最終我們還是要直接看原始碼。筆者真正看原始碼的第一步就是把專案的程式碼倉庫 clone 到本地。然後按專案 README 上的構建指南,在本地 build 一下。
如果是前端框架,我們可以在 HTML 中裡直接引入本地 build 出的 umd bundle(記得用 development build,不然會把程式碼壓縮,可讀性差),然後寫一個簡單的 demo,demo 裡引入本地的 build。如果是基於 Nodejs 的工具,我們可以用 npm link 把這個工具的命令 link 到本地。也可以直接看專案的 package.json 的入口檔案,直接用 node 執行那個檔案。
這裡要強調一下,大型的開源專案一般都會有一個 Contribution Guide,目的是讓想貢獻程式碼的開發者更快上手。裡面就有講怎麼在本地構建程式碼。
以 React 為例,React 的 Contributing Guide 裡就 Development Workflow 這一節。裡面有這麼一段話:
The easiest way to try your changes is to run yarn build core,dom –type=UMD and then open fixtures/packaging/babel-standalone/dev.html. This file already uses react.development.js from the build folder so it will pick up your changes.
所以 React 倉庫中的 fixtures/packaging/babel-standalone/dev.html 就是一個方便的 demo 頁。我們可以在這個頁面快速檢視我們在本地對程式碼的改動。
你可以嘗試著在專案的入口檔案中加入一句 log,看看是不是可以在控制檯/終端看到這句 log。如果可以的話,恭喜你,你現在可以隨便把玩這個專案了!
理清目錄結構
在看具體的程式碼之前,我們需要理清專案的目錄結構,這樣我們才能更快的知道在哪裡地方找相關功能的程式碼。
我們看看 React 的目錄結構。React 是一個 monorepo。也就是一個倉庫裡包含了多個子倉庫。我們在 packages 目錄下可以看到很多單獨的 package:
在 React 16 之後,React 的程式碼分為 React Core,Renderer 和 Reconciler 三部分。這是因為 React 的設計讓我們可以把負責對映資料到 UI 的 Reconciler 以及負責渲染 Vritual DOM 到各個終端的 Renderer 和 React Core 分開。React Core 包含了 React 的類定義和一些頂級 API。大部分的渲染和 View 層 diff 的邏輯都在 Reconciler 和 Renderer 中。
Babel 也是一個 monorepo。Babel 的核心程式碼是 babel-core 這個 package,Babel 開放了介面,讓我們可以自定義 Visitor,在AST轉換時被呼叫。所以 Babel 的倉庫中還包括了很多外掛,真正實現語法轉換的其實是這些外掛,而不是 babel-core 本身。
Vuejs 的程式碼比較典型,核心程式碼在 src 目錄下,按功能模組劃分。因為 Vue 也支援多平臺渲染,所以把平臺相關的程式碼都放到了 platform 資料夾下,core 資料夾中是 Vue 的核心程式碼,compiler 是 Vue 的模板編譯器,把 HTML 風格的模板編譯為 render function。
Webpack 和 Babel 一樣,可以說都是基於外掛的系統。Webpack 的主要原始碼在 lib 目錄下,裡面的 webpack.js 就是入口檔案。
上面說了四個專案的目錄結構,那我們遇到一個新的開源專案,應該怎麼了解它的目錄結構呢?
如果這個專案是一個 monorepo,首先我們要找到核心的那個 package,然後看裡面的程式碼。
不是 monorepo 的話,一般來說,如果這個專案是一個 CLI 的工具,那 bin 目錄下放的就是命令列介面相關的入口檔案,lib 或者 src 下面就是工具的核心程式碼。如果這個專案是一個前端 View 層框架,那目錄結構就和 Vue 類似。
作為驗證,大家可以看一下打包工具 parcel 和前端 View 層庫 moon 的目錄結構。目錄結構這個東西往往是大同小異,多看幾個專案就熟悉了。
debugger && 全域性搜尋大法
執行了本地的 build,瞭解了目錄結構,接下來我們就可以開始看原始碼了!之前說了,我們要以問題驅動,下面我就以 React 呼叫 setState 前後發生了什麼這個問題作為例子。
我們可以在 setState 的地方打一個斷點。首先我們要找到 setState 在什麼地方。這個時候之前的準備工作就派上用處了。我們知道 React 的共有 API 在 react 這個 package 下面。我們就在那個 package 裡面全域性搜尋。我們發現這個 API 定義在 src/ReactBaseClasses.js 這個檔案裡。
於是我們就在這裡打一個斷點:
1 2 3 4 5 6 7 8 9 10 11 |
Component.prototype.setState = function(partialState, callback) { invariant( typeof partialState === 'object' || typeof partialState === 'function' || partialState == null, 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.', ); debugger; this.updater.enqueueSetState(this, partialState, callback, 'setState'); }; |
然後執行本地 React build 的 demo 頁面,讓元件觸發 setState,我們就可以在 Devtool 裡看到斷點了。
我們走進 this.updater.enqueueSetState 這個呼叫,就來到了 ReactFiberClassComponent 這個函式中的 enqueueSetState,這裡呼叫了 enqueueUpdate 和 scheduleWork 兩個函式,如果要深入 setState 之後的流程,我們只需要再點選 走進這兩個函式裡看具體的程式碼就可以了。
如果想看 setState 之前發生了什麼,我們只需要看 Devtool 右邊的呼叫棧:
點選每一個 frame 就可以跳到對應的函式中,並且恢復當時的上下文。
結合一步一步的程式碼除錯,我們可以看到框架的函式呼叫棧。對於每個重要的函式,我們可以在倉庫裡搜尋到原始碼,進一步研究。
Node 工具的除錯方法也是相似的,我們可以在執行 node 命令時加上 –inspect 引數。具體可以看 Debugging Node.js with Chrome DevTools 這篇部落格。
其實大家都知道單步除錯這種辦法,但在哪裡打斷點才是最關鍵的。我們在熟悉框架的原理之後,就可以在框架的關鍵鏈路上打斷點,比如前端 View 層框架的宣告週期鉤子和 render 方法,Node 工具的外掛函式,這些程式碼都是框架執行的必經之地,是不錯的切入點。
如果是為了瞭解一個特定的問題,大家可以直接在自己覺得有問題的地方打斷點。然後把原始碼執行起來,想辦法讓程式碼執行到那個地方。我們在斷點可以看到區域性變數等等資訊,有助於定位問題。
來自開發團隊的資源
其實開源專案的開發團隊也都致力於讓更多的人蔘與到專案中來,降低專案的門檻。所以我們線上上其實可以找到很多來自開發團隊的資源。這些資源可以幫助我們去理解專案的原理。
關注核心開發者
每個專案都有一些核心開發者,比如 React 的 Dan Abramov, Andrew Clark 和 Sebastian Markbåge。Webpack 的 Tobias Koppers 和 Sean Larkin。Vue 的 Evan You。我們可以在 Twitter 上關注他們,瞭解專案的動態。
關注官方部落格和演講視訊
如果我們關注了上面的核心開發者,就會發現他們時常會發布一些和原始碼/專案原理有關的部落格或者視訊。
React 的官方部落格最近就有很多和專案開發有關的部落格。
- Behind the Scenes: Improving the Repository Infrastructure 這篇介紹的是 React 專案倉庫的基礎設施。
- Sneak Peek: Beyond React 16
Andrew Clark 一開始就寫了一篇介紹 fiber 架構的文件。 Dan Abramov 最近在 JSConf 上對 React 未來的一些新特性的介紹 – Beyond React 16。React 部落格中的 Sneak Peek: Beyond React 16 也是對這次 Talk 的介紹。
Evan You 介紹前端框架資料變化偵測原理的 Talk。Vue 文件中也有 Reactivity in Depth 這樣的介紹原理的章節。
Sean Larkin 的 Everything is a plugin! Mastering webpack from the inside out 介紹了 Webpack 的核心元件 Tapable。
James Kyle 的 How to Build a Compiler 可以讓我們瞭解 Babel 轉譯程式碼的基本流程。
寫在最後
本文最核心的觀點就是,看原始碼的目的是為了解決問題。我們鼓勵大家在本地把大型專案的原始碼跑起來,自己隨意把玩,研究。因為原始碼也是普通的程式碼,並沒有太多門檻。唯一的門檻可能就來源於開源專案作者和普通開發者之間的資訊不對稱,普通開發者對專案的原理和目錄結構不夠了解。
我們可以從開發者那裡獲取資源,同時也可以多閱讀社群裡的原始碼分析文章,這些都有助於我們理解專案的原理,為後續的原始碼分析打下基礎。