diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts index c2fe06a..ba829c0 100644 --- a/src/__tests__/utils.test.ts +++ b/src/__tests__/utils.test.ts @@ -3,6 +3,7 @@ import { getDefaultTplWrapper, getWrapperId, getXPathForElement, + nextTask, sleep, validateExportLifecycle, } from '../utils'; @@ -116,3 +117,20 @@ test('should getXPathForElement work well', () => { const xpath1 = getXPathForElement(virtualDOM, document); expect(xpath1).toBeUndefined(); }); + +it('should nextTick just executed once in one task context', async () => { + let counter = 0; + nextTask(() => ++counter); + nextTask(() => ++counter); + nextTask(() => ++counter); + nextTask(() => ++counter); + await sleep(0); + expect(counter).toBe(1); + + await sleep(0); + nextTask(() => ++counter); + await sleep(0); + nextTask(() => ++counter); + await sleep(0); + expect(counter).toBe(3); +}); diff --git a/src/interfaces.ts b/src/interfaces.ts index e7d31fe..2eff75d 100644 --- a/src/interfaces.ts +++ b/src/interfaces.ts @@ -11,6 +11,7 @@ declare global { __POWERED_BY_QIANKUN__?: boolean; __INJECTED_PUBLIC_PATH_BY_QIANKUN__?: string; __QIANKUN_DEVELOPMENT__?: boolean; + Zone?: CallableFunction; } } diff --git a/src/sandbox/proxySandbox.ts b/src/sandbox/proxySandbox.ts index db8c61d..84ec491 100644 --- a/src/sandbox/proxySandbox.ts +++ b/src/sandbox/proxySandbox.ts @@ -5,7 +5,7 @@ */ import type { SandBox } from '../interfaces'; import { SandBoxType } from '../interfaces'; -import { nextTick } from '../utils'; +import { nextTask } from '../utils'; import { getTargetValue, setCurrentRunningSandboxProxy } from './common'; /** @@ -215,13 +215,13 @@ export default class ProxySandbox implements SandBox { }, get(target: FakeWindow, p: PropertyKey): any { - if (p === Symbol.unscopables) return unscopables; - setCurrentRunningSandboxProxy(proxy); // FIXME if you have any other good ideas // remove the mark in next tick, thus we can identify whether it in micro app or not // this approach is just a workaround, it could not cover all complex cases, such as the micro app runs in the same task context with master in some case - nextTick(() => setCurrentRunningSandboxProxy(null)); + nextTask(() => setCurrentRunningSandboxProxy(null)); + + if (p === Symbol.unscopables) return unscopables; // avoid who using window.window or window.self to escape the sandbox environment to touch the really window // see https://github.com/eligrey/FileSaver.js/blob/master/src/FileSaver.js#L13 diff --git a/src/utils.ts b/src/utils.ts index f60a0ed..a008ac0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -14,12 +14,24 @@ export function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)); } +// Promise.then might be synchronized in Zone.js context, we need to use setTimeout instead to mock next tick. +const nextTick: (cb: () => void) => void = + typeof window.Zone === 'function' ? setTimeout : (cb) => Promise.resolve().then(cb); + +let globalTaskPending = false; /** - * run a callback after next tick + * Run a callback before next task executing, and the invocation is idempotent in every singular task + * That means even we called nextTask multi times in one task, only the first callback will be pushed to nextTick to be invoked. * @param cb */ -export function nextTick(cb: () => void): void { - Promise.resolve().then(cb); +export function nextTask(cb: () => void): void { + if (!globalTaskPending) { + globalTaskPending = true; + nextTick(() => { + cb(); + globalTaskPending = false; + }); + } } const fnRegexCheckCacheMap = new WeakMap();