最近在分析一些框架源码,在写笔记的时候,一些函数的调用栈希望用流程图的形式记录下来,打开 http://draw.io[1] 就是一顿操作,画了几个调用栈之后,感觉很麻烦。于是蹲在厕所里的我开始思考了,调用栈既然可以用 console.trace()
打印出来,那是不是也可以把数据记录下来直接画出流程图来?
当然我从不喜欢造轮子,首先熟练的打开 google
操作一波,发现地球之大,竟然没有我想要的工具?没有 JS 调用栈可视化工具?怎么办?在继续用 draw.io
手画和自己造轮子之间我陷入了深思。
当然我最后选择了造轮子,不然就没有这篇文章了。
比如说有这样一份代码:
// 轮子
import houndTrace from 'hound-trace-ui';
function f() {}
function e() {}
function d() { f() }
const b = () => { d(); e() };
const c = function () { d() };
function a() { b(); c() }
houndTrace.start();
a();
houndTrace.end();
可视化输出:
Jietu20190815-210018.jpg代码写的好不好,工具好不好用,不重要,一定要有酷炫的名字,那就是 hound-trace
,看这个谷歌翻译出来的名字多么清新脱俗,光芒四射。
首先想到的是能不能在函数调用前后做点什么?作为 21 世纪的程序员,下手前当然是先 google
和 stackoverflow
一波,看看能不能轻轻松松当个搬运工。逛了半天,靠谱的答案似乎有这些:
// 偷天换日大法
var old = UIIntentionalStream.instance.loadOlderPosts;
UIIntentionalStream.instance.loadOlderPosts = function() {
// hook before call
old();
// hook after call
};
// 原型拓展大法
Function.prototype.before = function (callback) {
var that = this;
return (function() {
callback.apply(this, arguments);
return (that.apply(this, arguments));
});
}
Function.prototype.after = function (callback) {
var that = this;
return (function() {
var result = that.apply(this, arguments);
callback.apply(this, arguments);
return (result);
});
}
var both = test.before(function(a) {
console.log('Before. Parameter = ', a);
}).after(function(a) {
console.log('After. Parameter = ', a);
});
both(17);
看起来不错,可是这,我要是看个 react
源码,想拿到调用栈信息。函数调用岂不都要重写个遍?不靠谱,还不如去手画呢。
找啊找啊找,灵光一闪?AST。运行的时候不行,直接改代码不就完了。就这么干?于是就写了个 babel
插件,在代码里下点毒。babel-plugin-hound-trace[3] 这个插件干啥呢?
// ...
module.exports = function (babel) {
return {
visitor: {
// 在我们的代码里面遇到函数声明语句,函数表达式,箭头函数表达式的时候
// 该插件会注入一些代码
FunctionDeclaration: (path) => {
// ...
},
FunctionExpression: expressionHandle.bind(null, babel),
ArrowFunctionExpression: expressionHandle.bind(null, babel)
}
};
};
// ...
比如源码里的函数如下:
function test(a, b, c) {
const cj = 'cj';
}
下毒之后(经过这个插件处理之后):
function test(a, b, c) {
let __traceParent__ = window.__traceParent__;
let __traceOldParent__ = __traceParent__;
if (window.__trace__) {
if (!__traceParent__.next) {
__traceParent__.next = [];
}
const current = {
name: 'test',
params: [\\"a\\", \\"b\\", \\"c\\"]
};
__traceParent__.next.push(current);
window.__traceParent__ = current;
}
const cj = 'cj';
window.__traceParent__ = __traceOldParent__;
}
注入的代码比较奇怪,因为实现的思路是借助 window
上的全局变量来做这个事情,所以看起来奇怪。(暂时还没想其它好方法)
注入了代码之后就简单了 ,可以看到代码里注入了 window.__trace__
这个变量用于是否记录该函数,所以 hound-trace
包的代码就自然出来了:
let trace = false;
// 开始记录调用栈
function __NoTraceHook__start() {
if (trace) { return }
window.__trace__ = trace = true;
window.__traceParent__ = {};
}
// 结束记录调用栈
function __NoTraceHook__end(callback) {
if (!trace) { return }
window.__trace__ = trace = false;
callback && callback(window.__traceParent__);
}
export default {
start: __NoTraceHook__start,
end: __NoTraceHook__end
};
到这里,就差可视化渲染了,考虑到 UI
层是比较个性的,所以又拆了个包出来 hound-trace-ui
底层调用 hound-trace
的 API ,这样以后就能随意加皮肤了。
hound-trace-ui
其实很简单,就是在 __NoTraceHook__end
的回调里可以拿到调用栈的数据,然后怎么用这个数据就随意了,这个包里使用的是 mermaid
做可视化(因为简单):
// 调用 hound-trace 包代码
import houndTrace from '../../hound-trace/src/index';
// 渲染数据逻辑
import renderCallStack from './renderCallStack';
import './index.css';
function start() {
houndTrace.start();
}
// 包装底层 API
function end() {
houndTrace.end(callStack => {
setTimeout(() => {
// 调用 mermaid 包渲染数据
renderCallStack(callStack);
}, 14);
});
}
export default {
start,
end,
endAndRenderCallStack: end
};
好吧,就这样世界上又多了一个轮子。
感兴趣的可以去 star ,当然更希望大家给出奇淫技巧一起完善。—> hound-trace[4] <--
也可以加入群讨论该工具的相关技术: 878244740。
[1]
http://draw.io: http://draw.io/
[2]hound-trace: https://github.com/cj0x39e/hound-trace
[3]babel-plugin-hound-trace: https://github.com/cj0x39e/hound-trace/tree/master/packages/babel-plugin-hound-trace
[4]hound-trace: https://github.com/cj0x39e/hound-trace