使

使用 js-coroutines 在 TypeScript 中无故障处理 1,000,000 条记录数据

2025-06-07

使用 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
        }
    }
}, {})
Enter fullscreen mode Exit fullscreen mode

这个例程首先检查我们是否正在寻找某些东西,如果不是,则采取更快的路径。

假设它正在搜索,它会使用一个巧妙的技巧,即多次解析值来更新进度。这使得它能够在拥有 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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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>
}
Enter fullscreen mode Exit fullscreen mode

这里我们混合使用了辅助异步函数和生成器,以便获得最大程度的控制。我们最后关注的生成器是用于计算图表结果的生成器:

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
}
Enter fullscreen mode Exit fullscreen mode

这个函数只是计算函数提取的标签数量,然后对结果进行适当的排序。

替代文本

文章来源:https://dev.to/miketalbot/glitch-free-1-000-000-record-data-processing-in-typescript-with-js-coroutines-fmp
PREV
你好!感谢你在 DEV 的第一个月的疯狂体验 :)
NEXT
从 Tailwind 迁移到 Vanilla-er CSS