shift
看下面的代码,这是一个依赖库中源码里发现的。
immediatePush(data: BreadcrumbPushData): BreadcrumbPushData[] {
data.time || (data.time = getTimestamp())
if (this.stack.length >= this.maxBreadcrumbs) {
this.shift()
}
this.stack.push(data)
this.stack.sort((a, b) => a.time - b.time)
logger.log(this.stack)
return this.stack
}
private shift(): boolean { // 删除第一个元素,为什么要返回一个布尔值
return this.stack.shift() !== undefined
}
删除第一个元素,为什么要返回一个布尔值?这个布尔值并未使用。
如果数组为空,shift() 方法将返回 undefined。在某些情况下,你可能需要知道 shift() 是否成功移除了元素,这就需要检查返回值。
这种设计模式在处理队列(FIFO)或栈(LIFO)数据结构时很常见,因为它允许你了解操作是否成功,以及在操作后数组的状态。
console
在 JavaScript 中,如果你在全局作用域中重写了 console 对象,并且这个重写涉及到了 console.log 或其他 console 方法,那么在某些情况下,这可能会导致无限递归。这种情况通常发生在你尝试记录(log)信息到控制台(console),但是 console 对象已经被替换为一个会触发自身调用的函数。
例如,如果你有一个 logger 对象,它监听 console.log 并执行一些额外的逻辑,然后调用原始的 console.log 方法,这可能会导致无限递归:
// 监听控制台的报错
const consolePlugin: BasePluginType<BrowserEventTypes, BrowserClient> = {
name: BrowserEventTypes.CONSOLE,
monitor(notify) {
if (!('console' in _global)) {
return
}
const logType = ['log', 'debug', 'info', 'warn', 'error', 'assert']
logType.forEach(function (level: string): void {
if (!(level in _global.console)) return
replaceOld(_global.console, level, function (originalConsole: () => any): Function {
return function (...args: any[]): void {
if (originalConsole) {
notify(BrowserEventTypes.CONSOLE, { args, level })
originalConsole.apply(_global.console, args)
}
}
})
})
},
transform(collectedData: ConsoleCollectType) {
return collectedData
},
consumer(transformedData: ConsoleCollectType) {
if (globalVar.isLogAddBreadcrumb) {
addBreadcrumbInBrowser.call(this, transformedData, BrowserBreadcrumbTypes.CONSOLE, Severity.fromString(transformedData.level))
}
}
}
/**
* 如果用户输入console,并且logger是打开的会造成无限递归,执行callback前,会把监听console的行为关掉
*
* @export
* @param {Function} callback
*/
export function silentConsoleScope(callback: Function) {
globalVar.isLogAddBreadcrumb = false
callback()
globalVar.isLogAddBreadcrumb = true
}
为了避免这种情况,你可以在执行回调之前临时替换 console.log 方法,执行回调后再恢复原来的 console.log 方法。这样可以确保在执行回调时不会触发递归调用。
const originalConsoleLog = console.log;
console.log = function(...args) {
// 执行一些额外的逻辑
originalConsoleLog.apply(console, args); // 这里可能会触发递归
};
有没有人好奇,为啥需要重写console对象,如果是监控控制台报错呢?
如何确保传参的类型完全一致?
首先,我们思考一下,为什么设计库和依赖的时候,要保证类型一致呢?
那肯定是不一致,会报错呀?
这样想一定没有问题,如果应该是数字类型,我传入一个对象,那怎么计算,怎么进行加减乘除,对吧?(类型安全)
那再深入一点,为了更方便维护,一个库和依赖,比如在GitHub上开源,那可以有很多人都能更改的,如果你没有指定类型,那其他人怎么知道,这里该是什么类型?你说可以看代码呀,是的,但是这需要花费时间,所以可以提效的事情,为什么不做好点呢?(代码可读性,可维护)
然后库和依赖,都是给其他人一起使用的,如果你知道了要传递什么类型,其他人是不是就不会乱传其他类型了,如果不想正常使用库功能的除外。(文档和示例的清晰度)
只要是人写的代码,都可能有bug和功能不足的时候,对吧,你能保证你的功能就完美无缺吗?不可能的,对吧,所以这个时候,就会需要开发新功能,那这个时候,新功能是不是就会要用到一些已存在的变量或常量呢?那这个时候,你是不是要保证它的类型?(重用和扩展)
然后一些组件库和功能库,你是否看到过test文件夹,或者是__tests__文件,这是干嘛的呀?知道jest的就应该能猜到了吧,自动化测试,就是模拟你这个库的使用,看看是否正常,所以类型不一致,这里就会很难受了,还有查问题的时候,调试功能,类型变来变去,是不是会很难受。(测试和调试)
好了,有没有人会问,类型怎么变来变去,就问在在空字符串的前面加一个加号(+)输出,会是什么类型呢?(隐式类型转换)
最后,类型一致,对编译器和运行环境是有优化的,因为能避免不必要的类型检查和转换操作(性能优化)
好了,既然它如此重要,我们下面就来看看源码中,如何保证类型一致吧
/**
* 绑定配置项
*
* @param {Partial<O>} [options={}]
* @memberof BaseTransport
*/
bindOptions(options: Partial<O> = {}): void {
const { dsn, beforeDataReport, service, maxDuplicateCount, backTrackerId, configReportUrl } = options
const functionType = ToStringTypes.Function
const optionArr = [
[service, 'service', ToStringTypes.String],
[dsn, 'dsn', ToStringTypes.String],
[maxDuplicateCount, 'maxDuplicateCount', ToStringTypes.Number],
[beforeDataReport, 'beforeDataReport', functionType],
[backTrackerId, 'backTrackerId', functionType],
[configReportUrl, 'configReportUrl', functionType]
]
validateOptionsAndSet.call(this, optionArr) // 类型一致才绑定到对应的类中,类型不正确就使用默认值
}
export function validateOptionsAndSet(this: any, targetArr: [any, string, ToStringTypes][]) {
targetArr.forEach(
([target, targetName, expectType]) => toStringValidateOption(target, targetName, expectType) && (this[targetName] = target)
)
}
export function toStringValidateOption(target: any, targetName: string, expectType: ToStringTypes): boolean {
if (toStringAny(target, expectType)) return true
typeof target !== 'undefined' && logger.error(`${targetName}期望传入:${expectType}类型,当前是:${nativeToString.call(target)}类型`)
return false
}
原来indexOf要这样用
这个函数看着很简单,但是用到了取反运算符!,也用到了~,有没有区别不了这两者的,请看下文:
/**
* 原子字符中是否包含目标字符
*
* @export
* @param {string} origin 原字符
* @param {string} target 目标字符
* @return {*} {boolean}
*/
export function isInclude(origin: string, target: string): boolean {
return !!~origin.indexOf(target)
}
- origin.indexOf(target):这个方法返回 target 在 origin 中第一次出现的位置的索引。如果 target 不在 origin 中,它返回 -1。
- ~ 操作符(按位非):这是一个一元操作符,它对操作数的每一位进行按位非操作。在 JavaScript 中,任何非零数字的按位非结果都是 0,而零(0)的按位非结果是 -1。因此,如果 origin.indexOf(target) 返回 -1(表示 target 不在 origin 中),按位非操作会得到 0。如果返回的是任何其他值(表示 target 在 origin 中),按位非操作会得到 1。
- ! 操作符(逻辑非):这是一个逻辑操作符,它将操作数转换为布尔值,然后取反。在 JavaScript 中,0 被视为 false,而任何非零值被视为 true。因此,无论 ~ 操作的结果是什么,! 操作都会将其取反。如果 ~ 的结果是 0(false),! 操作后得到 true;如果 ~ 的结果是 1(true),! 操作后得到 false。
所以,整个表达式 !!~origin.indexOf(target) 的目的是:
- 如果 target 在 origin 中,indexOf 返回一个非 -1 的值,按位非操作得到 1,然后逻辑非操作得到 false。
- 如果 target 不在 origin 中,indexOf 返回 -1,按位非操作得到 0,然后逻辑非操作得到 true。
这个表达式实际上是一个巧妙的技巧,用来简化代码并直接得到一个布尔值,表示 target 是否包含在 origin 中.
~的作用
-1 ---> 0
0 ---> -1
1 ---> -2
2 ---> -3
!取反变成布尔值
0 ---> false
非0 ---> true
获取时间戳
这个大家应该都能想到new Date().getTime(),那你有考虑过还有其他方法吗?来看看源码中用了什么方法?为啥用了这种方式呢?
/**
* 获取当前的时间戳
*
* @export
* @return {*} {number}
*/
export function getTimestamp(): number {
return Date.now()
}
Date为一个时间相关对象
new Date().getTime() | Date.now() |
---|---|
new Date()创建一个新时间对象,接着调用该对象的getTime()方法,获取时间戳 | now()是Date的一个静态方法,获取时间戳 |
慢 | 更快 |
既然如此,那么以后我都用Date.now()好了。然后,我又喵到一个同样是统计时间的方式——performance.now();
除了上面两种以外,还有一种获取时间戳的方式,performance是一个很有趣的接口,可以获取当前页面与性能相关的信息,包含三个只读对象:navigation/timing/memory,若干方法:比如now();
从统计浏览器加载性能来说,performance.now()比Date.now()好很多,前者精确度更高(千分之五秒),更可靠(不会受浏览器时间影响),然而目前支持率不太够(android 4.3及以下,ios 8.4及以下)。
所以为了防止在移动端出现兼容问题,所以就不使用performance.now();
由于Date.now()性能优于new Date().getTime(),所以就使用Date.now()。
好了,今天就分享到这里啦,大家还看到源码里有哪些优化方案呢?欢迎在评论区讨论哈。
原文链接:https://juejin.cn/post/7351300892614656063 作者:0522Skylar