做者:Carl Mungazijavascript
翻譯:瘋狂的技術宅html
原文:www.smashingmagazine.com/2019/07/jav…前端
未經容許嚴禁轉載java
你還記得本身第一次深刻挖掘經常使用的庫或框架的源代碼時的情景嗎?對我而言,那一刻是我三年前做爲前端開發人員的第一份工做。node
咱們剛剛完成了用於建立在線課程的內部遺留框架的重寫。在開始重寫時,咱們花時間研究了許多不一樣的解決方案,包括 Mithril、Inferno、Angular、React、Aurelia、Vue 和 Polymer。由於我是一個萌新(我剛重新聞轉向網絡開發),我記得每一個框架的複雜性都讓人感到懼怕,並且不理解框架的工做方式。react
當我開始更深刻地研究咱們選擇的 Mithril 框架時,個人能力增加了。從那之後,我對 JavaScript 的瞭解以及通常的編程方式獲得了很大的提升,我花了不少時間深刻研究天天在工做種或在本身的項目中使用的庫。在本文中,我將分享一些分析庫或框架的方法。git
經過 Mithril 的 hyperscript 功能介紹如何去閱讀源代碼。github
閱讀源代碼的好處之一是可使你學到更多的東西。當我第一次看到 Mithril 的代碼庫時,對虛擬 DOM 的含義只有一個模糊的概念。當我讀完時,就知道了虛擬 DOM 是一種技術,它涉及建立描述用戶界面的對象樹應該是什麼樣的。而後使用 DOM API(例如 document.createElement
)將該樹轉換爲 DOM 元素。經過建立描述用戶界面將來狀態的新樹,而後將其與舊樹中的對象進行比較來執行更新。web
以前我已經在各類文章和教程中讀到過這些內容,雖然頗有幫助,可是在程序的上下文中可以觀察它對我來講是很是有啓發性的。它還告訴我在比較不一樣的框架時要問哪些問題。例如我如今不是去查看 GitHub 上的 star 數量,而是會問「每一個框架執行更新的方式如何影響性能和用戶體驗?」這樣的問題。chrome
另外一個好處是增長你對良好應用架構的理解。雖然大多數開源項目一般與其存儲庫遵循相同的結構,但每一個項目都包含差別。Mithril 的結構很是扁平,若是你熟悉它的 API,能夠對文件夾中的代碼進行有根據的猜想,好比render
、router
和 request
等。另外一方面,React 的結構也反映了它的新架構。維護者將負責 UI 更新的模塊(react-reconciler
)與負責渲染 DOM 元素的模塊(react-dom
)分開。
這樣作的好處之一是,開發人員如今能夠經過 hook 到 react-reconciler
包來編寫本身的自定義渲染器。我最近研究過的模塊捆綁包 Parcel 也有像 React 這樣的 packages
文件夾。密鑰模塊名爲 parcel-bundler
,它包含負責建立捆綁包、熱啓動模塊服務器和命令行工具的代碼。
不久以後,你正在閱讀的源代碼將引導你進入 JavaScript 規範。
另外一個令我感到驚訝的好處是:你能夠更輕鬆地閱讀官方 JavaScript 規範,該規範定義了語言的工做方式。我第一次閱讀規範的時候是在分析 throw Error
和 throw new Error
之間的區別。之因此要分析這個,是由於我注意到 Mithril 在其 m
函數的實現中使用了 throw Error
,我想知道這樣是否是比 throw new Error
更好。從那之後,我也學會了邏輯運算符 &&
和 ||
不必定返回布爾值,找到了控制 ==
等式運算符如何強制賦值的[規則](http ://www.ecma-international.org/ecma-262/#sec-abstract-equality-comparison)和Object.prototype.toString.call({})
返回 '[object Object]'
的[緣由](http://www.ecma- international.org/ecma-262/#sec-object.prototype.tostring) 。
有不少方法能夠處理源代碼。我發現最簡單的方法是,從你選擇的庫中挑選一種方法,並去記錄調用它時會發生什麼。不是去記錄每一步,而是要嘗試肯定其總體流程和結構。
我最近用這種方法閱讀了 ReactDOM.render
的代碼 ,所以學到了不少關於 React Fibre 及其實現背後的一些原理。值得慶幸的是,因爲 React 是一個流行的框架,我在同一個問題上看到過不少其餘開發人員撰寫的文章,這也加快了這個過程。
這深刻探討並向我介紹了co-operative scheduling,window.requestIdleCallback
方法和真實的鏈表的示例(React 經過把更新放入隊列來處理更新,這是優先更新的鏈表)。執行此類操做時,建議用庫建立一個很是基本的程序。這可使得調試時更容易,由於你不用去處理由其餘庫引發的棧跟蹤信息。
若是沒有對代碼進行深刻研究,我會正在處理的項目中打開 /node_modules
文件夾,或者轉到 GitHub 存儲庫。當我遇到錯誤或有趣的功能時,一般會發生這種狀況。在 GitHub 上閱讀代碼時,請確保你正在閱讀最新版本。你能夠經過單擊用於更改分支的按鈕,並選擇 「tags」 來查看帶有最新版本標記的代碼。庫和框架永遠在持續更新,因此你不但願把精力花費在下一版本中可能會刪除的內容。
還有另外一種閱讀源代碼的方式,我喜歡稱之爲「粗略一瞥」,這種方法並不那麼簡單。在我剛剛開始閱讀代碼的時候安裝了 express.js,我打開了它的 /node_modules
文件夾並瀏覽了它的依賴項。若是 README
沒有給我一個滿意的解釋,我就會閱讀源代碼。這樣作讓我獲得了一些有趣的發現:
merge-descriptors
只添加在源對象上直接找到的屬性,它還合併了不可枚舉的屬性,而 utils-merge
只迭代對象的可枚舉屬性以及在其原型鏈中找到的屬性。 merge-descriptors
使用 Object.getOwnPropertyNames()
和 Object.getOwnPropertyDescriptor()
,而 utils-merge
使用了 for..in
;setprototypeof
模塊提供了一種跨平臺設置實例化對象原型的方式;escape-html
是一個有 78 行代碼的模塊,用於轉義字符串內容,所以能夠用它在 HTML 內容中進行插值。雖然閱讀源代碼的結果不太可能當即就能用得上,可是可以使你對本身使用的庫或框架的依賴關係有一個大體的瞭解,這是很是有用的。
在調試前端代碼時,瀏覽器的調試工具是你最好的朋友。除此以外,它們容許你隨時暫停程序並檢查其狀態、跳過函數的執行、進入或退出程序。不過有時這不可能當即作到,由於代碼有可能已經被壓縮過。我傾向於取消它們的通知,並將未經壓縮的代碼複製到 /node_modules
目錄中的相關文件裏。
像其餘程序同樣進行調試。造成一個假設,而後進行測試。
React-Redux 是一個用於管理 React 應用狀態的庫。在處理諸如此類的庫時,我首先會搜索已經編寫過有關其實現的文章。在這個案例研究中,我遇到了這篇文章(blog.isquaredsoftware.com/2018/11/rea…
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
:沒法解析 undefined
或 null
的屬性 connectHOC
。這是由於該函數沒有默認參數能夠依賴。
注意:有關此內容的更多信息,請閱讀 David Walsh 的文章(davidwalsh.name/destructuri…
createConnect
自己在其函數體中沒有任何功能。它返回一個名爲 connect
的函數,我在代碼裏使用的函數:
export default connect(null, mapDispatchToProps)(MarketContainer)
複製代碼
它須要四個參數,都是可選的,前三個參數根據參數是否存在及其值類型來定義它們的行爲,這是經過 match
函數來實現的。如今由於提供給 match
的第二個參數是導入 connect
的三個函數之一,我必須決定應該遵循哪一個線程。
在這裏學習的重點是:若是這些參數是函數,用於將第一個參數包裝爲 connect
的代理函數,isPlainObject
用於檢查普通對象或 warning
模塊,它揭示瞭如何將調試器設置爲中斷全部異常。在匹配函數以後,咱們來到 connectHOC
,這個函數接受咱們的 React 組件並將它鏈接到 Redux。它是另外一個函數調用,返回 wrapWithConnect
,實際上它用來處理將組件鏈接到 store 的函數。
看一看 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 issue,它們解釋了該代碼如何進行處理的案例,例如檢查源自 iFrame 的對象。