# 1. 引言

[javascript-knowledge-reading-source-code](https://www.smashingmagazine.com/2019/07/javascript-knowledge-reading-source-code/) 这篇文章介绍了阅读源码的重要性，精读系列也已有八期源码系列文章，分别是：

- [精读《Immer.js》源码](https://github.com/dt-fe/weekly/blob/v2/048.%E7%B2%BE%E8%AF%BB%E3%80%8AImmer.js%E3%80%8B%E6%BA%90%E7%A0%81.md)
- [精读《sqorn 源码》](https://github.com/dt-fe/weekly/blob/v2/073.%E7%B2%BE%E8%AF%BB%E3%80%8Asqorn%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
- [精读《Epitath 源码 - renderProps 新用法》](https://github.com/dt-fe/weekly/blob/v2/075.%E7%B2%BE%E8%AF%BB%E3%80%8AEpitath%20%E6%BA%90%E7%A0%81%20-%20renderProps%20%E6%96%B0%E7%94%A8%E6%B3%95%E3%80%8B.md)
- [精读《Htm - Hyperscript 源码》](https://github.com/dt-fe/weekly/blob/v2/082.%E7%B2%BE%E8%AF%BB%E3%80%8AHtm%20-%20Hyperscript%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
- [精读《React PowerPlug 源码》](https://github.com/dt-fe/weekly/blob/v2/092.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20PowerPlug%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
- [精读《syntax-parser 源码》](https://github.com/dt-fe/weekly/blob/v2/093.%E7%B2%BE%E8%AF%BB%E3%80%8Asyntax-parser%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
- [精读《react-easy-state 源码》](https://github.com/dt-fe/weekly/blob/v2/098.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-easy-state%20%E6%BA%90%E7%A0%81%E3%80%8B.md)
- [精读《Inject Instance 源码》](https://github.com/dt-fe/weekly/blob/v2/110.%E7%B2%BE%E8%AF%BB%E3%80%8AInject%20Instance%20%E6%BA%90%E7%A0%81%E3%80%8B.md)

笔者自己的感悟是，读过大量源码的程序员有以下几个特质：

1. 思考具有系统性，主要体现在改一处代码模块时，会将项目所有文件串联起来整体考虑，提前评估影响面。
2. 思考具有前瞻性，对已实现的方案可以快速评价所处阶段（临时 or 标准 or 可拓展），将边界情况提前解决，将框架 BUG 降低到最小程度。
3. 代码实现更优雅，有大量源码经验做支撑，解决同样问题时，这些程序员可以用更短的行数、更合适的三方库解决问题，代码可读性更好，模块拆分更合理，更利于维护。

既然阅读源码这么重要，那么怎么才能读好源码呢？本周精读的文章就是一篇方法论文章，告诉你如何更好的阅读源码。

# 2. 概述

原文分三个部分：阅读源码的好处、阅读源码的技巧、以及 Redux Connect 的案例研究。

## 阅读源码的好处

阅读源码有助于理解抽象的概念，比如虚拟 DOM；有助于做方案调研，而不仅仅只看 Github star 数量；了解优秀框架目录结构的设计；看到一些陌生的工具函数，还可能激发你对 JS 规范的查阅，这种问题驱动的方式也是笔者推荐的 JS 规范学习方式。

## 阅读源码的技巧

最好的阅读源码方式是看文章，如果源码的作者有写源码解读文章，这就是最省力的方式。虽然直接看代码可以了解到所有细节，但当你不清楚设计思路时，仅看源码可能会找不到方向，而读源码的最终目的是找到核心的设计理念，如果一个框架没有自己核心设计理念，这个框架也不值得诞生，更不值得被阅读。如果框架的作者已经将框架核心理念写成了文章，那读文章就是最佳方案。

还有一种方式是断点，写一个最小程序，在框架执行入口出打下断点，然后按照执行路径一步步理解。虽然执行路径中会存在大量无关的函数干扰精力，但如果你足够有耐心，当断点走完时一定会有所收获。

原文还提到了一种看源码方式，即没有目的的寻宝。在寻找框架主要思路的过程中，遇到一些有意思的函数，可以停下来仔细阅读，可能会发现一些对你有启发的代码片段。

## Redux Connect 案例研究

原文以 Redux Connect 作为案例介绍研究思路。

首先看到 Connect 的功能 “包装组件” 后，就要问自己两个问题：

1. Connect 是如何实现包装组件后原样返回组件，但却增强组件功能的？（高阶组件知识）
2. 了解这个设计模式后，如何利用已有的文档实现它？

通过创建一个使用 Connect 的基本程序：

```js
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](https://github.com/reduxjs/react-redux/blob/master/src/connect/connect.js#L46) 我们就可以学习到 [Facade Pattern](http://jargon.js.org/_glossary/FACADE_PATTERN.md) - 门面模式。

从 `createConnect` 函数调用处：

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

我们可以学习到解构默认函数参数的知识点。

总之，在学习源码的过程中，可以了解到一些新的 JS 特性，一些设计模式，这些都是额外的宝藏，不断理解并学会运用到自己写的框架里，就实现了源码学习的目的。

# 3. 精读

原文介绍了学习源码的两个技巧，并利用 Redux Connect 实例说明了源码学习过程中可以学到许多周边知识，都让我们受益匪浅。

笔者结合之前写过的八篇源码分析文章，把最重要的设计思路提取出来，以实际的例子展示阅读源码能给我们思维带来哪些帮助。

## Immerjs 源码的精华

Immer 可以让我们以 Mutable 的方式更新对象，最终得到一个 Immutable 对象：

```js
this.setState(produce(state => (state.isShow = true)))
```

> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/048.%E7%B2%BE%E8%AF%BB%E3%80%8AImmer.js%E3%80%8B%E6%BA%90%E7%A0%81.md#3-%E7%B2%BE%E8%AF%BB)。

核心思路是利用 Proxy 把脏活累活做掉。上面的例子中，`state` 已经是一个代理（Proxy）对象，通过自定义 `setting` 不断递归进行浅拷贝，最后返回一个新引用的顶层对象作为 `produce` 的返回值。

从 Immerjs 中，我们学到了 Proxy 可以化腐朽为神奇的用法，比看任何 Proxy 介绍文章都直观。

## sqorn 源码的精华

sqorn 是一个 sql orm，举例来看：

```js
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"
```

> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/073.%E7%B2%BE%E8%AF%BB%E3%80%8Asqorn%20%E6%BA%90%E7%A0%81%E3%80%8B.md#3-%E7%B2%BE%E8%AF%BB)

核心思路是在链式调用过程中创建 context 存储结构，并在链式调用的时候不断填充 context 信息，最终拿到的是一个结构化 context 对象，生成 sql 语句也就简单了。

从 sqorn 中，我们学到了如何实现链式调用 `init().a().b().c().print()` 最后拿到一个综合的结果，原理是内部维护了一个不断修改的对象。不论前端 React Vue 还是后端框架 Koa 等，一般都有内置的 context，一般实现这种优雅语法的框架内部都会维护 context。

## Epitath 源码的精华

Epitath 在 React Hooks 之前出来，解决了高阶函数地狱的问题：

```js
const App = epitath(function*() {
  const { count } = yield <Counter />
  const { on } = yield <Toggle />

  return (
    <MyComponent counter={count} toggle={on} />
  )
})

<App />
```

> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/075.%E7%B2%BE%E8%AF%BB%E3%80%8AEpitath%20%E6%BA%90%E7%A0%81%20-%20renderProps%20%E6%96%B0%E7%94%A8%E6%B3%95%E3%80%8B.md#3-%E7%B2%BE%E8%AF%BB)

其核心是利用 `generator` 的迭代，将 React 组件的平级结构还原成嵌套结构，将嵌套写法打平了：

```plain
yield <A>
yield <B>
yield <C>
// 等价于
<A>
  <B>
    <C />
  </B>
</A>
```

从 epitath 中，我们了解到 `generator` 原来可以这么用，正因为其执行是多次迭代的，因此我们可以利用这个特性，改变代码运行结构。

## Htm - Hyperscript 源码的精华

Htm 将模版语法很自然的融入到了 html 中：

```js
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>
`;
```

> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/082.%E7%B2%BE%E8%AF%BB%E3%80%8AHtm%20-%20Hyperscript%20%E6%BA%90%E7%A0%81%E3%80%8B.md#3-%E7%B2%BE%E8%AF%BB)

其核心是怎么根据模版拿到 dom 元素的 AST？拿到 AST 后就方便生成后续内容了。

作者的办法是：

```js
const TEMPLATE = document.createElement("template");
TEMPLATE.innerHTML = str;
```

这样 TEMPLATE 就自带了 AST 解析，这是利用浏览器自带的 AST 解析拿到了 AST。从 Htm 中，我们学到了 `innerHTML` 可以生成标准 AST，所以只要有浏览器运行环境，需要拿 AST 的时候，不需要其他库，`innerHTML` 就是最好的方案。

## React PowerPlug 源码的精华

React PowerPlug 是一个利用 render props 进行状态管理的工具库。

它可以在 JSX 中对任意粒度插入状态管理：

```js
<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>
```

> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/092.%E7%B2%BE%E8%AF%BB%E3%80%8AReact%20PowerPlug%20%E6%BA%90%E7%A0%81%E3%80%8B.md#2-%E7%B2%BE%E8%AF%BB)

这个库的核心就是利用 render props 解决 JSX 局部状态管理的痛点，通过读源码了解 render props 的使用方式是这个源码带给你的最大价值。

## syntax-parser 源码的精华

syntax-parser 是一个 JS 版语法解器生成器，笔者也是作者，使用方式：

```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.
);
```

> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/093.%E7%B2%BE%E8%AF%BB%E3%80%8Asyntax-parser%20%E6%BA%90%E7%A0%81%E3%80%8B.md#2-%E7%B2%BE%E8%AF%BB)

syntax-parser 的核心是利用双向链表实现了可回溯的语法解析器，了解了这个库，你可以自己实现 JS 调用堆栈，并在任意时候返回某个之前的执行状态重新执行。同时这个库的源码也会加强你对链表的理解，以及拓展你对链表使用场景的想象。

## react-easy-state 源码的精华

react-easy-state 利用 Proxy 创建了一个简易的全局数据流管理方式：

```js
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>);
```

> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/098.%E7%B2%BE%E8%AF%BB%E3%80%8Areact-easy-state%20%E6%BA%90%E7%A0%81%E3%80%8B.md)

react-easy-state 利用了 [observer-util](https://github.com/nx-js/observer-util) 实现主要功能，从中我们能学到最有价值的就是 Proxy 与 React 结合的设计理念，即利用 `getter` `setter` 实现数据与视图的双向绑定，或者叫依赖追踪，更多细节就不在这里展开，感兴趣可以阅读笔者之前写的 [抽丝剥茧，实现依赖追踪](https://github.com/dt-fe/weekly/blob/master/35.%E7%B2%BE%E8%AF%BB%E3%80%8Adob%20-%20%E6%A1%86%E6%9E%B6%E5%AE%9E%E7%8E%B0%E3%80%8B.md#%E6%8A%BD%E4%B8%9D%E5%89%A5%E8%8C%A7%E5%AE%9E%E7%8E%B0%E4%BE%9D%E8%B5%96%E8%BF%BD%E8%B8%AA) 一节。

## Inject Instance 源码的精华

inject-instance 是一个 Class 实现依赖注入的库：

```js
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)
  }
}
```

> 详细源码解读可以阅读 [这里](https://github.com/dt-fe/weekly/blob/v2/110.%E7%B2%BE%E8%AF%BB%E3%80%8AInject%20Instance%20%E6%BA%90%E7%A0%81%E3%80%8B.md#2-%E7%B2%BE%E8%AF%BB)

主要对我们有两个启发，第一可以利用装饰器为对象存储一些额外信息，这些信息在必要的时候我们可以用到；第二是依赖注入并不复杂，通过提前实例化后，可以解决循环依赖的问题，即所有循环依赖问题都可以通过加一个父级解决。

# 4. 总结

阅读代码不是目的，读懂源码背后要表达的核心设计思路才是目的。比如写脚手架，阅读了大量脚手架源码的人写出的代码，与一个没有经验的人写出的代码会有天壤之别，这之间的差距就是对一些设计模式、三方库、结构设计的经验差距。

只学习理论太空洞，只看代码又太局限，学会从代码中看出理论才是最佳学习方式。

> 讨论地址是：[精读《源码学习》 · Issue #179 · dt-fe/weekly](https://github.com/dt-fe/weekly/issues/179)

**如果你想参与讨论，请 [点击这里](https://github.com/dt-fe/weekly)，每周都有新的主题，周末或周一发布。前端精读 - 帮你筛选靠谱的内容。**

> 关注 **前端精读微信公众号**

<img width=200 src="https://img.alicdn.com/tfs/TB165W0MCzqK1RjSZFLXXcn2XXa-258-258.jpg">

> 版权声明：自由转载-非商用-非衍生-保持署名（[创意共享 3.0 许可证](https://creativecommons.org/licenses/by-nc-nd/3.0/deed.zh)）
