前言

这是学习源码整体架构第四篇。整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现。文章学习的是打包整合后的代码,不是实际仓库中的拆分的代码。

其余三篇分别是:

1.学习 jQuery 源码整体架构,打造属于自己的 js 类库

2.学习underscore源码整体架构,打造属于自己的函数式编程类库

3.学习 lodash 源码整体架构,打造属于自己的函数式编程类库

感兴趣的读者可以点击阅读。

导读
本文通过梳理前端错误监控知识、介绍 sentry错误监控原理、 sentry初始化、 Ajax上报、 window.onerror、window.onunhandledrejection几个方面来学习 sentry的源码。

开发微信小程序,想着搭建小程序错误监控方案。最近用了丁香园 开源的 Sentry 小程序 SDKsentry-miniapp。 顺便研究下 sentry-javascript仓库 的源码整体架构,于是有了这篇文章。

本文分析的是打包后未压缩的源码,源码总行数五千余行,链接地址是:https://browser.sentry-cdn.com/5.7.1/bundle.js, 版本是 v5.7.1

本文示例等源代码在这我的 github博客中github blog sentry,需要的读者可以点击查看,如果觉得不错,可以顺便 star一下。

看源码前先来梳理下前端错误监控的知识。

前端错误监控知识

摘抄自 慕课网视频教程:前端跳槽面试必备技巧
别人做的笔记:前端跳槽面试必备技巧-4-4 错误监控类

前端错误的分类

1.即时运行错误:代码错误

try...catch

window.onerror (也可以用 DOM2事件监听)

2.资源加载错误

object.onerror: dom对象的 onerror事件

performance.getEntries()

Error事件捕获

3.使用 performance.getEntries()获取网页图片加载错误

varallImgs=document.getElementsByTagName('image')

varloadedImgs=performance.getEntries().filter(i=>i.initiatorType==='img')

最后 allImsloadedImgs对比即可找出图片资源未加载项目

Error事件捕获代码示例

window.addEventListener('error', function(e) {console.log('捕获', e)
}, true) // 这里只有捕获才能触发事件,冒泡是不能触发

上报错误的基本原理

1.采用 Ajax通信的方式上报

2.利用 Image对象上报 (主流方式)

Image上报错误方式: (newImage()).src='https://lxchuan12.cn/error?name=若川'

Sentry 前端异常监控基本原理

1.重写 window.onerror 方法、重写 window.onunhandledrejection 方法

如果不了解 onerror和onunhandledrejection方法的读者,可以看相关的 MDN文档。这里简要介绍一下:

MDN GlobalEventHandlers.onerror

window.onerror = function (message, source, lineno, colno, error) {console.log('message, source, lineno, colno, error', message, source, lineno, colno, error);
}

参数:
message:错误信息(字符串)。可用于 HTML onerror=""处理程序中的 event
source:发生错误的脚本 URL(字符串)
lineno:发生错误的行号(数字)
colno:发生错误的列号(数字)
errorError对象(对象)

MDN unhandledrejection

Promisereject 且没有 reject 处理器的时候,会触发 unhandledrejection 事件;这可能发生在 window 下,但也可能发生在 Worker 中。 这对于调试回退错误处理非常有用。

Sentry 源码可以搜索 global.onerror 定位到具体位置

 GlobalHandlers.prototype._installGlobalOnErrorHandler = function () {// 代码有删减// 这里的 this._global 在浏览器中就是 windowthis._oldOnErrorHandler = this._global.onerror;this._global.onerror = function (msg, url, line, column, error) {}// code ...}

同样,可以搜索 global.onunhandledrejection 定位到具体位置

GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function () {// 代码有删减this._oldOnUnhandledRejectionHandler = this._global.onunhandledrejection;this._global.onunhandledrejection = function (e) {}
}

2.采用 Ajax上传

支持 fetch 使用 fetch,否则使用 XHR

BrowserBackend.prototype._setupTransport = function () {// 代码有删减if (supportsFetch()) {return new FetchTransport(transportOptions);}return new XHRTransport(transportOptions);
};

2.1 fetch

FetchTransport.prototype.sendEvent = function (event) {var defaultOptions = {body: JSON.stringify(event),method: 'POST',referrerPolicy: (supportsReferrerPolicy() ? 'origin' : ''),};return this._buffer.add(global$2.fetch(this.url, defaultOptions).then(function (response) { return ({status: exports.Status.fromHttpCode(response.status),}); }));
};

2.2 XMLHttpRequest

XHRTransport.prototype.sendEvent = function (event) {var _this = this;return this._buffer.add(new SyncPromise(function (resolve, reject) {// 熟悉的 XMLHttpRequestvar request = new XMLHttpRequest();request.onreadystatechange = function () {if (request.readyState !== 4) {return;}if (request.status === 200) {resolve({status: exports.Status.fromHttpCode(request.status),});}reject(request);};request.open('POST', _this.url);request.send(JSON.stringify(event));}));
}

接下来主要通过Sentry初始化、如何 Ajax上报window.onerror、window.onunhandledrejection三条主线来学习源码。

如果看到这里,暂时不想关注后面的源码细节,直接看后文小结1和2的两张图。或者可以点赞或收藏这篇文章,后续想看了再看。

Sentry 源码入口和出口

var Sentry = (function(exports){// code ...var SDK_NAME = 'sentry.javascript.browser';var SDK_VERSION = '5.7.1';// code ...// 省略了导出的Sentry的若干个方法和属性// 只列出了如下几个exports.SDK_NAME = SDK_NAME;exports.SDK_VERSION = SDK_VERSION;// 重点关注 captureMessageexports.captureMessage = captureMessage;// 重点关注 initexports.init = init;return exports;
}({}));

Sentry.init 初始化 之 init 函数

初始化

// 这里的dsn,是sentry.io网站会生成的。
Sentry.init({ dsn: 'xxx' });
// options 是 {dsn: '...'}
function init(options) {// 如果options 是undefined,则赋值为 空对象if (options === void 0) { options = {}; }// 如果没传 defaultIntegrations 则赋值默认的if (options.defaultIntegrations === undefined) {options.defaultIntegrations = defaultIntegrations;}// 初始化语句if (options.release === undefined) {var window_1 = getGlobalObject();// 这是给  sentry-webpack-plugin 插件提供的,webpack插件注入的变量。这里没用这个插件,所以这里不深究。// This supports the variable that sentry-webpack-plugin injectsif (window_1.SENTRY_RELEASE && window_1.SENTRY_RELEASE.id) {options.release = window_1.SENTRY_RELEASE.id;}}// 初始化并且绑定initAndBind(BrowserClient, options);
}

getGlobalObject、inNodeEnv 函数

很多地方用到这个函数 getGlobalObject。其实做的事情也比较简单,就是获取全局对象。浏览器中是 window

/*** 判断是否是node环境* Checks whether we're in the Node.js or Browser environment** @returns Answer to given question*/
function isNodeEnv() {// tslint:disable:strict-type-predicatesreturn Object.prototype.toString.call(typeof process !== 'undefined' ? process : 0) === '[object process]';
}
var fallbackGlobalObject = {};
/*** Safely get global scope object** @returns Global scope object*/
function getGlobalObject() {return (isNodeEnv()// 是 node 环境 赋值给 global? global: typeof window !== 'undefined'? window// 不是 window self 不是undefined 说明是 Web Worker 环境: typeof self !== 'undefined'? self// 都不是,赋值给空对象。: fallbackGlobalObject);

继续看 initAndBind 函数

initAndBind 函数之 new BrowserClient(options)

function initAndBind(clientClass, options) {// 这里没有开启debug模式,logger.enable() 这句不会执行if (options.debug === true) {logger.enable();}getCurrentHub().bindClient(new clientClass(options));
}

可以看出 initAndBind(),第一个参数是 BrowserClient 构造函数,第二个参数是初始化后的 options。 接着先看 构造函数 BrowserClient。 另一条线 getCurrentHub().bindClient() 先不看。

BrowserClient 构造函数

var BrowserClient = /** @class */ (function (_super) {// `BrowserClient` 继承自`BaseClient`__extends(BrowserClient, _super);/*** Creates a new Browser SDK instance.** @param options Configuration options for this SDK.*/function BrowserClient(options) {if (options === void 0) { options = {}; }// 把`BrowserBackend`,`options`传参给`BaseClient`调用。return _super.call(this, BrowserBackend, options) || this;}return BrowserClient;
}(BaseClient));

从代码中可以看出BrowserClient 继承自 BaseClient,并且把 BrowserBackendoptions传参给 BaseClient调用。

先看 BrowserBackend,这里的 BaseClient,暂时不看。

BrowserBackend之前,先提一下继承、继承静态属性和方法。

__extends、extendStatics 打包代码实现的继承

未打包的源码是使用 ES6extends实现的。这是打包后的对 ES6extends的一种实现。

如果对继承还不是很熟悉的读者,可以参考我之前写的文章。面试官问:JS的继承

  1. // 继承静态方法和属性

  2. var extendStatics = function(d, b) {

  3. // 如果支持 Object.setPrototypeOf 这个函数,直接使用

  4. // 不支持,则使用原型__proto__ 属性,

  5. // 如何还不支持(但有可能__proto__也不支持,毕竟是浏览器特有的方法。)

  6. // 则使用for in 遍历原型链上的属性,从而达到继承的目的。

  7. extendStatics = Object.setPrototypeOf ||

  8. ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) ||

  9. function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; };

  10. return extendStatics(d, b);

  11. };

  12. function __extends(d, b) {

  13. extendStatics(d, b);

  14. // 申明构造函数__ 并且把 d 赋值给 constructor

  15. function __() { this.constructor = d; }

  16. // (__.prototype = b.prototype, new __()) 这种逗号形式的代码,最终返回是后者,也就是 new __()

  17. // 比如 (typeof null, 1) 返回的是1

  18. // 如果 b === null 用Object.create(b) 创建 ,也就是一个不含原型链等信息的空对象 {}

  19. // 否则使用 new __() 返回

  20. d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());

  21. }

不得不说这打包后的代码十分严谨,上面说的我的文章 面试官问:JS的继承 中没有提到不支持 __proto__的情况。看来这文章可以进一步严谨修正了。 让我想起 Vue源码中对数组检测代理判断是否支持 __proto__的判断。

// vuejs 源码:https://github.com/vuejs/vue/blob/dev/dist/vue.js#L526-L527
// can we use __proto__?
var hasProto = '__proto__' in {};

看完打包代码实现的继承,继续看 BrowserBackend 构造函数

BrowserBackend 构造函数 (浏览器后端)

var BrowserBackend = /** @class */ (function (_super) {__extends(BrowserBackend, _super);function BrowserBackend() {return _super !== null && _super.apply(this, arguments) || this;}/*** 设置请求*/BrowserBackend.prototype._setupTransport = function () {if (!this._options.dsn) {// We return the noop transport here in case there is no Dsn.// 没有设置dsn,调用BaseBackend.prototype._setupTransport 返回空函数return _super.prototype._setupTransport.call(this);}var transportOptions = __assign({}, this._options.transportOptions, { dsn: this._options.dsn });if (this._options.transport) {return new this._options.transport(transportOptions);}// 支持Fetch则返回 FetchTransport 实例,否则返回 XHRTransport实例,// 这两个构造函数具体代码在开头已有提到。if (supportsFetch()) {return new FetchTransport(transportOptions);}return new XHRTransport(transportOptions);};// code ...return BrowserBackend;
}(BaseBackend));

BrowserBackend 又继承自 BaseBackend

BaseBackend 构造函数 (基础后端)

/*** This is the base implemention of a Backend.* @hidden*/
var BaseBackend = /** @class */ (function () {/** Creates a new backend instance. */function BaseBackend(options) {this._options = options;if (!this._options.dsn) {logger.warn('No DSN provided, backend will not do anything.');}// 调用设置请求函数this._transport = this._setupTransport();}/*** Sets up the transport so it can be used later to send requests.* 设置发送请求空函数*/BaseBackend.prototype._setupTransport = function () {return new NoopTransport();};// code ...BaseBackend.prototype.sendEvent = function (event) {this._transport.sendEvent(event).then(null, function (reason) {logger.error("Error while sending event: " + reason);});};BaseBackend.prototype.getTransport = function () {return this._transport;};return BaseBackend;
}());

通过一系列的继承后,回过头来看 BaseClient 构造函数。

BaseClient 构造函数(基础客户端)

var BaseClient = /** @class */ (function () {/*** Initializes this client instance.** @param backendClass A constructor function to create the backend.* @param options Options for the client.*/function BaseClient(backendClass, options) {/** Array of used integrations. */this._integrations = {};/** Is the client still processing a call? */this._processing = false;this._backend = new backendClass(options);this._options = options;if (options.dsn) {this._dsn = new Dsn(options.dsn);}if (this._isEnabled()) {this._integrations = setupIntegrations(this._options);}}// code ...return BaseClient;
}());

小结1. new BrowerClient 经过一系列的继承和初始化

可以输出下具体 newclientClass(options)之后的结果:

function initAndBind(clientClass, options) {if (options.debug === true) {logger.enable();}var client = new clientClass(options);console.log('new clientClass(options)', client);getCurrentHub().bindClient(client);// 原来的代码// getCurrentHub().bindClient(new clientClass(options));
}

最终输出得到这样的数据。我画了一张图表示。重点关注的原型链用颜色标注了,其他部分收缩了。

initAndBind 函数之 getCurrentHub().bindClient()

继续看 initAndBind 的另一条线。

function initAndBind(clientClass, options) {if (options.debug === true) {logger.enable();}getCurrentHub().bindClient(new clientClass(options));
}

获取当前的控制中心 Hub,再把 newBrowserClient() 的实例对象绑定在 Hub上。

getCurrentHub 函数

// 获取当前Hub 控制中心
function getCurrentHub() {// Get main carrier (global for every environment)var registry = getMainCarrier();// 如果没有控制中心在载体上,或者它的版本是老版本,就设置新的。// If there's no hub, or its an old API, assign a new oneif (!hasHubOnCarrier(registry) || getHubFromCarrier(registry).isOlderThan(API_VERSION)) {setHubOnCarrier(registry, new Hub());}// node 才执行// Prefer domains over global if they are there (applicable only to Node environment)if (isNodeEnv()) {return getHubFromActiveDomain(registry);}// 返回当前控制中心来自载体上。// Return hub that lives on a global objectreturn getHubFromCarrier(registry);
}

衍生的函数 getMainCarrier、getHubFromCarrier

function getMainCarrier() {// 载体 这里是window// 通过一系列new BrowerClient() 一系列的初始化// 挂载在  carrier.__SENTRY__ 已经有了三个属性,globalEventProcessors, hub, loggervar carrier = getGlobalObject();carrier.__SENTRY__ = carrier.__SENTRY__ || {hub: undefined,};return carrier;
}
// 获取控制中心 hub 从载体上
function getHubFromCarrier(carrier) {// 已经有了则返回,没有则new Hubif (carrier && carrier.__SENTRY__ && carrier.__SENTRY__.hub) {return carrier.__SENTRY__.hub;}carrier.__SENTRY__ = carrier.__SENTRY__ || {};carrier.__SENTRY__.hub = new Hub();return carrier.__SENTRY__.hub;
}

bindClient 绑定客户端在当前控制中心上

Hub.prototype.bindClient = function (client) {// 获取最后一个var top = this.getStackTop();// 把 new BrowerClient() 实例 绑定到top上top.client = client;
};
Hub.prototype.getStackTop = function () {// 获取最后一个return this._stack[this._stack.length - 1];
};

小结2. 经过一系列的继承和初始化

再回过头来看 initAndBind函数

function initAndBind(clientClass, options) {if (options.debug === true) {logger.enable();}var client = new clientClass(options);console.log(client, options, 'client, options');var currentHub = getCurrentHub();currentHub.bindClient(client);console.log('currentHub', currentHub);// 源代码// getCurrentHub().bindClient(new clientClass(options));
}

最终会得到这样的 Hub实例对象。笔者画了一张图表示,便于查看理解。

初始化完成后,再来看具体例子。 具体 captureMessage 函数的实现。

Sentry.captureMessage('Hello, 若川!');

captureMessage 函数

通过之前的阅读代码,知道会最终会调用 Fetch接口,所以直接断点调试即可,得出如下调用栈。 接下来描述调用栈的主要流程。

调用栈主要流程:

captureMessage

function captureMessage(message, level) {var syntheticException;try {throw new Error(message);}catch (exception) {syntheticException = exception;}// 调用 callOnHub 方法return callOnHub('captureMessage', message, level, {originalException: message,syntheticException: syntheticException,});
}

=> callOnHub

/*** This calls a function on the current hub.* @param method function to call on hub.* @param args to pass to function.*/
function callOnHub(method) {// 这里method 传进来的是 'captureMessage'// 把method除外的其他参数放到args数组中var args = [];for (var _i = 1; _i < arguments.length; _i++) {args[_i - 1] = arguments[_i];}// 获取当前控制中心 hubvar hub = getCurrentHub();// 有这个方法 把args 数组展开,传递给 hub[method] 执行if (hub && hub[method]) {// tslint:disable-next-line:no-unsafe-anyreturn hub[method].apply(hub, __spread(args));}throw new Error("No hub defined or " + method + " was not found on the hub, please open a bug report.");
}

=> Hub.prototype.captureMessage

接着看 Hub.prototype 上定义的 captureMessage 方法

Hub.prototype.captureMessage = function (message, level, hint) {var eventId = (this._lastEventId = uuid4());var finalHint = hint;// 代码有删减this._invokeClient('captureMessage', message, level, __assign({}, finalHint, { event_id: eventId }));return eventId;
};

=> Hub.prototype._invokeClient

/*** Internal helper function to call a method on the top client if it exists.** @param method The method to call on the client.* @param args Arguments to pass to the client function.*/
Hub.prototype._invokeClient = function (method) {// 同样:这里method 传进来的是 'captureMessage'// 把method除外的其他参数放到args数组中var _a;var args = [];for (var _i = 1; _i < arguments.length; _i++) {args[_i - 1] = arguments[_i];}var top = this.getStackTop();// 获取控制中心的 hub,调用客户端也就是new BrowerClient () 实例中继承自 BaseClient 的 captureMessage 方法// 有这个方法 把args 数组展开,传递给 hub[method] 执行if (top && top.client && top.client[method]) {(_a = top.client)[method].apply(_a, __spread(args, [top.scope]));}
};

=> BaseClient.prototype.captureMessage

BaseClient.prototype.captureMessage = function (message, level, hint, scope) {var _this = this;var eventId = hint && hint.event_id;this._processing = true;var promisedEvent = isPrimitive(message)? this._getBackend().eventFromMessage("" + message, level, hint): this._getBackend().eventFromException(message, hint);// 代码有删减promisedEvent.then(function (event) { return _this._processEvent(event, hint, scope); })// 代码有删减return eventId;
};

最后会调用 _processEvent 也就是

=> BaseClient.prototype._processEvent

这个函数最终会调用

_this._getBackend().sendEvent(finalEvent);

也就是

=> BaseBackend.prototype.sendEvent

BaseBackend.prototype.sendEvent = function (event) {this._transport.sendEvent(event).then(null, function (reason) {logger.error("Error while sending event: " + reason);});
};

=> FetchTransport.prototype.sendEvent 最终发送了请求

FetchTransport.prototype.sendEvent

FetchTransport.prototype.sendEvent = function (event) {var defaultOptions = {body: JSON.stringify(event),method: 'POST',// Despite all stars in the sky saying that Edge supports old draft syntax, aka 'never', 'always', 'origin' and 'default// https://caniuse.com/#feat=referrer-policy// It doesn't. And it throw exception instead of ignoring this parameter...// REF: https://github.com/getsentry/raven-js/issues/1233referrerPolicy: (supportsReferrerPolicy() ? 'origin' : ''),};// global$2.fetch(this.url, defaultOptions) 使用fetch发送请求return this._buffer.add(global$2.fetch(this.url, defaultOptions).then(function (response) { return ({status: exports.Status.fromHttpCode(response.status),}); }));
};

看完 Ajax上报 主线,再看本文的另外一条主线 window.onerror 捕获。

window.onerror 和 window.onunhandledrejection 捕获 错误

例子:调用一个未申明的变量。

func();

Promise 不捕获错误

new Promise(() => {fun();
})
.then(res => {console.log('then');
})

captureEvent

调用栈主要流程:

window.onerror

GlobalHandlers.prototype._installGlobalOnErrorHandler = function () {if (this._onErrorHandlerInstalled) {return;}var self = this; // tslint:disable-line:no-this-assignment// 浏览器中这里的 this._global.  就是windowthis._oldOnErrorHandler = this._global.onerror;this._global.onerror = function (msg, url, line, column, error) {var currentHub = getCurrentHub();// 代码有删减currentHub.captureEvent(event, {originalException: error,});if (self._oldOnErrorHandler) {return self._oldOnErrorHandler.apply(this, arguments);}return false;};this._onErrorHandlerInstalled = true;
};

window.onunhandledrejection

GlobalHandlers.prototype._installGlobalOnUnhandledRejectionHandler = function () {if (this._onUnhandledRejectionHandlerInstalled) {return;}var self = this; // tslint:disable-line:no-this-assignmentthis._oldOnUnhandledRejectionHandler = this._global.onunhandledrejection;this._global.onunhandledrejection = function (e) {// 代码有删减var currentHub = getCurrentHub();currentHub.captureEvent(event, {originalException: error,});if (self._oldOnUnhandledRejectionHandler) {return self._oldOnUnhandledRejectionHandler.apply(this, arguments);}return false;};this._onUnhandledRejectionHandlerInstalled = true;
};

共同点:都会调用 currentHub.captureEvent

currentHub.captureEvent(event, {originalException: error,
});

=> Hub.prototype.captureEvent

最终又是调用 _invokeClient ,调用流程跟 captureMessage 类似,这里就不再赘述。

this._invokeClient('captureEvent')

=> Hub.prototype._invokeClient

=> BaseClient.prototype.captureEvent

=> BaseClient.prototype._processEvent

=> BaseBackend.prototype.sendEvent

=> FetchTransport.prototype.sendEvent

最终同样是调用了这个函数发送了请求。

可谓是殊途同归,行文至此就基本已经结束,最后总结一下。

总结

Sentry-JavaScript源码高效利用了 JS的原型链机制。可谓是惊艳,值得学习。

本文通过梳理前端错误监控知识、介绍 sentry错误监控原理、 sentry初始化、 Ajax上报、 window.onerror、window.onunhandledrejection几个方面来学习 sentry的源码。还有很多细节和构造函数没有分析。

总共的构造函数(类)有25个,提到的主要有9个,分别是: Hub、BaseClient、BaseBackend、BaseTransport、FetchTransport、XHRTransport、BrowserBackend、BrowserClient、GlobalHandlers

其他没有提到的分别是 SentryError、Logger、Memo、SyncPromise、PromiseBuffer、Span、Scope、Dsn、API、NoopTransport、FunctionToString、InboundFilters、TryCatch、Breadcrumbs、LinkedErrors、UserAgent

这些构造函数(类)中还有很多值得学习,比如同步的 Promise(SyncPromise)。 有兴趣的读者,可以看这一块官方仓库中采用 typescript写的源码SyncPromise,也可以看打包后出来未压缩的代码。

读源码比较耗费时间,写文章记录下来更加费时间(比如写这篇文章跨度十几天...),但收获一般都比较大。

如果读者发现有不妥或可改善之处,再或者哪里没写明白的地方,欢迎评论指出。另外觉得写得不错,对您有些许帮助,可以点赞、评论、转发分享,也是对笔者的一种支持。万分感谢。

推荐阅读

知乎滴滴云:超详细!搭建一个前端错误监控系统
掘金BlackHole1:JavaScript集成Sentry
丁香园 开源的 Sentry 小程序 SDKsentry-miniapp
sentry官网
sentry-javascript仓库

关于

作者:常以若川为名混迹于江湖。前端路上 | PPT爱好者 | 所知甚少,唯善学。
个人博客 http://lxchuan12.cn 使用 vuepress重构了,阅读体验可能更好些
https://github.com/lxchuan12/blog,相关源码和资源都放在这里,求个 star^_^~

微信交流群,加我微信lxchuan12,注明来源,拉您进前端视野交流群

下图是公众号二维码:若川视野,一个可能比较有趣的前端开发类公众号,目前前端内容不多

往期文章

工作一年后,我有些感悟(写于2017年)

高考七年后、工作三年后的感悟

面试官问:JS的继承

学习 jQuery 源码整体架构,打造属于自己的 js 类库

学习underscore源码整体架构,打造属于自己的函数式编程类库

学习 lodash 源码整体架构,打造属于自己的函数式编程类库

由于公众号限制外链,点击阅读原文,或许阅读体验更佳,觉得文章不错,可以点个在看呀^_^

学习 sentry 源码整体架构,打造属于自己的前端异常监控SDK相关推荐

  1. 学习 vuex 源码整体架构,打造属于自己的状态管理库

    前言 这是学习源码整体架构第五篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现.本篇文章学习的是实际仓库的代码. 其余四篇分别是: ...

  2. 学习 axios 源码整体架构,打造属于自己的请求库

    前言 这是学习源码整体架构系列第六篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现.本篇文章学习的是实际仓库的代码. 学习源码整体 ...

  3. 学习 redux 源码整体架构,深入理解 redux 及其中间件原理

    如果觉得内容不错,可以设为星标置顶我的公众号 1. 前言 你好,我是若川.这是学习源码整体架构系列第八篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是 ...

  4. 学习 lodash 源码整体架构,打造属于自己的函数式编程类库

    前言 这是 学习源码整体架构系列第三篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现.文章学习的是打包整合后的代码,不是实际仓库中 ...

  5. 学习underscore源码整体架构,打造属于自己的函数式编程类库

    前言 上一篇文章写了 jQuery整体架构,学习 jQuery 源码整体架构,打造属于自己的 js 类库 虽然看过挺多 underscore.js分析类的文章,但总感觉少点什么.这也许就是纸上得来终觉 ...

  6. 学习 jQuery 源码整体架构,打造属于自己的 js 类库

    虽然现在基本不怎么使用 jQuery了,但 jQuery流行 10多年的 JS库,还是有必要学习它的源码的.也可以学着打造属于自己的 js类库,求职面试时可以增色不少. 本文章学习的是 v3.4.1版 ...

  7. 学习 launch-editor 源码整体架构,探究 vue-devtools「在编辑器中打开组件」功能实现原理...

    1. 前言 你好,我是若川[1],微信搜索「若川视野」关注我,专注前端技术分享,一个愿景是帮助5年内前端开阔视野走向前列的公众号.欢迎加我微信ruochuan12,长期交流学习. 这是学习源码整体架构 ...

  8. vuex 源码整体架构学习

    前言 这是学习源码整体架构第五篇.整体架构这词语好像有点大,姑且就算是源码整体结构吧,主要就是学习是代码整体结构,不深究其他不是主线的具体函数的实现.本篇文章学习的是实际仓库的代码. 其余四篇分别是: ...

  9. Python源码解读之零 Python源码整体架构

    今天打算开一个新的系列,本系列将和大家一起对Python的源码进行分析,有参考陈儒的<Python 源码剖析>(强烈推荐这本书),做到知其然知其所以然. 总体架构 首先我们先从总体结构看一 ...

最新文章

  1. 队列的应用、栈的应用
  2. UE 手游在 iOS 平台运行时内存占用太高?试试这样着手优化
  3. vue中使用js-cookie
  4. 数据说话,88000条数据绘制北京市地图
  5. excel转txt工具
  6. 实测java 与php运行速度比较
  7. LINQPad工具-linq、sql、IL优化和转换
  8. linux gpart 用法,gpart 使用笔记
  9. 使用SPA/GPA 参数--SAP内存参数设置SET /GET PARAMTER ID
  10. CANoe+Carmaker联合仿真测试
  11. JS阻止浏览器刷新的方法
  12. 怎么样计算机械功率,电功率与机械功率的换算公式
  13. linux读usb蓝牙数据,嵌入式Linux下USB蓝牙设备驱动.pdf
  14. 3D 机器视觉应用并采用 DLP 技术的精确点云生成
  15. 亚马逊云科技-游戏孵化营第一课学习心得
  16. 怎么撰写一份优秀的数据分析报告(三)
  17. MAPE 平均绝对百分误差
  18. 多项式轨迹--五次多项式轨迹
  19. NodeJS之child_process模块
  20. 2020外地人如何把户口迁入广州

热门文章

  1. C++和Rust_后端程序员一定要看的语言大比拼:Java vs. Go vs. Rust
  2. python random randint_python中random.randint和random.randrange的区别详解
  3. 常见的不同类型运算的转换方式
  4. 【代码笔记】iOS-下拉选项cell
  5. 转:数据库关系模式的范式详解
  6. 新架设了一个CVS服务器 --by yp
  7. 全国计算机等级考试题库二级C操作题100套(第07套)
  8. 中医 西班牙语 PHP,php – MySql西班牙语字符数据
  9. android sdk 2.9.5.0,如何找到问题Android SDK 3.0错误:(9,5)错误:资源android:attr/colorError未找到...
  10. html5 type submit,input type=submit