限制及如何解决
字符长度限制
TL;DR: 解决方案。
TimeredCounter
实现原理是将字符串视为某个进制的数字,然后将其转换为十进制数字。这样做的目的是为了方便采样数字制作滚动列表。
这种方式在大多数情况下都是可行的,但是当数字太大或太小时,就会出现问题。
在 ECMA 规范中,当 number < 10^-5 || number > 10^21
时,数字将使用科学记数法表示[1][2]。
这样问题就来了,数字将使用科学记数法表示后会丢失部分精度。
1234567891012131415161
=> 1.2345678910121315e+21 // 丢失了后 6 位精度
由于实现方式的原因,这种情况在 timered-counter-string
上较为容易出现。 因为进制数基本上是字符串中不重复字符的个数,这将很容易得到一个超过 10^21
的数字。
类似的,当我们使用 timered-counter-number
时,如果数值小于 10^-5
,数字也将使用科学记数法表示。
如何解除字符长度限制
要解决这个问题,我们需要使用第三方高精度计算库,如 decimal.js。TimeredCounter
提供了两个适配器:
BuildInNumberAdapter
(默认): 使用Number
。
DecimalJsAdapter
: 使用decimal.js
,需要安装 decimal.js。- 当然,你可以实现自己的适配器,只需要实现
NumberAdapter
接口。
在下方示例中你可以切换适配器查看效果。
9007199254740991
是 JavaScript 中能够精确表示的最大整数,大于该值时将会丢失精度。你可以尝试输入:
9007199254740992
:9007199254740991 + 19007199254741001
:9007199254740991 + 109007199254741091
:9007199254740991 + 100观察精度丢失的情况。
点击查看代码
<script setup>
import { ref, watch } from "vue";
import { setNumberAdapter } from "timered-counter";
const number = ref(Number.MAX_SAFE_INTEGER.toString(10));
const adapters = [
{ label: "BuildInNumberAdapter", value: "number" },
// { label: "BuildInBigintAdapter", adapter: BuildInBigintAdapter() },
{ label: "DecimalJsAdapter", value: "decimal.js" },
];
const adapterIndex = ref(0);
watch(adapterIndex, (index) => setNumberAdapter(adapters[index].value));
const realRenderNumber = ref();
watch([number, adapterIndex], () => (realRenderNumber.value = number.value), {
immediate: true,
});
const counterRef = ref();
function handleAnimationEnd() {
realRenderNumber.value = counterRef.value?.getAttribute("aria-label");
}
function handleInput(e) {
number.value = e.target.value
.split("")
.filter((c) => (c >= "0" && c <= "9") || c === "-" || c === ".")
.join("");
}
</script>
<template>
<div class="text-center">
<timered-counter-number
ref="counterRef"
:key="adapters[adapterIndex].value"
:value="number"
@timered-counter-animation-end="handleAnimationEnd"
/>
</div>
<div
v-if="realRenderNumber !== number"
class="bg-[var(--vp-c-danger-soft)] p-2 rounded mt-4"
>
Mismatch: should be
<span class="text-[var(--vp-c-danger-1)]">{{ number }}</span> but rendered
as <span class="text-[var(--vp-c-danger-1)]">{{ realRenderNumber }}</span>
</div>
<hr />
<div class="flex gap-4">
<textarea
class="flex-auto border border-solid p-1"
@input="handleInput"
:value="number"
/>
<select
v-model="adapterIndex"
class="self-start border border-solid p-1 appearance-auto"
>
<option v-for="(adapter, index) in adapters" :key="index" :value="index">
{{ adapter.label }}
</option>
</select>
</div>
</template>
<style scoped></style>
支持 emoji 分词
对于一般的 emoji
,我们可以直接使用默认的配置,但是对于一些特殊的 emoji
,如 👨👩👧👦
。他们被称为 Emoji ZWJ Sequence
,是由多个 emoji
组合而成的。
这种情况下,难以将包含该 emoji
的字符串正确的识别。我们可以使用 Intl.Segmenter 或第三方库。 如 grapheme-splitter。TimeredCounter
提供了三个适配器:
BuildInStringAdapter
(默认): 使用String.split("")
分割字符串。IntlSegmenterAdapter
: 使用Intl.Segmenter
,需要浏览器支持。GraphemeSplitterAdapter
: 使用grapheme-splitter
。需要安装 grapheme-splitter
在下方示例中你可以切换适配器查看效果。
你可以尝试输入下列字符查看效果。
⌚
:基础 emoji↔️
:文本字符渲染为 emoji👩
:可修饰的基础 emoji👩🏿
:可修饰的基础 emoji + emoji 修饰符🧑💻
:emoji 组合序列
点击查看代码
<script setup>
import { onMounted, ref, watch } from "vue";
import { setStringAdapter } from "timered-counter";
const string = ref("emoji 🎉 🧑💻");
const adapters = [
{ label: "BuildInStringAdapter", value: "string" },
{
label: "BuildInIntlSegmenterAdapter",
value: "intl-segmenter",
},
{ label: "GraphemeSplitterAdapter", value: "grapheme-splitter" },
];
const adapterIndex = ref(0);
watch(adapterIndex, (index) => setStringAdapter(adapters[index].value));
const realRenderString = ref();
watch([string, adapterIndex], () => (realRenderString.value = string.value), {
immediate: true,
});
const counterRef = ref();
function handleAnimationEnd() {
realRenderString.value = counterRef.value?.getAttribute("aria-label");
}
onMounted(() => setTimeout(() => handleAnimationEnd(), 1000));
</script>
<template>
<div class="text-center">
<timered-counter-string
ref="counterRef"
:value="string"
:key="adapters[adapterIndex].value"
@timered-counter-animation-end="handleAnimationEnd"
/>
</div>
<div
v-if="realRenderString !== string"
class="bg-[var(--vp-c-danger-soft)] p-2 rounded mt-4"
>
Mismatch: should be
<span class="text-[var(--vp-c-danger-1)]">{{ string }}</span> but rendered
as <span class="text-[var(--vp-c-danger-1)]">{{ realRenderString }}</span>
</div>
<hr />
<div class="flex gap-4">
<textarea class="flex-auto border border-solid p-1" v-model="string" />
<select
v-model="adapterIndex"
class="self-start border border-solid p-1 appearance-auto"
>
<option v-for="(adapter, index) in adapters" :key="index" :value="index">
{{ adapter.label }}
</option>
</select>
</div>
</template>
<style scoped></style>