스케줄링 가능성
예약 가능성은 트리거 동작으로 인해 부작용 함수가 다시 실행될 때 언제, 얼마나 자주, 어떤 방식으로 실행될지 결정하는 기능을 말합니다.
const data = {foo: 1}
const obj = new Proxy(data, { /*...*/ }
effect(() => {
console.log(obj.foo)
})
obj.foo++
console.log('end')
부작용 함수에서 obj.foo를 인쇄하면 의존성 수집 프로세스가 완료됩니다. obj.foo에 대한 후속 자체 증가 연산은 종속성을 트리거하여 부작용 함수가 다시 실행되도록 합니다. 최종 출력은 다음과 같습니다:
1
2
end
코드를 다시 정렬하지 않고 출력을 원한다고 가정해 보겠습니다:
1
end
2
즉, 부수 효과 함수의 트리거를 지연시키려면 효과 함수에 대한 옵션 매개 변수인 옵션을 설계하면 됩니다:
effect(
() => {
console.log(obj.foo)
},
// options
{
// 스케줄러 스케줄러
scheduler(fn) {
// ...
}
}
)
스케줄러 구현
옵션은 스케줄러 속성을 가진 객체로, 나중에 다른 속성으로 확장할 수 있습니다. 스케줄러는 부작용 함수의 실행 형태와 타이밍을 결정하는 데 사용할 수 있는 함수입니다. 효과를 위해 부수 함수를 등록하는 과정에서 옵션 객체가 두 번째 파라미터로 전달되는데, 이 옵션 객체를 부수 함수에 속성으로 탑재해야 향후 응답이 트리거될 때 옵션을 검색할 수 있으므로 그에 따라 부수 함수를 수정해야 합니다:
function effect(fn, options = {}) {
const effectFn = () => {
cleanup(effectFn)
activeEffect = effectFn
effectStack.push(effectFn)
fn()
effectStack.pop()
activeEffect = effectStack[effectStack.length - 1]
}
// effectFn에 마운트 옵션
effectFn.options = options
effectFn.deps = []
effectFn()
}
스케줄러 함수를 사용하면 트리거 함수에서 부수 함수 재실행이 트리거될 때 사용자가 전달한 스케줄러 함수를 직접 호출할 수 있으므로 사용자에게 제어권을 부여할 수 있습니다:
function trigger(target, key) {
const depsMap = bucket.get(target);
if (!depsMap) return;
// 모든 부수 함수 효과를 키별로 가져오기
const effects = depsMap.get(key);
const effectsToRun = new Set();
effects && effects.forEach((effectFn) => {
if (effectFn !== activeEffect) {
effectsToRun.add(effectFn)
}
});
effectsToRun.forEach((effectFn) => {
// 부작용 함수에 옵션이 있는 경우.scheduler스케줄러 속성,
// 이렇게 하려면 스케줄러를 호출하고 부작용 함수를 스케줄러에 인수로 전달합니다.
if (effectFn.options.scheduler) {
effectFn.options.scheduler(effectFn)
} else {
effectFn()
}
})
}
스케줄러가 부작용 함수의 실행 순서를 변경합니다.
위 코드에서 보듯이 트리거 액션이 부작용 함수의 실행을 트리거할 때, 옵션 스케줄러 스케줄러에 부작용 함수가 존재하는지 우선적으로 확인하고, 존재하는 경우 현재 부작용 함수를 파라미터로 스케줄러 함수에 전달하여 스케줄러 함수를 실행하고, 사용자가 부작용 함수의 실행 형태와 실행 시기를 제어할 수 있도록 하며, 스케줄러 속성이 존재하지 않으면 직접 부작용 함수가 실행됩니다. 스케줄러 속성이 존재하지 않으면 사이드 이펙트 함수가 직접 실행됩니다.
스케줄러 설정을 통해 설명한 요구 사항을 구현하고 코드의 실행 순서를 변경할 수 있습니다:
const data = {foo: 1}
const obj = new Proxy(data, { /*...*/ }
effect(() => {
console.log(obj.foo)
},{
options: {
scheduler(fn) {
// 부수 함수를 setTimeout에 매개 변수로 전달하면 동기 작업이 완료된 후 실행되는 매크로 작업으로 바뀝니다.
setTimeout(fn)
}
}
})
obj.foo++
console.log('end')
스케줄러에서 가져온 부작용 함수 fn을 setTimeout 함수에 전달하면 매크로 작업이 열려 부작용 함수 fn을 실행하므로 모든 동기 코드 실행이 끝나면 부작용 함수의 두 번째 실행이 예상 출력을 얻게 됩니다:
1
end
2
스케줄러가 코드 실행 횟수를 변경합니다.
다음 코드에서는 부작용 함수에서 obj.foo를 읽은 다음 obj.foo에 대해 두 개의 자체 증가 연산을 수행합니다:
const data = {foo: 1}
const obj = new Proxy(data, { /*...*/ }
effect(() => {
console.log(obj.foo)
})
obj.foo++
obj.foo++
당연히 부작용 함수는 유효 등록 시 초기 호출과 두 자체 증가 연산 모두 부작용 함수에 대한 종속성 호출을 트리거하므로 출력이 세 번 실행됩니다:
// 작업 대기열 정의
const jobQueue = new Set();
// Promise 사용.resolve() 프로미스 인스턴스를 생성하고 이를 사용하여 마이크로태스크 대기열에 작업을 추가합니다.
const p = Promise.resolve();
// 플래그는 대기열이 새로 고쳐지고 있는지 여부를 나타냅니다.
let isFlushing = false;
function flushJob() {
// 대기열이 새로 고쳐지는 중이면 아무것도 하지 마세요.
if (isFlushing) return;
// 참으로 설정하면 새로 고쳐진다는 의미입니다.
isFlushing = true;
// 마이크로태스크 대기열에서 jobQueue 대기열 새로 고치기
p.then(() => {
jobQueue.forEach((job) => job());
}).finally(() => {
// 재설정은 완료되면 플러싱
isFlushing = false;
});
}
effect(
() => {
console.log(obj.foo);
},
{
scheduler(fn) {
// 스케줄링할 때마다 jobQueue 대기열에 부작용 함수를 추가합니다.
jobQueue.add(fn);
// 대기열을 새로 고치려면 flushJob을 호출하세요.
flushJob();
},
}
);
위의 출력은 분명히 기대에 부합하지만, 생각해 보면 객체의 foo 속성이 몇 번이나 증가했는지에 상관없이 최종 값만 중요하기 때문에 객체.foo의 중간 증분 연산은 신경 쓰지 않습니다. 따라서 모든 중간 증분 연산이 부작용 함수의 실행을 트리거하지 않기를 원합니다. 이 역시 스케줄러의 도움으로 가능합니다:
여기서는 먼저 실행해야 하는 모든 부수 함수를 저장하는 것이 주된 목적인 Set, jobQueue 유형의 변수를 설정합니다. 동일한 부수 함수가 반복적으로 추가되는 것을 방지하기 위해 자동 강조 해제 기능을 활용하기 위해 Set 유형을 선택했습니다. 그런 다음 대기열이 플러시되고 있는지 여부를 나타내는 isFlushing 변수를 전역적으로 추가합니다. 동시에 flushJob 함수를 설정하여 먼저 isFlushing의 값을 판단합니다. 값이 참이면 큐가 새로 고쳐지고 있으므로 아무것도 하지 않고 종료하고, 값이 거짓이면 후속 코드로 넘어갈 수 있으며, 후속 flushJob 함수가 반복적으로 실행되지 않도록 먼저 isFlushing을 true로 설정합니다. 함수를 참으로 설정하여 후속 flushJob 호출이 반복되지 않도록 하면, flushJob 호출이 몇 번 반복되더라도 한 주기에 한 번만 실행됩니다. Promise 인스턴스 변수 p를 설정하고, flushJob 내에서 p.then을 통해 마이크로태스크 대기열에 함수를 추가하고, 마이크로태스크 대기열 내에서 jobQueue를 트래버스하고, p.finally에서 isFlushing 변수를 false로 복원하면 이후의 flushJob 호출이 참으로 이루어집니다. 거짓으로 설정하여 후속 flushJob 함수가 정상적으로 들어갈 수 있도록 합니다.
이 코드의 효과는 obj.foo에 대한 두 개의 연속 증분 연산이 스케줄러 함수를 병렬로 연속해서 두 번 실행한다는 것인데, 이는 jobQueue.add(fn) 문으로 동일한 부수 함수가 두 번 추가되지만 Set 데이터 구조의 중복 제거 기능으로 인해 현재 부수 함수인 jobQueue에는 하나의 항목만 존재하게 된다는 것을 의미합니다. 함수입니다. 마찬가지로 flushJob은 동기식 및 순차적으로 두 번 실행되지만 isFlushing 플래그로 인해 실제로는 이벤트 루프에서 한 번만, 즉 마이크로태스크 대기열에서 한 번만 실행됩니다. 마이크로태스크 큐가 실행을 시작하면 jobQueue를 탐색하고 그 안에 저장된 부수 함수를 실행합니다. 작업 큐에는 부작용 함수가 하나만 있기 때문에 한 번만 실행되며, 실행될 때쯤에는 이미 obj.foo 필드 값이 존재하므로 원하는 출력을 얻게 됩니다.
이는 Vue.js에서 반응형 데이터를 연속으로 여러 번 수정하지만 업데이트를 한 번만 트리거하는 것과 약간 비슷하지만, 실제로 Vue.js는 내부적으로 훨씬 더 나은 스케줄러를 구현하지만 일반적인 아이디어는 동일합니다.





