[譯] 經過閱讀代碼來提高你的 JavaScript 水平

寫在前面的話

原文地址: Improve Your JavaScript Knowledge By Reading Source Codejavascript

爲了符合閱讀習慣,本篇採用意譯。html

前言:在你的早期程序生涯中,弄清楚開源庫和框架的源碼是一個不小的挑戰。這篇文章中,做者卡爾給咱們分享了他是如何客服恐懼開始閱讀源碼的,這對提升知識水平有幫助。他也用 Redux 做爲一個例子來展現如何攻克一個庫。前端

你還記得第一次去深度閱讀源碼時的狀況嗎?對我而言,是三年前的事了。java

當時咱們已經完成了內部遺留框架的重寫。在重寫之初,咱們花時間去調查了多種解決方案包括 Mithril,Inferno, Angular, React, Aurelia, Vue 和 Polymer。做爲一個剛剛重新聞行業轉行的我,我還記得被每種框架的複雜性支配的恐懼,同時不理解它們是如何工做的。node

當我開始研究選擇的框架時,個人理解也就在增加了。從那時起,我深刻研究那些在工做或我的項目中使用的庫,在數個小時以內,個人 Javascript 知識和編程技巧也都受益不淺。在這片文章中,我會分享幾種方式,你能夠找一個你喜歡的庫或者框架,而後使用它做爲學習工具。react

閱讀源碼的好處

學習源碼的主要好處是能學到不少東西。當我第一次看到Mithril的代碼庫時,我對虛擬DOM的含義有一個模糊的概念。 當我完成時,我知道虛擬DOM是一種技術,它涉及建立描述用戶界面應該是什麼樣的對象樹。 而後使用DOM API(例如document.createElement)將該樹轉換爲DOM元素。 經過建立描述用戶界面的將來狀態的新樹,而後將其與舊樹中的對象進行比較來執行更新。git

我在各類文章和教程中已經閱讀了全部這些內容,雖然它頗有幫助,可是在咱們發佈的應用程序的上下文中可以觀察它對我來講很是有啓發性。 它還告訴我在比較不一樣的框架時要問哪些問題。而不是盯着 GitHub 關注量,好比 「每一個框架執行更新的方式如何影響性能和用戶體驗?」等問題。github

另外一個好處是增長對良好應用程序架構的理解和理解。 雖然大多數開源項目一般遵循相同的結構,但每一個項目都包含差別。 Mithril 的結構很是平坦,若是你熟悉它的API,你能夠對文件夾中的代碼進行有根據的猜想,例如渲染,路由器和請求。 另外一方面,React的結構反映了它的新架構。 維護者將負責UI更新的模塊(react-reconciler)與負責呈現DOM元素的模塊(react-dom)分開。express

這樣作的好處之一是,如今開發人員能夠經過掛鉤 react-reconciler 軟件包來編寫本身的自定義渲染器。 我最近研究過的模塊打包工具 Parcel, 也有像React這樣的包文件夾。 關鍵模塊名命名爲 parcel-bundler,它包含負責建立捆綁包,啓動熱模塊服務器和命令行工具的代碼。編程

另外一個好處 - 令我感到驚訝的是 - 能夠更輕鬆地閱讀官方JavaScript規範定義的語言如何工做。 我第一次閱讀規範是在調查 throw Error 和 throw 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上閱讀代碼時,請確保正在閱讀最新版本。能夠經過單擊用於更改分支的按鈕並選擇「tags」來查看具備最新版本標記的提交中的代碼。庫和框架永遠在進行更改,所以不須要了解可能在下一版本中刪除的內容。

另外一種不那麼簡單的閱讀源代碼的方式是我喜歡稱之爲「粗略一瞥」的方法。在我開始閱讀代碼的早期,我安裝了express.js,打開了它的/ node_modules文件夾並完成了它的依賴項。若是自述文件沒有給我一個使人滿意的解釋,我會閱讀源代碼。這樣作讓我獲得瞭如下有趣的發現:

  • Express依賴於兩個模塊,這兩個模塊合以很是不一樣的方式並對象。 merge-descriptors只添加直接在原對象上直接找到的屬性,它還合併了不可枚舉的屬性,而utils-merge 只迭代對象的可枚舉屬性以及在其原型鏈中找到的屬性。 merge-descriptors使用 Object.getOwnPropertyNames()Object.getOwnPropertyDescriptor(),而utils-merge使用for..in;
  • setprototypeof 模塊提供了一種設置實例化對象原型的跨平臺方式;
  • escape-html是一個78行模塊,用於轉義一串內容。能夠在HTML內容中進行插值。

雖然這些發現不太可能當即有用,可是庫或框架使用的依賴關係有一個大體的瞭解是有用的。

在調試前端代碼時,瀏覽器的調試工具是最好的朋友。除此以外,它們容許您隨時中止程序並檢查其狀態,跳過函數的執行或進入或退出程序。有時這不能當即生效,由於代碼被壓縮。我傾向於將其解壓並將解壓的代碼複製到 /node_modules文件夾中的相關文件中。

案例研究:Redux 的 Connent Function

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)
複製代碼

它須要四個可選參數,前三個參數都經過一個匹配函數來根據參數是否存在及其值類型來定義它們的行爲。由於如今提供匹配的第二個參數是導入 connect 的三個函數之一,我必須決定要遵循哪一個線程。

若是這些參數是函數,是檢查普通對象的實用程序或者是設置斷點來調試暴露錯誤的警告模塊,那麼如今就是學習代理函數的時候,這個函數包裹了第一個參數給 connect。在匹配函數以後,來看看 connectHOC,這個函數接受咱們的 React 組件並將它鏈接到 Redux。它是另外一個函數調用,它返回 wrapWithConnect,它實際上處理將組件鏈接到存儲的函數。

看看 connectHOC 的實現,我能夠理解爲何它須要鏈接來隱藏它的實現細節。它是 React-Redux 的核心,包含不須要經過鏈接公開的邏輯。即便這裏的探討即將結束,若是繼續,這將是查閱以前發現的參考資料的最佳時機,由於它包含對代碼庫的很是詳細的解釋。

總結

讀取源代碼起初很困難,但與任何事情同樣,隨着時間的推移變得更容易。咱們的目標不是理解一切,而是要得到不一樣的視角和新知識。關鍵是要對整個過程進行深思熟慮,並對全部事情充滿好奇。

舉個例子,我發現 isPlainObject 函數頗有趣,由於它使用了if(typeof obj!=='object'|| obj === null) return false以確保給定的參數是普通對象。當我第一次閱讀它的實現時,我想知道爲何它沒有使用 Object.prototype.toString.call(opts)!=='[object Object]',這是用更少的代碼並區分對象和對象子類型,好比 Date 對象。可是,閱讀下一行顯示,例如,在使用 connect 的開發人員返回 Date 對象的極不可能的事件中,這將由 Object.getPrototypeOf(obj)=== null 檢查處理。

isPlainObject 的另外一個吸引人的是這段代碼:

whileObject.getPrototypeOf(baseProto)!== null){
 baseProto = Object.getPrototypeOf(baseProto)
}
複製代碼

谷歌一下能夠找到相關話題,StackOverflow下的Redux問題,解釋該代碼如何檢查源自iFrame的對象。

參考

pic
相關文章
相關標籤/搜索