js-코루틴을 사용하여 TypeScript에서 글리치 없는 1,000,000 레코드 데이터 처리

오프라인 시스템을 사용하거나 로컬 데이터에 액세스하는 등 프런트 엔드에서 데이터를 처리해야 하는 경우가 있습니다. 데이터가 커지면 UI에 결함이 생길 수 있습니다. 며칠 전에 js-coroutine을 사용하여 UI 업데이트와 동시에 검색을 실행하는 방법을 보여주는 글을 썼습니다. 검색보다 더 많은 기능을 수행하는 TypeScript의 더 강력한 버전에 뛰어들겠다고 생각했습니다. 또한 진행되는 동안 레코드를 렌더링하고 다양한 진행률 표시기를 가지고 있습니다. 완료되면 여러 표를 수행하여 일부 차트를 업데이트합니다.

검색이 계속됨에 따라 계속 입력하고 레코드 탐색을 시작할 수 있는 방법에 주목하십시오. 이는 메인 스레드에서 협업 멀티태스킹을 사용하여 수행됩니다.


이 창을 확대하면 Recharts에서 제공하는 툴팁이 제대로 작동하지 않습니다. 전체 화면 버전 보기

이 데모는 "싱글톤"함수를 정의할 수 있는 js-coroutines의 새로운 기능을 사용합니다. Singleton 함수는 이전 실행이 아직 진행 중인 경우 자동으로 취소하고 다시 시작합니다. 이것이 바로 이와 같은 검색에 필요한 것입니다.

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를 호출하면 검색이 진행되는 동안 모든 것이 원활하게 업데이트되도록 UI를 다시 그리는 표준 React.useState()에 일부 데이터가 병합됩니다.

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>
}


여기에서는 헬퍼 Async 함수와 생성기를 혼합하여 사용하여 최대한 제어할 수 있습니다. 마지막으로 남은 관심 생성기는 차트 결과를 계산하는 생성기입니다.

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
}


이것은 단순히 함수에 의해 추출된 레이블을 세고 결과를 적절하게 정렬합니다.

좋은 웹페이지 즐겨찾기