1. 引言
javascript-knowledge-reading-source-code 這篇文章介紹了閱讀原始碼的重要性,精讀系列也已有八期原始碼系列文章,分別是:
- 精讀《Immer.js》原始碼
- 精讀《sqorn 原始碼》
- 精讀《Epitath 原始碼 - renderProps 新用法》
- 精讀《Htm - Hyperscript 原始碼》
- 精讀《React PowerPlug 原始碼》
- 精讀《syntax-parser 原始碼》
- 精讀《react-easy-state 原始碼》
- 精讀《Inject Instance 原始碼》
筆者自己的感悟是,度過大量原始碼的程式設計師有以下幾個特質:
- 思考具有系統性,主要體現在改一處程式碼模組時,會將專案所有檔案串聯起來整體考慮,提前評估影響面。
- 思考具有前瞻性,對已實現的方案可以快速評價所處階段(臨時 or 標準 or 可擴充),將邊界情況提前解決,將框架 BUG 降低到最小程度。
- 程式碼實現更優雅,有大量原始碼經驗做支撐,解決同樣問題時,這些程式設計師可以用更短的行數、更合適的三方庫解決問題,程式碼可讀性更好,模組拆分更合理,更利於維護。
既然閱讀原始碼這麼重要,那麼怎麼才能讀好原始碼呢?本週精讀的文章就是一篇方法論文章,告訴你如何更好的閱讀原始碼。
2. 概述
原文分三個部分:閱讀原始碼的好處、閱讀原始碼的技巧、以及 Redux Connect 的案例研究。
閱讀原始碼的好處
閱讀原始碼有助於理解抽象的概念,比如虛擬 DOM;有助於做方案調研,而不僅僅只看 Github star 數量;瞭解優秀框架目錄結構的設計;看到一些陌生的工具函式,還可能激發你對 JS 規範的查閱,這種問題驅動的方式也是筆者推薦的 JS 規範學習方式。
閱讀原始碼的技巧
最好的閱讀原始碼方式是看文章,如果原始碼的作者有寫原始碼解讀文章,這就是最省力的方式。雖然直接看程式碼可以瞭解到所有細節,但當你不清楚設計思路時,僅看原始碼可能會找不到方向,而讀原始碼的最終目的是找到核心的設計理念,如果一個框架沒有自己核心設計理念,這個框架也不值得誕生,更不值得被閱讀。如果框架的作者已經將框架核心理念寫成了文章,那讀文章就是最佳方案。
還有一種方式是斷點,寫一個最小程式,在框架執行入口出打下斷點,然後按照執行路徑一步步理解。雖然執行路徑中會存在大量無關的函式干擾精力,但如果你足夠有耐心,當斷點走完時一定會有所收穫。
原文還提到了一種看原始碼方式,即沒有目的的尋寶。在尋找框架主要思路的過程中,遇到一些有意思的函式,可以停下來仔細閱讀,可能會發現一些對你有啟發的程式碼片段。
Redux Connect 案例研究
原文以 Redux Connect 作為案例介紹研究思路。
首先看到 Connect 的功能 “包裝元件” 後,就要問自己兩個問題:
- Connect 是如何實現包裝元件後原樣返回元件,但卻增強元件功能的?(高階元件知識)
- 瞭解這個設計模式後,如何利用已有的文件實現它?
通過建立一個使用 Connect 的基本程式:
class MarketContainer extends Component {
}
const mapDispatchToProps = dispatch => {
return {
updateSummary: (summary, start, today) => dispatch(updateSummary(summary, start, today))
}
}
export default connect(null, mapDispatchToProps)(MarketContainer);
複製程式碼
比如從生成 connect 函式的 createConnect 我們就可以學習到 Facade Pattern - 門面模式。
從 createConnect
函式呼叫處:
export function createConnect({
connectHOC = connectAdvanced,
mapStateToPropsFactories = defaultMapStateToPropsFactories,
mapDispatchToPropsFactories = defaultMapDispatchToPropsFactories,
mergePropsFactories = defaultMergePropsFactories,
selectorFactory = defaultSelectorFactory
} = {})
複製程式碼
我們可以學習到解構預設函式引數的知識點。
總之,在學習原始碼的過程中,可以瞭解到一些新的 JS 特性,一些設計模式,這些都是額外的寶藏,不斷理解並學會運用到自己寫的框架裡,就實現了原始碼學習的目的。
3. 精讀
原文介紹了學習原始碼的兩個技巧,並利用 Redux Connect 例項說明了原始碼學習過程中可以學到許多周邊知識,都讓我們受益匪淺。
筆者結合之前寫過的八篇原始碼分析文章,把最重要的設計思路提取出來,以實際的例子展示閱讀原始碼能給我們思維帶來哪些幫助。
Immerjs 原始碼的精華
Immer 可以讓我們以 Mutable 的方式更新物件,最終得到一個 Immutable 物件:
this.setState(produce(state => (state.isShow = true)))
複製程式碼
詳細原始碼解讀可以閱讀 這裡。
核心思路是利用 Proxy 把髒活累活做掉。上面的例子中,state
已經是一個代理(Proxy)物件,通過自定義 setting
不斷遞迴進行淺拷貝,最後返回一個新引用的頂層物件作為 produce
的返回值。
從 Immerjs 中,我們學到了 Proxy 可以化腐朽為神奇的用法,比看任何 Proxy 介紹文章都直觀。
sqorn 原始碼的精華
sqorn 是一個 sql orm,舉例來看:
const sq = require("sqorn-pg")();
const Person = sq`person`,
Book = sq`book`;
// SELECT
const children = await Person`age < ${13}`;
// "select * from person where age < 13"
複製程式碼
詳細原始碼解讀可以閱讀 這裡
核心思路是在鏈式呼叫過程中建立 context 儲存結構,並在鏈式呼叫的時候不斷填充 context 資訊,最終拿到的是一個結構化 context 物件,生成 sql 語句也就簡單了。
從 sqorn 中,我們學到了如何實現鏈式呼叫 init().a().b().c().print()
最後拿到一個綜合的結果,原理是內部維護了一個不斷修改的物件。不論前端 React Vue 還是後端框架 Koa 等,一般都有內建的 context,一般實現這種優雅語法的框架內部都會維護 context。
Epitath 原始碼的精華
Epitath 在 React Hooks 之前出來,解決了高階函式地獄的問題:
const App = epitath(function*() {
const { count } = yield <Counter />
const { on } = yield <Toggle />
return (
<MyComponent counter={count} toggle={on} />
)
})
<App />
複製程式碼
詳細原始碼解讀可以閱讀 這裡
其核心是利用 generator
的迭代,將 React 元件的平級結構還原成巢狀結構,將巢狀寫法打平了:
yield <A>
yield <B>
yield <C>
// 等價於
<A>
<B>
<C />
</B>
</A>
複製程式碼
從 epitath 中,我們瞭解到 generator
原來可以這麼用,正因為其執行是多次迭代的,因此我們可以利用這個特性,改變程式碼執行結構。
Htm - Hyperscript 原始碼的精華
Htm 將模版語法很自然的融入到了 html 中:
html`
<div class="app">
<${Header} name="ToDo's (${page})" />
<ul>
${todos.map(
todo => html`
<li>${todo}</li>
`
)}
</ul>
<button onClick=${() => this.addTodo()}>Add Todo</button>
<${Footer}>footer content here<//>
</div>
`;
複製程式碼
詳細原始碼解讀可以閱讀 這裡
其核心是怎麼根據模版拿到 dom 元素的 AST?拿到 AST 後就方便生成後續內容了。
作者的辦法是:
const TEMPLATE = document.createElement("template");
TEMPLATE.innerHTML = str;
複製程式碼
這樣 TEMPLATE 就自帶了 AST 解析,這是利用瀏覽器自帶的 AST 解析拿到了 AST。從 Htm 中,我們學到了 innerHTML
可以生成標準 AST,所以只要有瀏覽器執行環境,需要拿 AST 的時候,不需要其他庫,innerHTML
就是最好的方案。
React PowerPlug 原始碼的精華
React PowerPlug 是一個利用 render props 進行狀態管理的工具庫。
它可以在 JSX 中對任意粒度插入狀態管理:
<Value initial="React">
{({ value, set, reset }) => (
<>
<Select
label="Choose one"
options={["React", "Preact", "Vue"]}
value={value}
onChange={set}
/>
<Button onClick={reset}>Reset to initial</Button>
</>
)}
</Value>
複製程式碼
詳細原始碼解讀可以閱讀 這裡
這個庫的核心就是利用 render props 解決 JSX 區域性狀態管理的痛點,通過讀原始碼瞭解 render props 的使用方式是這個原始碼帶給你的最大價值。
syntax-parser 原始碼的精華
syntax-parser 是一個 JS 版語法解器生成器,筆者也是作者,使用方式:
import { createParser, chain, matchTokenType, many } from "syntax-parser";
const root = () => chain(addExpr)(ast => ast[0]);
const addExpr = () =>
chain(matchTokenType("word"), many(addPlus))(ast => ({
left: ast[0].value,
operator: ast[1] && ast[1][0].operator,
right: ast[1] && ast[1][0].term
}));
const addPlus = () =>
chain("+"), root)(ast => ({
operator: ast[0].value,
term: ast[1]
}));
const myParser = createParser(
root, // Root grammar.
myLexer // Created in lexer example.
);
複製程式碼
詳細原始碼解讀可以閱讀 這裡
syntax-parser 的核心是利用雙向連結串列實現了可回溯的語法解析器,瞭解了這個庫,你可以自己實現 JS 呼叫堆疊,並在任意時候返回某個之前的執行狀態重新執行。同時這個庫的原始碼也會加強你對連結串列的理解,以及擴充你對連結串列使用場景的想象。
react-easy-state 原始碼的精華
react-easy-state 利用 Proxy 建立了一個簡易的全域性資料流管理方式:
import React from "react";
import { store, view } from "react-easy-state";
const counter = store({ num: 0 });
const increment = () => counter.num++;
export default view(() => <button onClick={increment}>{counter.num}</button>);
複製程式碼
詳細原始碼解讀可以閱讀 這裡
react-easy-state 利用了 observer-util 實現主要功能,從中我們能學到最有價值的就是 Proxy 與 React 結合的設計理念,即利用 getter
setter
實現資料與檢視的雙向繫結,或者叫依賴追蹤,更多細節就不在這裡展開,感興趣可以閱讀筆者之前寫的 抽絲剝繭,實現依賴追蹤 一節。
Inject Instance 原始碼的精華
inject-instance 是一個 Class 實現依賴注入的庫:
import {inject} from 'inject-instance'
import B from './B'
class A {
@inject('B') private b: B
public name = 'aaa'
say() {
console.log('A inject B instance', this.b.name)
}
}
複製程式碼
詳細原始碼解讀可以閱讀 這裡
主要對我們有兩個啟發,第一可以利用裝飾器為物件儲存一些額外資訊,這些資訊在必要的時候我們可以用到;第二是依賴注入並不複雜,通過提前例項化後,可以解決迴圈依賴的問題,即所有迴圈依賴問題都可以通過加一個父級解決。
4. 總結
閱讀程式碼不是目的,讀懂原始碼背後要表達的核心設計思路才是目的。比如寫腳手架,閱讀了大量腳手架原始碼的人寫出的程式碼,與一個沒有經驗的人寫出的程式碼會有天壤之別,這之間的差距就是對一些設計模式、三方庫、結構設計的經驗差距。
只學習理論太空洞,只看程式碼又太侷限,學會從程式碼中看出理論才是最佳學習方式。
如果你想參與討論,請 點選這裡,每週都有新的主題,週末或週一釋出。前端精讀 - 幫你篩選靠譜的內容。
關注 前端精讀微信公眾號
版權宣告:自由轉載-非商用-非衍生-保持署名(創意共享 3.0 許可證)