062019.3

创建可缓存计算过程的函数

受reselect启发,实际上,对于某类特定函数——输入相同的情况下,输出一定相同的纯函数,实际上,对于相同的输入参数,没有必要进行多次计算,而是可以将结果缓存起来,在识别到相同的参数时,直接取出该参数对应的结果即可。因此,我编写了computex:

/**
 * 创建一个可缓存的纯函数。
 * 使用场景:纯函数,相同的参数永远得到相同的结果。
 * @param {*} fn 原始纯函数
 * @param {*} expire 缓存过期时间
 * @example
 * const fn = computex((state) => {
 *   return state.name + ':' + state.age
 * })
 * var a1 = fn({})
 * var a2 = fn({})
 * // a1 === a2 // 因为参数相同,所以结果使用了缓存,因此也相同(引用相同)
 */
export function computex(fn, expire = 60000) {
  const cache = {}

  // 创建一个循环任务,检查缓存是否过期,如果过期,则删除缓存,释放内存
  const recycle = () => {
    const keys = Object.keys(cache)
    keys.forEach((key) => {
      const { time } = cache[key]
      if (time + expire <= Date.now()) {
        delete cache[key]
      }
    })
    setTimeout(recycle, 1000)
  }

  // 如果expire设置为0,则缓存永久生效
  if (expire) {
    recycle()
  }

  return function(...args) {
    const hash = getObjectHash(args)
    if (hash in cache) {
      const item = cache[hash]
      return item.result
    }

    const result = fn.apply(this, args)
    const time = Date.now()
    cache[hash] = { result, time }

    return result
  }
}

其中computex本身是一个函数,它接收一个函数,返回一个函数,返回的函数是接收到函数的复刻版,运行它得到的结果和运行传入函数是一样的。这样就可以做到缓存计算过程。

/**
 * 创建一个在同一个同步进程中只获取一次的函数。
 * 使用场景:反复调用一个函数,而该函数在本批次运行时,实际执行结果应该相同。
 * @param {*} fn
 * @param {number} expire 缓存过期时间,根据实际的情况来看,如果一个运算比较大,可以设大一点
 * @example
 * const get = getx(() => Date.now())
 * var a1 = get()
 * var a2 = get()
 * // a1 === a2 // 在同步进程中,直接使用缓存
 */
export function getx(fn, expire = 10) {
  var iscalling = false
  var cache = null
  var timer = null

  return function() {
    clearTimeout(timer)
    timer = setTimeout(() => {
      iscalling = false
      cache = null
    }, expire)

    if (iscalling) {
      return cache
    }

    iscalling = true

    const result = fn.call(this)
    cache = result
    return result
  }
}

getx是另外一个逻辑。当在同一个同步进程中,我们可能会反复调用一个函数,而这些函数在这一次反复调用时,实际上是执行了同一个过程,所以,得到的结果也是一样的,但是在执行过程中消耗了资源。因此,我们可以将这些结果缓存起来,在这一个同步进程结束后再清理掉即可。

/**
 * 创建一个缓存Promise的异步函数
 * @param {*} fn
 * @param {*} expire 缓存的过期时间,不设置的时候,当Promise结束缓存就会被清空,而如果设置了expire,完全按照expire清空缓存,而不会依赖Promise的结束
 */
export function asyncx(fn, expire = 0) {
	const cache = {}

	// 创建一个循环任务,检查缓存是否过期,如果过期,则删除缓存,释放内存
	const recycle = () => {
		const keys = Object.keys(cache)
		keys.forEach((key) => {
			const { time } = cache[key]
			if (time + expire <= Date.now()) {
				delete cache[key]
			}
		})
		setTimeout(recycle, 1000)
	}

	// 如果expire设置为0,则缓存永久生效
	if (expire > 0) {
		recycle()
	}

	return function(...args) {
		const hash = getObjectHash(args)
		if (hash in cache) {
			const item = cache[hash]
			return item.deferer
		}

		const deferer = new Promise((resolve, reject) => {
			Promise.resolve().then(() => fn.apply(this, args)).then(resolve).catch(reject).then(() => {
				if (expire > 0) {
					return
				}
				delete cache[hash]
			})
		})
		const time = Date.now()
		cache[hash] = { deferer, time }

		return deferer
	}
}

而对于基于Promise的异步操作,则通过asyncx来实现缓存。缓存的并非Promise最终返回的结果,而是缓存Promise本身,在一个Promise进行中的时候,如果再次调用这个函数,得到的会是未结束的Promise,而非重新发起一个Promise。这有助于解决短时间内反复请求某个api接口的情况。

20:27:44 已有2条回复
  1. hello,有个问题要请教一下你,之前你说过网易云的播放器音质挺烂,你说过用了什么耳机再来用苹果的,iphone的音质很差。 我想要你帮我推荐一个手机耳机,或者说是便捷式耳机。不不太想要太大,如果说在体验上很好也是可以接受的。


    一直关注你。
    #735 苦瓜 2019-03-08 21:48 回复
  2. 谢谢关注,其实我对耳机要求不高,所以没有太多研究。你可以试试airpod,时尚感比较强,如果是追求音质,还是推荐索尼的耳机,如果希望续航长一点,可以考虑项圈的,我买的JBL虽然音质我还觉得不错,但是续航弱。
    #742 回复给#735 否子戈 2019-03-11 10:15 回复

获取一个对象的hash值

我们比较两个对象,比较好的一种办法是直接比较两个对象的hash值,对于相同结构和内容的对象,hash值应该相等。怎么获取对象的hash值?

/**
 * 获取一个字符串的hash
 * @param {*} str
 */
function getStringHash(str) {
	let hash = 5381
	let i = str.length

	while(i) {
		hash = (hash * 33) ^ str.charCodeAt(--i)
	}

	return hash >>> 0
}

/**
 * 格式化对象,类似JSON.stringify,但是支持自引用嵌套
 * @param {*} obj
 */
export function stringifyObject(obj) {
	const exists = [obj] // 存储已经处理过的,避免死循环
	const used = [] // 记录被用到的引用标记
	const stringifyObjectByKeys = (obj) => {
		if (isArray(obj)) {
			let items = obj.map((item) => {
				if (item && typeof item === 'object') {
					return stringifyObjectByKeys(item)
				}
				else {
					return JSON.stringify(item)
				}
			})
			let str = '[' + items.join(',') + ']'
			return str
		}

		let str = '{'
		let keys = Object.keys(obj)
		let total = keys.length
		keys.sort()
		keys.forEach((key, i) => {
			let value = obj[key]
			str += key + ':'

			if (value && typeof value === 'object') {
				let index = exists.indexOf(value)
				if (index > -1) {
					str += '#' + index
					used.push(index)
				}
				else {
					exists.push(value)
					let num = exists.length - 1
					str += '#' + num + stringifyObjectByKeys(value)
				}
			}
			else {
				str += JSON.stringify(value)
			}

			if (i < total - 1) {
				str += ','
			}
		})
		str += '}'
		return str
	}
	let str = stringifyObjectByKeys(obj)

	exists.forEach((item, i) => {
		if (!used.includes(i)) {
			str = str.replace(new RegExp(`:#${i}`, 'g'), ':')
		}
	})

	if (used.includes(0)) {
		str = '#0' + str
	}

	return str
}

/**
 * 获取一个对象的hash值
 * @param {*} obj
 */
export function getObjectHash(obj) {
	if (typeof obj !== 'object') {
		return
	}

	let str = stringifyObject(obj)
	let hash = getStringHash(str)
	return hash
}

这里面考虑到了一个非常严重的问题,就是当一个对象如果存在自引用的情况,普通的遍历会引起死循环而导致程序挂掉,但这个函数不会,它会用一个临时变量去记录已经处理过的对象,如果在下一次处理到一个对象时,首先会去临时变量里面查找,如果找到的话,会把这个变量的索引值作为参考系数,直接用于hash计算。

14:11:38 已有0条回复
052019.3

给博客加上内置二维码功能

之前用了qr.liantu.com的接口生存二维码,并且通过curl抓取到接口生成的二维码图片之后,再保存到本地的一个cache目录下。之前用着挺爽的,但是最近这个网站开始抽风,生成二维码非常慢,最后完全无法忍受了,于是决定还是自己生成二维码得了。

我的目标是给每一篇文章生成一个二维码,放在cache目录下,在我的首页和路迹栏目都需要用到。于是下载了知名的二维码库phpqrcode,然后集成到我自己的工具函数中。自己写的代码如下:

<?php

include_once(__DIR__.'/phpqrcode/qrlib.php');

function get_qr_code_url($text) {
  $path = "/cache/qrcode-".md5($text).".png";
  $file = WP_CONTENT_DIR.$path;

  if (!file_exists($file)) {
    QRcode::png($text, $file, QR_ECLEVEL_L, 4);
  }

  $url = home_url("/wp-content".$path);
  return $url;
}

然后在主题文件中调用get_qr_code_url函数即可。$text则通过get_the_permalink()得到,这样就快速把二维码集成到自己到博客中了。

22:51:18 已有0条回复
162019.2

昨晚改到2点多,对HelloType抛出的错误信息再次进行了整理。之所以要花那么多时间去整理,是因为我发现在平时使用的时候,特别依赖这个抛出的错误来判断真实的错误情况。之前输出的错误在展示具体类型的时候,丢掉了细节,在发现错误,回去看具体的原始数据和类型容器的时候无法完整的看到,而经过改造后,可以看到更具体的内容,又便于调试。

接下来还想做一件事,就是对traces进行升级,可以通过stack找到该trace产生的位置,并将这些trace的stack加入到eorror的独立属于中去,这样可以定位每一个判决在代码中的位置。

12:12:11 已有0条回复
152019.2

慎用import/export嵌套

今天遇到一个极其蛋疼的情况,两个js模块,相互循环引用,造成一个bug。

// a.js
import { SomeB } from './b.js'

export const SomeA = 'aaa:' + SomeB
// b.js
import { SomeA } from './a.js'

export const SomeB = 'bbb:' + SomeA

接下来我在一个文件中使用模块b:

// c.js
import { SomeB } from './b.js'

console.log(SomeB)

你知道会得到什么结果吗?总之是得到错误的结果。在chrome中直接运行会直接报错,说a.js中的SomeB未定义。这……

其实这是真的。因为a.js和b.js相互引用,导致的结果就是,无论是谁,在执行的时候,都需要另外一个模块加载完成才能进行自己的解释。这就很蛋疼:

c.js -> b.js -> a.js -> b.js -> a.js ...

而js解释器一般会灵活处理,即不会死循环的去如此解释,而是在发现循环的时候,就直接结束,即:

c.js -> b.js -> a.js $

但是,当a.js需要b.js所带来的上下文时,实际上b.js还没有被解释,因为b.js依赖a.js的解释结果,因此,在a.js中,SomeB被认为是not defined,这已经不是TypeError,而是ReferenceError,也就是在上下文中,该变量未被声明。

因此,千万不要循环引用啊,切记切记。

17:01:03 已有0条回复
022019.2

现代浏览器已经不需要babel和webpack了

在Robust中,我提到说babel和webpack具有时间节点上的意义,但是现在看来,这个时间节点已经过去,它们可以功成身退了。怎么讲?我们使用babel的主要目的,是将js高版本的语法编译为低版本语法,以在前几年时间里,浏览器还没有跟上节奏,很多新版本特性没支持的情况下,解决临时问题。但是,现在不同了,大部分新特性浏览器都会提前埋好,标准出来时,浏览器已经支持了。而且,很多特性我们之前不敢用,我们担心某些浏览器不支持。但是随着时间的流逝,可以确定的是,两年前的特性浏览器一定已经支持了,只是新发布的标准还是比较难确定的。而比较激进的chrome,则大部分我们需要的特性都是支持的。我觉得,ES2018发布之后,我们就可以不需要babel了,因为99%以上的语法浏览器都支持了,其中最重要也是最慢的,就是object spread/resest这个特性:

var obja = { a: 1 }
var objb = { ...obja, b: 2 }

这个特性已经成为标准的一部分,那么基本上我们需要的特性都有了。我们对ES标准,已经没有太大的要求,只希望它慢慢迭代,更正和修复一些小问题了。

除了特性支持以外,模块引入也已经被支持了。我们不需要通过webpack来打包,支持我们写的import/export语法,浏览器原生支持这些语法。

<sricpt type="module">
import SomeModule from 'http://xxx/somemodule.mjs'
</script>

它的工作方式非常符合我们的理解,这使得我们完全可以放弃AMD那种复杂的模块引入方式,这就让曾经传言浏览器内置require.js这样的传言不攻自破。

另外,还有一个特性,即tagged string tempaltes,这个特性可以直接替代jsx,ES本身就支持类似jsx那样的语法,只是需要一个tag解析函数而已,而这些都是在运行时可以完成的。举个例子:

import { jsx } from 'https://xxx/jsx'

const vdom = props => jsx`<div class="${props.className}">${props.children}</div>`

ES本身就支持通过一个函数去解析字符串,然后返回任意形式的数据。这样一来,字符串模板本身就可以实现和jsx相同的功能。当然,还是有一点不同,jsx是需要编译的,编译后直接生成代操作的程序代码,在运行的时候,不需要经过解析解析模板这个步骤,省了一些性能。而如果运行时去解析字符串模板,可以缓存,因为字符串本身的结构不会变化,但是比较复杂的结构,还是需要每次都去进行解析模板,所以,性能上会有消耗。但无论如何,ES已经给了我们这样的能力,我们完全可以抛开babel,99%的代码都可以运行。

11:21:59 已有0条回复
212019.1

陆续收到听众的打赏,虽然不多,但是点燃了我的热情,才仅仅做了一期的Robust而已,没想到认可度很高,想我这种喜欢打持久战的人,一期根本满足不了。因为周末要去山西参加朋友婚礼,顺带完结多年想去北方玩的冲动,所以,下一期的Robust会早发(不会停更),我会趁晚上的事件陆续录好,周末之前上传到电播平台,Robust只会上传到企鹅FM和网易云音乐,其他平台要是转载,一定是盗版……竟然开始幻想盗版的事……公众号也会发,关注我的私人公众号 wwwtangshuangnet 最快收到上新消息。

12:56:26 已有0条回复
182019.1

Mimic Relative Positioning Inside an SVG with Nested SVGs

SVG动图

view-source:https://camo.githubusercontent.com/29765c4a32f03bd01d44edef1cd674225e3c906b/68747470733a2f2f63646e2e7261776769742e636f6d2f66616365626f6f6b2f6372656174652d72656163742d6170702f323762343261632f73637265656e636173742e737667

102019.1

js解析html属性列表

因为项目的一个小组件里面需要对类似html属性字符串进行解析,找了很久没有找到对应的库来做,干脆自己写一个。

function parseAttrs(source) {
	let separator = /\s/;
	let cursor = '';

	let str = source.trim().replace(/\n+/g, ' ').replace(/\s+/g, ' ');
	let length = str.length;

	let attrs = [];

	let current = null;
	let type = 'key';
	let reset = () => {
		current = {
			key: '',
			value: ''
		};
		cursor = '';
	};
	reset();

	for (let i = 0; i < length; i ++) {
		let char = str.charAt(i);
		let needPush = true;

		// 遇到引号
		if (char === '"' || char === "'") {
			// 引号开始
			if (!cursor) {
				cursor = char;
				type = 'value';
				needPush = false;
			}
			// 引号结束
			else if (cursor === char) {
				cursor = '';
				type = 'key';
				needPush = false;
			}
		}

		if (char === '=' && type === 'key') {
			needPush = false;
		}

		if (/[\w\W]/.test(char) && needPush) {
			current[type] += type === 'key' && char === ' ' ? '' : char;
		}

		// 遇到分隔符
		if ((separator.test(char) && cursor === '') || i === length - 1) {
			attrs.push(current);
			reset();
		}
	}

	return attrs;
}

思路非常简单,就是遍历整个字符串,遇到特殊标记就记录下来,再遇到特殊标记就结束一个属性的查找,最后得到一个数组。

19:03:52 已有0条回复