배치 작업 - 순수한 불변성이 가치가 없을 때

시리즈의 이전 부분에서 나는 질문으로 기사를 끝냈습니다.
  • 업데이트를 일괄 처리할 때 복사 성능을 개선할 수 있습니까?

  • 우리는 불변성이 부작용을 피하는 좋은 방법이라는 것을 보았습니다. 그렇다면 왜(그리고 언제) 누군가 그것을 사용하지 않을까요?

    사용 사례 예



    UI 애플리케이션에 다음이 포함되어 있다고 상상해 보십시오.
  • 이메일 문자열의 A입니다.

  • 이메일 등록/해제 기능 이메일 문자열 배열을 받은 다음 그에 따라 세트를 업데이트합니다.

  • let emailSet = new Set([
        '[email protected]', 
        '[email protected]',
        '[email protected]',
        //...
    ]);
    
    const registerEmails = (list: string[]) => {
        list.forEach(email => {
            emailSet = new Set(emailSet).add(email)
        })
    }
    
    const unregisterEmails = (list: string[]) => {
        list.forEach(email => {
            emailSet = new Set(emailSet).delete(email)
        })
    }
    
    

    💡 컨셉에 대한 설명은 편하게 확인해주세요.

    두 함수 모두 변경 없이 업데이트됩니다emailSet. 항상 Set with new Set() 생성자의 새 복사본을 만든 다음 최신 버전만 변경합니다. 이는 다음과 같은 몇 가지 과제를 제시합니다.

    제약 - 복제 비용이 많이 들 수 있음



    세트를 복제할 때 각 항목이 새 세트로 복사되므로 복제에 소요된 총 시간은 세트 크기O(size(Set))에 비례합니다. 이것이 원래 세트의 부작용을 피하면서 가능한 한 복제를 피해야 하는 주된 이유입니다.

    문제 #1 - 변경되지 않은 세트 복제



    다음과 같은 경우 불필요한 복제가 수행됩니다.
  • 기존 이메일을 등록합니다
  • .
  • 존재하지 않는 이메일 등록 취소

  • 이것은 수정하기 쉽습니다. "선택적 복제"를 수행하도록 기능을 업데이트할 수 있습니다(실제 수정이 있는 경우에만 Set 변경).

    const registerEmails = (list: string[]) => {
        list.forEach(email => {
            /* Check if email not registered before cloning */
            if (!emailSet.has(email)) {
                emailSet = new Set(emailSet).add(email)
            }
        })
    }
    
    const unregisterEmails = (list: string[]) => {
        list.forEach(email => {
            /* Check if email registered before cloning */
            if (emailSet.has(email) {
                emailSet = new Set(emailSet).delete(email)
            }
        })
    }
    

    💡 클라이언트 측 프레임워크(예: Angular, React 등)는 일반적으로 === 테스트에 의존하여 구성 요소 변경 사항을 감지합니다. 쓸모없는 복제를 강제하면 복제 프로세스와 프레임워크 내부 diff 검사 모두에서 시간이 낭비됩니다.

    문제 #2 - 돌연변이를 일괄 처리하지 않음



    우리 코드는 특정 상황에서 여전히 성능이 좋지 않습니다. 등록/등록 해제할 10개의 이메일 목록을 수신하면 세트가 forEach 루프 내에서 10번 복제될 수 있습니다.

    registerEmails([
        '[email protected]', // New email, clone Set
        '[email protected]', // New email, clone Set
        '[email protected]', // New email, clone Set
        //... (New email, clone Set x7)
    ])
    

    Is it possible to keep the immutability benefits while also speeding up code execution? We still want a new Set, but this much cloning is not desired in this case.


    배치



    위의 문제에 대한 해결책을 일괄 처리라고 합니다. 일괄 처리 컨텍스트 외부에서 보면 모든 것이 변경 불가능해 보이지만(부작용 없음) 내부에서는 가능한 경우 변경 가능성을 사용합니다.

    배처는 대상 개체(이 경우 Set)를 래핑하고 규칙을 따르는 변경을 위한 API를 제공합니다.

  • 절대적으로 필요할 때까지 복제 대상을 지연합니다(전화 willChange() ).
  • 개체가 복제된 후 이후에 필요한 만큼 개체를 변경할 수 있습니다(mutate currentValue ).
  • registerEmails 함수의 배처를 예로 들어 보겠습니다.

    const registerEmails = (list: string[]) => {
        /* Create the batcher context for emailSet */
        let batcher = prepareBatcher(emailSet);
    
        list.forEach(email => {
            /* Use batcher currentValue property to refer to Set */
            if (!batcher.currentValue.has(email)) {
                /* Let batcher know a change is about to happen */
                batcher.willChange();
                /* We can mutate currentValue (Set) directly now */
                batcher.currentValue.add(email)
                /* Update our emailSet variable */
                emailSet = batcher.currentValue;
            }
        })
    }
    
    
    

    구성 가능한 Batcher



    이전 코드는 성능이 우수하지만 일괄 처리 아키텍처에 코드 재사용 가능성이 존재할 수 있습니다. 이를 구현하는 한 가지 방법은 다음과 같습니다.
  • 함수는 객체가 아닌 배처(수정할 객체를 래핑함)를 인수로 받습니다.
  • 함수가 Batcher API를 사용하여 원하는 변경을 수행합니다.
  • 결국 이 함수는 객체가 아닌 배처를 반환합니다.

  • 이전 코드 스니펫을 더 재사용 가능한 함수로 리팩터링해 보겠습니다.

    /* This can be reused for any Set */
    const add = <T>(batcher: Batcher<Set<T>>, item: T) => {
        if (!batcher.currentValue.has(item)) {
            batcher.willChange();
            batcher.currentValue.add(item);
        }
        return batcher;
    }
    
    /* This can be reused for any Set */
    const remove = <T>(batcher: Batcher<Set<T>>, item: T) => {
        if (batcher.currentValue.has(item)) {
            batcher.willChange();
            batcher.currentValue.delete(item);
        }
        return batcher;
    }
    

    이제 함수를 프로젝트로 가져올 수 있습니다.

    const registerEmails = (batcher: Batcher<Set<string>>, list: string[]) => {
        list.forEach(email => {
            add(batcher, email);
        });
        return batcher;
    }
    
    const unregisterEmails = (batcher: Batcher<Set<string>>, list: string[]) => {
        list.forEach(email => {
            remove(batcher, email);
        });
        return batcher;
    }
    
    /* Call registerEmails */
    let batcher = prepareBatcher(emailSet);
    registerEmails(batcher, [...]);
    emailSet = batcher.currentValue;
    
    

    더 높은 수준의 절차를 계속 만들 수 있습니다.

    const complexOperation = (batcher: Batcher<Set<string>>) => {
        /* Apply operations */
        registerEmails(batcher, [...]);
        unregisterEmails(batcher, [...]);
        unregisterEmails(batcher, [...]);
        registerEmails(batcher, [...]);
        return batcher;
    }
    
    let batcher = prepareBatcher(emailSet);
    /* Call the function */
    complexOperation(batcher);
    /* Update variable */
    emailSet = batcher.currentValue;
    
  • 복제가 여전히 최대 한 번 발생합니다! 최적화가 없었다면 length(array) 내부의 각 등록/등록 취소 호출에 대해 size(Set)개의 복제본(complexOperation개의 항목 복사본 포함)이 있었을 수 있습니다.
  • 코드는 모듈식이며 재사용이 가능하므로 prepareBatcher(emailSet)를 호출하여 함수에 제공하기만 하면 됩니다.

  • 변경 사항이 없는 경우 참조 동등성은 여전히 ​​개체를 나타냅니다.

  • 개념의 증거



    저는 최근에 Batcher 아키텍처에 대한 개념 증명을 제시했습니다. 아래 CodeSandbox 예제에서 console.log s를 확인할 수 있습니다.



    소스 코드는 다음에서 찾을 수 있습니다.


    스택코메이트 / 데이터 구조


    Typescript 프로젝트를 위한 데이터 구조.





    지금은 add , removefilter 방법을 사용할 수 있습니다. 새로운 작업이 곧 제공될 예정입니다.

    좋은 웹페이지 즐겨찾기