使用 js-coroutines 在 TypeScript 中无故障处理 1,000,000 条记录数据
有时我们需要在前端处理数据,例如使用离线系统或访问本地数据。当数据量很大时,很容易导致 UI 出现问题。几天前,我写了一篇文章,演示了如何使用 js-coroutines 实现搜索与 UI 更新同时运行。我想深入研究一下 TypeScript 中一个更强大的版本,它的功能远不止搜索;它还能实时渲染记录,并带有各种进度指示器。完成后,它会执行一系列制表操作来更新一些图表。
请注意,在搜索继续的同时,您可以继续输入,甚至开始浏览记录。这是通过主线程上的协作多任务实现的。
请注意,当此窗口缩放时,Recharts 提供的工具提示将无法正常工作。查看全屏版本
此演示使用了js-coroutines的一项新功能,允许您定义一个“单例”函数。如果上一次运行仍在进行中,单例函数会自动取消并重新开始。这正是您进行此类搜索所需要的。
const process = singleton(function*(resolve: Function, search: string, sortColumn: string) {
let yieldCounter = 0
if (!search.trim() && !sortColumn?.trim()) {
resolve({ data, searching: false })
addCharts(data)
return
}
resolve({ searching: true, data: [] })
let parts = search.toLowerCase().split(" ")
let i = 0
let progress = 0
let output : Data[] = []
for (let record of data) {
if (
parts.every(p =>
record.description
.split(" ")
.some(v => v.toLowerCase().startsWith(p))
)
) {
output.push(record)
if (output.length === 250) {
resolve({data: output})
yield sortAsync(output, (v : Data)=>v[sortColumn])
}
}
let nextProgress = ((i++ / data.length) * 100) | 0
if (nextProgress !== progress) resolve({ progress: nextProgress })
progress = nextProgress
yield* check()
}
resolve({sorting: true})
yield sortAsync(output, (v : Data)=>v[sortColumn])
resolve({sorting: false})
resolve({ searching: false, data: output })
addCharts(output)
function* check(fn?: Function) {
yieldCounter++
if ((yieldCounter & 127) === 0) {
if (fn) fn()
yield
}
}
}, {})
这个例程首先检查我们是否正在寻找某些东西,如果不是,则采取更快的路径。
假设它正在搜索,它会使用一个巧妙的技巧,即多次解析值来更新进度。这使得它能够在拥有 250 条记录时立即显示结果,每 1% 更新一次进度,然后打开和关闭搜索和排序指示器。
调用 resolve 只是将一些数据合并到标准的 React.useState() 中,它会重新绘制 UI 以确保在搜索过程中所有内容都能顺利更新。
interface Components {
data?: Array<Data>
searching?: boolean
progress?: number,
sorting?: boolean,
charts?: []
}
function UI(): JSX.Element {
const [search, setSearch] = React.useState("")
const [sortColumn, setSortColumn] = React.useState('')
const [components, setComponents] = React.useState<Components>({})
React.useEffect(() => {
setComponents({ searching: true })
// Call the singleton to process
process(merge, search, sortColumn)
}, [search, sortColumn])
return (
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
fullWidth
helperText="Search for names, colors, animals or countries. Separate words with spaces."
InputProps={{
endAdornment: components.searching ? (
<CircularProgress color="primary" size={"1em"} />
) : null
}}
variant="outlined"
value={search}
onChange={handleSetSearch}
label="Search"
/>
</Grid>
<Grid item xs={12} style={{visibility: components.searching ? 'visible' : 'hidden'}}>
<LinearProgress
variant={components.sorting ? "indeterminate": "determinate"}
value={components.progress || 0}
color="secondary"
/>
</Grid>
<Grid item xs={12}>
<RecordView sortColumn={sortColumn} onSetSortColumn={setSortColumn} records={components.data} />
</Grid>
{components.charts}
</Grid>
)
function merge(update: Components): void {
setComponents((prev: Components) => ({ ...prev, ...update }))
}
function handleSetSearch(event: React.ChangeEvent<HTMLInputElement>) {
setSearch(event.currentTarget.value)
}
}
该merge
函数会在例程进行过程中进行更新,并且由于我们定义了“单例”函数,因此只要搜索或排序属性发生变化,它就会自动停止并重新启动。
每个图表单独开始计算,我们将它们的执行“加入”到主进程中,以便重新启动主进程也将重新启动图表。
function Chart({data, column, children, cols} : {cols?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12, data: Array<Data>, column: (row: any)=>string, children?: any}) {
const [chartData, setData] = React.useState()
React.useEffect(()=>{
const promise = run(count(data, column))
// Link the lifetime of the count function to the
// main process singleton
process.join(promise).then((result: any)=>setData(result))
}, [data, column])
return <Grid item xs={cols || 6}>
{!chartData ? <CircularProgress/> : <ResponsiveContainer width='100%' height={200}>
<BarChart data={chartData}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis dataKey="name" />
<YAxis />
<Tooltip />
<Bar dataKey="value" fill="#8884d8">
{children ? children(chartData) : null}
</Bar>
</BarChart>
</ResponsiveContainer>}
</Grid>
}
这里我们混合使用了辅助异步函数和生成器,以便获得最大程度的控制。我们最后关注的生成器是用于计算图表结果的生成器:
function * count(data: Data[], column: (row: Data)=>string, forceLabelSort?: boolean) : Generator<any, Array<ChartData>, any> {
const results = yield reduceAsync(data, (accumulator: any, d: Data)=>{
const value = column(d)
accumulator[value] = (accumulator[value] || 0) + 1
return accumulator
}, {})
let output : Array<ChartData> = []
yield forEachAsync(results, (value: number, key: string)=>{
key && output.push({name: key, value})
})
if(output.length > 20 && !forceLabelSort) {
yield sortAsync(output, (v:ChartData)=>-v.value)
} else {
yield sortAsync(output, (v:ChartData)=>v.name)
}
return output
}
这个函数只是计算函数提取的标签数量,然后对结果进行适当的排序。
文章来源:https://dev.to/miketalbot/glitch-free-1-000-000-record-data-processing-in-typescript-with-js-coroutines-fmp