通過閱讀源代碼提高你的 JavaScript 知識

已發表: 2022-03-10
快速總結 ↬當您還處於編程生涯的早期階段時,挖掘開源庫和框架的源代碼可能是一項艱鉅的工作。 在本文中,Carl Mungazi 分享了他如何克服恐懼並開始使用源代碼來提高自己的知識和技能。 他還使用 Redux 來展示他如何分解庫。

你還記得你第一次深入挖掘你經常使用的庫或框架的源代碼嗎? 對我來說,那一刻發生在三年前我作為前端開發人員的第一份工作期間。

我們剛剛重寫了用於創建電子學習課程的內部遺留框架。 在重寫開始時,我們花時間研究了許多不同的解決方案,包括 Mithril、Inferno、Angular、React、Aurelia、Vue 和 Polymer。 由於我是一個初學者(我剛剛從新聞業轉向網絡開發),我記得對每個框架的複雜性感到害怕,並且不了解每個框架是如何工作的。

當我開始更深入地研究我們選擇的框架 Mithril 時,我的理解增長了。 從那時起,我花大量時間深入研究我每天在工作中或在我自己的項目中使用的庫的內容,極大地幫助了我對 JavaScript 和一般編程的了解。 在這篇文章中,我將分享一些你可以使用你最喜歡的庫或框架並將其用作教育工具的方法。

秘銀超標函數源碼
我對閱讀代碼的第一次介紹是通過 Mithril 的超腳本函數。 (大預覽)
跳躍後更多! 繼續往下看↓

閱讀源代碼的好處

閱讀源代碼的主要好處之一是你可以學到很多東西。 當我第一次查看 Mithril 的代碼庫時,我對虛擬 DOM 是什麼有一個模糊的概念。 完成後,我了解到虛擬 DOM 是一種涉及創建對象樹的技術,這些對象樹描述了您的用戶界面應該是什麼樣子。 然後使用諸如document.createElement之類的 DOM API 將該樹轉換為 DOM 元素。 通過創建一個描述用戶界面未來狀態的新樹然後將其與舊樹中的對象進行比較來執行更新。

我已經在各種文章和教程中閱讀了所有這些內容,雖然它很有幫助,但能夠在我們發布的應用程序的上下文中觀察它對我來說非常有啟發性。 它還教會了我在比較不同的框架時要問哪些問題。 例如,我現在知道要問諸如“每個框架執行更新的方式如何影響性能和用戶體驗?”之類的問題,而不是查看 GitHub 的星星。

另一個好處是增加了您對良好應用程序架構的欣賞和理解。 雖然大多數開源項目的存儲庫通常遵循相同的結構,但每個項目都包含差異。 Mithril 的結構非常扁平,如果您熟悉它的 API,您可以對諸如renderrouterrequest文件夾中的代碼進行有根據的猜測。 另一方面,React 的結構反映了它的新架構。 維護者將負責 UI 更新的模塊( react-reconciler )與負責渲染 DOM 元素的模塊( react-dom )分開。

這樣做的好處之一是,開發人員現在可以更輕鬆地通過掛鉤到react-reconciler包來編寫自己的自定義渲染器。 Parcel,我最近一直在研究的一個模塊捆綁器,也有一個像 React 這樣的packages文件夾。 關鍵模塊名為parcel-bundler ,它包含負責創建包、啟動熱模塊服務器和命令行工具的代碼。

JavaScript 規範中解釋 Object.prototype.toString 工作原理的部分
不久之後,您正在閱讀的源代碼將引導您了解 JavaScript 規範。 (大預覽)

還有一個好處——這讓我很驚喜——是你可以更輕鬆地閱讀官方的 JavaScript 規範,該規範定義了該語言的工作方式。 我第一次閱讀規範是在我研究throw Errorthrow new Error之間的區別時(劇透警告 - 沒有)。 我對此進行了調查,因為我注意到 Mithril 在其m函數的實現中使用了throw Error ,我想知道使用它是否比throw new Error有好處。 從那以後,我也學會了邏輯運算符&&|| 不一定返回布爾值,找到管理==相等運算符如何強制值的規則以及Object.prototype.toString.call({})返回'[object Object]'原因。

閱讀源代碼的技巧

有很多方法可以處理源代碼。 我發現最簡單的開始方法是從您選擇的庫中選擇一個方法並記錄調用它時發生的情況。 不要記錄每一步,而是嘗試確定其整體流程和結構。

我最近使用ReactDOM.render完成了這項工作,因此學到了很多關於 React Fiber 以及其實現背後的一些原因。 值得慶幸的是,由於 React 是一個流行的框架,我遇到了很多其他開發人員就同一問題撰寫的文章,這加快了進程。

這次深入探討還向我介紹了協作調度的概念、 window.requestIdleCallback方法和鍊錶的真實示例(React 通過將更新放入隊列中來處理更新,隊列是優先更新的鍊錶)。 這樣做時,建議使用該庫創建一個非常基本的應用程序。 這使得調試時更容易,因為您不必處理由其他庫引起的堆棧跟踪。

如果我不進行深入審查,我將在我正在處理的項目中打開/node_modules文件夾,或者我將轉到 GitHub 存儲庫。 當我遇到錯誤或有趣的功能時,通常會發生這種情況。 在 GitHub 上閱讀代碼時,請確保您閱讀的是最新版本。 您可以通過單擊用於更改分支的按鈕並選擇“標籤”來查看帶有最新版本標籤的提交中的代碼。 庫和框架永遠在發生變化,因此您不想了解下一個版本中可能會刪除的內容。

另一種閱讀源代碼較少涉及的方法是我喜歡稱之為“粗略瀏覽”的方法。 在我開始閱讀代碼的早期,我安裝了express.js ,打開了它的/node_modules文件夾並檢查了它的依賴項。 如果README沒有為我提供令人滿意的解釋,我會閱讀源代碼。 這樣做讓我得到了這些有趣的發現:

  • Express 依賴於兩個模塊,它們都合併對象,但以非常不同的方式合併對象。 merge-descriptors僅添加直接在源對像上直接找到的屬性,它還合併不可枚舉的屬性,而utils-merge僅迭代對象的可枚舉屬性以及在其原型鏈中找到的屬性。 merge-descriptors使用Object.getOwnPropertyNames()Object.getOwnPropertyDescriptor()utils-merge使用for..in
  • setprototypeof模塊提供了一種設置實例化對象原型的跨平台方式;
  • escape-html是一個 78 行的模塊,用於對內容字符串進行轉義,以便將其插入 HTML 內容中。

雖然這些發現可能不會立即有用,但對您的庫或框架使用的依賴關係有一個大致的了解是很有用的。

在調試前端代碼時,瀏覽器的調試工具是你最好的朋友。 除其他外,它們允許您隨時停止程序並檢查其狀態、跳過函數的執行或步入或退出它。 有時這不會立即成為可能,因為代碼已被縮小。 我傾向於將其縮小並將未縮小的代碼複製到/node_modules文件夾中的相關文件中。

ReactDOM.render 函數的源代碼
像對待任何其他應用程序一樣進行調試。 形成一個假設,然後對其進行檢驗。 (大預覽)

案例研究:Redux 的連接函數

React-Redux 是一個用於管理 React 應用程序狀態的庫。 在處理諸如此類的流行庫時,我首先搜索有關其實現的文章。 在此案例研究中,我遇到了這篇文章。 這是閱讀源代碼的另一個好處。 研究階段通常會引導您閱讀諸如此類的內容豐富的文章,這些文章只會提高您自己的思維和理解。

connect是一個 React-Redux 函數,它將 React 組件連接到應用程序的 Redux 存儲。 如何? 好吧,根據文檔,它執行以下操作:

“...返回一個新的連接組件類,它包裝了您傳入的組件。”

讀完之後,我會問以下問題:

  • 我是否知道函數接受輸入然後返回包含附加功能的相同輸入的任何模式或概念?
  • 如果我知道任何這樣的模式,我將如何根據文檔中給出的解釋來實現它?

通常,下一步是創建一個使用connect的非常基本的示例應用程序。 然而,這次我選擇使用我們在 Limejump 構建的新 React 應用程序,因為我想了解最終將進入生產環境的應用程序上下文中的connect

我關注的組件如下所示:

 class MarketContainer extends Component { // code omitted for brevity } const mapDispatchToProps = dispatch => { return { updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today)) } } export default connect(null, mapDispatchToProps)(MarketContainer);

它是一個容器組件,包裝了四個較小的連接組件。 您在導出connect方法的文件中遇到的第一件事就是以下註釋: connect 是 connectAdvanced 的外觀。 不用走太遠,我們就有了第一個學習時刻:一個觀察外觀設計模式的機會。 在文件的末尾,我們看到connect導出了一個名為createConnect的函數的調用。 它的參數是一堆默認值,已經像這樣被解構:

 export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory } = {})

同樣,我們遇到了另一個學習時刻:導出調用的函數解構默認函數參數。 解構部分是一個學習時刻,因為如果代碼是這樣編寫的:

 export function createConnect({ connectHOC = connectAdvanced, mapStateToPropsFactories = defaultMapStateToPropsFactories, mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories, mergePropsFactories = defaultMergePropsFactories, selectorFactory = defaultSelectorFactory })

它會導致這個錯誤Uncaught TypeError: Cannot destructure property 'connectHOC' of 'undefined' or 'null'. 這是因為該函數沒有可依賴的默認參數。

注意有關這方面的更多信息,您可以閱讀 David Walsh 的文章。 一些學習時刻可能看起來微不足道,這取決於您對語言的了解,因此最好專注於您以前從未見過或需要了解更多的東西。

createConnect本身在其函數體中什麼也不做。 它返回一個名為connect的函數,我在這裡使用的函數:

 export default connect(null, mapDispatchToProps)(MarketContainer)

它有四個參數,都是可選的,前三個參數每個都通過一個match函數,該函數根據參數是否存在以及它們的值類型來幫助定義它們的行為。 現在,因為提供給match的第二個參數是導入connect的三個函數之一,所以我必須決定要遵循哪個線程。

如果這些參數是函數,則用於包裝第一個參數以connect的代理函數、用於檢查普通對象的isPlainObject實用程序或揭示如何設置調試器以在所有異常上中斷的warning模塊都有學習時刻。 在匹配函數之後,我們來到connectHOC ,該函數將我們的 React 組件連接到 Redux。 這是另一個返回wrapWithConnect的函數調用,該函數實際處理將組件連接到商店。

查看connectHOC的實現,我可以理解為什麼它需要connect來隱藏其實現細節。 它是 React-Redux 的核心,包含不需要通過connect公開的邏輯。 儘管我將在這裡結束深入探討,但如果我繼續的話,這將是查閱我之前找到的參考資料的最佳時機,因為它包含對代碼庫的非常詳細的解釋。

概括

一開始閱讀源代碼很困難,但與任何事情一樣,隨著時間的推移會變得更容易。 目標不是要了解所有內容,而是要獲得不同的觀點和新知識。 關鍵是要對整個過程深思熟慮,並對每一件事都充滿好奇。

例如,我發現isPlainObject函數很有趣,因為它使用if (typeof obj !== 'object' || obj === null) return false來確保給定參數是一個普通對象。 當我第一次閱讀它的實現時,我想知道為什麼它不使用Object.prototype.toString.call(opts) !== '[object Object]' ,它的代碼更少並且區分對象和對象子類型,例如 Date目的。 但是,閱讀下一行會發現,在極不可能發生的情況下,使用connect的開發人員返回 Date 對象,例如,這將由Object.getPrototypeOf(obj) === null檢查處理。

isPlainObject中另一個有趣的地方是這段代碼:

 while (Object.getPrototypeOf(baseProto) !== null) { baseProto = Object.getPrototypeOf(baseProto) }

一些谷歌搜索讓我找到了這個 StackOverflow 線程和 Redux 問題,解釋了該代碼如何處理案例,例如檢查源自 iFrame 的對象。

閱讀源代碼的有用鏈接

  • “如何逆向工程框架”,Max Koretskyi,Medium
  • “如何閱讀代碼”,Aria Stewart,GitHub