谈谈如何更有质量地看源码

前言
最近有很多童鞋跟鱼头说,面试的时候动不动就问源码。
也有很多童鞋遇到问题的时候,鱼头建议这些童鞋看相关库 / 框架 / 项目的源码。
但是也有很多童鞋向鱼头抱怨说:“源码太难了。”
那么源码真的是一块难啃的硬骨头吗?
其实不是的。
作为一个优秀(或说合格)的开源项目,它的代码一定不会是晦涩难懂的。不仅是代码本身,这些项目配套的注释,单元测试,示例代码,函数名以及文档一定是能够很好地辅助你读源码的。
下面就让鱼头来跟大家谈谈我自己的一些看源码心得。
(注:这不是最佳实践,只算是鱼头个人的经验,不一定适用于所有人,如果你有不同的意见,欢迎在下方评论区域留言。)

正文

看配套说明

在看一个开源项目源码之前,鱼头首先会先看其文档,不一定是会细致到各个API,但是会先理解这个项目的背景,思想,以及解决的问题是什么。
举个例子,我们来看看一个叫做runtime-hooks的开源工具库。这里先上部分代码:

function withHookAfter (originalFn, hookFn) {
return function () {
var output = originalFn.apply(this, arguments)
hookFn.apply(this, arguments)
return output
}
}

这段函数我们是看懂了,就是劫持原函数,并且在原函数执行之后,执行我们自己的逻辑。但是如果没有相关的业务经验,我们不一定能理解为什么这么干,不理解就很容易会忘记。
但是我们可以看看相关文档里,作者对这个库说明的部分节选:
假设有一个被业务广泛使用的函数,我们是否能够在既不更改调用它的业务代码,也不更改该函数源码的前提下,在其执行前后注入一段我们自定义的逻辑呢?
(来源于:基于原型链劫持的前端代码插桩实践)
哦,那么这么说我们就理解了,通过这种方案,当我们需要入侵某个函数的时候,就不需要再进行一些复杂的hack,魔改动作了。
那么我们就理解了这段函数的存在的意义,因为理解,所以这段代码,自然而然的就记住了,以后遇到类似需要的场景也能够轻而易举的想到这个方案。

看类型文件

现在有许多的开源项目都是用 TypeScript
来写的,既然是如此,通常在根目录下都会有一个 .d.ts
文件专门定义API类型的。
例如我们来看看一个开源的Web IDE库 ace.js

看看它根目录下的 ace.d.ts
里的部分代码:

export interface EventEmitter {
once(name: string, callback: Function): void;
setDefaultHandler(name: string, callback: Function): void;
removeDefaultHandler(name: string, callback: Function): void;
on(name: string, callback: Function, capturing?: boolean): void;
addEventListener(name: string, callback: Function, capturing?: boolean): void;
off(name: string, callback: Function): void;
removeListener(name: string, callback: Function): void;
removeEventListener(name: string, callback: Function): void;
}

通过上面的类型注解,我们很容易就能知道每个API的具体用法。
鱼头我为什么要拿这个来说呢,因为有的时候要解决的问题,可能只是API熟悉的问题,但是有些开源项目的文档写的并不走心,上述的这个就是这样,所以有的时候为了了解一个API,又不想花太多时间去找具体源码位置时,就可以打开类型文件来看看具体的API使用法则。

看注释

在我们深入到某一个具体的函数或者文件时,如果我们能先知道它是干啥的,那么对于我们要理解这段代码来说,是事半功倍的。
举个例子,我们来看看redux。

redux/src/createStore.js
的开头有这么一段注释:

Creates a Redux store that holds the state tree. The only way to change the data in the store is to call dispatch()
on it.

创建一个保存状态树的Redux仓库。更改仓库中数据的唯一方法是对其调用 dispatch()

哦,那么通过上面的注释,我们就知道 dispatch
方法是用来对数据进行调度的。而且是唯一的一个改变数据的方法。
有意思的是这个文件约300行代码里,估计有100行是注释,那么当我们看完注释之后,即使不看具体实现,也很容易明白它究竟做了什么。

当我们再去看具体实现的时候,我们带着“ 它主要是干了这事
”的想法去看,那么对具体实现的理解就更轻松了。

看测试样例

除了上述的几个方法,我们还可以看测试样例。其实测试样例,对于我们理解源码,或迅速上手一个陌生项目来说是非常高效的。

例如我们看vuex的 vuex/test/unit/store.spec.js
里的一个例子:

describe('Store', () => {
it('committing mutations', () => {
const store = new Vuex.Store({
state: {
a: 1
},
mutations: {
[TEST] (state, n) {
state.a += n
}
}
})
store.commit(TEST, 2)
expect(store.state.a).toBe(3)
})
it('dispatching actions, with returned Promise', done => {
const store = new Vuex.Store({
state: {
a: 1
},
mutations: {
[TEST] (state, n) {
state.a += n
}
},
actions: {
[TEST] ({ commit }, n) {
return new Promise(resolve => {
setTimeout(() => {
commit(TEST, n)
resolve()
}, 0)
})
}
}
})
expect(store.state.a).toBe(1)
store.dispatch(TEST, 2).then(() => {
expect(store.state.a).toBe(3)
done()
})
})
})

即使是不懂vuex的童鞋,通过上述的两个样例,我们也很容易就理解 mutations
是同步的,而 actions
是异步的。甚至依靠着这两个样例,我们也能轻松的上手 vuex
了。

看官方例子

还有就是官方例子辣,其实鱼头发现很多童鞋,在学一个库/框架的时候,并不喜欢看官方例子,反而喜欢看网上各种教程,虽然看教程不是不好,但是如果本身该库/框架就有官方例子,那么再去找二手知识,就有点本末倒置了。

例如我们看webpack的 webpack/examples/typescript

const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin");

module.exports = (env = "development") => ({
mode: env,
entry: {
output: "./index.ts"
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: "ts-loader",
options: {
transpileOnly: true
}
}
]
},
resolve: {
extensions: [".ts", ".js", ".json"]
},
plugins: [new ForkTsCheckerWebpackPlugin({ async: env === "production" })]
});

就这样,这是一个可以直接CV的typescript配置,多简单,这样就不需要再去网上找各种不知结果又添加了各种个人理解让你蒙圈的教程了。

总结

以上的就是鱼头日常看源码时会关注的一些点。
当然这只是鱼头的一些经验总结,不一定适用于每个人。
而且鱼头水平有限,也不能保证100%无误。
如果各位读者有任何不同意见,或者自己的经验,非常欢迎各位在下方留言区域留言。
– EOF –


了方便进行探讨和交流,我为大家建立了一个读者群,一起学习,一起进步。

:heart:爱心三连击

1.看到这里了就点个在看支持下吧,你的 「在看」
是我创作的动力。

2.关注公众号 达达前端
「每天为您分享原创或精选文章」

3.特殊阶段,带好口罩,做好个人防护。

4.添加微信【xiaoda0423】,拉你进 技术交流群
一起学习
扫码关注公众号,订阅更多精彩内容。

好文章,我

在看