Skip to content

Commit

Permalink
🐛 constructable checking should not be cached if the function prototy…
Browse files Browse the repository at this point in the history
…pe function was added after first running (#1381)
  • Loading branch information
kuitos authored Apr 13, 2021
1 parent 94ab4fe commit 1f97acf
Show file tree
Hide file tree
Showing 4 changed files with 81 additions and 23 deletions.
1 change: 0 additions & 1 deletion src/prefetch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ declare global {
cancelIdleCallback: (handle: RequestIdleCallbackHandle) => void;
}

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface Navigator {
connection: {
saveData: boolean;
Expand Down
43 changes: 43 additions & 0 deletions src/sandbox/__tests__/common.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/**
* @author Kuitos
* @since 2021-04-12
*/

import { getTargetValue } from '../common';

describe('getTargetValue', () => {
it('should work well', () => {
const a1 = getTargetValue(window, undefined);
expect(a1).toEqual(undefined);

const a2 = getTargetValue(window, null);
expect(a2).toEqual(null);

const a3 = getTargetValue(window, function bindThis(this: any) {
return this;
});
const a3returns = a3();
expect(a3returns).toEqual(window);
});

it('should work well while function added prototype methods after first running', () => {
function prototypeAddedAfterFirstInvocation(this: any, field: string) {
this.field = field;
}
const notConstructableFunction = getTargetValue(window, prototypeAddedAfterFirstInvocation);
// `this` of not constructable function will be bound automatically, and it can not be changed by calling with special `this`
const result = {};
notConstructableFunction.call(result, '123');
expect(result).toStrictEqual({});
expect(window.field).toEqual('123');

prototypeAddedAfterFirstInvocation.prototype.addedFn = () => {};
const constructableFunction = getTargetValue(window, prototypeAddedAfterFirstInvocation);
// `this` coule be available if it be predicated as a constructable function
const result2 = {};
constructableFunction.call(result2, '456');
expect(result2).toStrictEqual({ field: '456' });
// window.field not be affected
expect(window.field).toEqual('123');
});
});
10 changes: 2 additions & 8 deletions src/sandbox/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,7 @@ export function setCurrentRunningSandboxProxy(proxy: WindowProxy | null) {
currentRunningSandboxProxy = proxy;
}

const functionBoundedValueMap = new WeakMap<CallableFunction, CallableFunction>();
export function getTargetValue(target: any, value: any): any {
const cachedBoundFunction = functionBoundedValueMap.get(value);
if (cachedBoundFunction) {
return cachedBoundFunction;
}

/*
仅绑定 isCallable && !isBoundedFunction && !isConstructable 的函数对象,如 window.console、window.atob 这类。目前没有完美的检测方式,这里通过 prototype 中是否还有可枚举的拓展方法的方式来判断
@warning 这里不要随意替换成别的判断方式,因为可能触发一些 edge case(比如在 lodash.isFunction 在 iframe 上下文中可能由于调用了 top window 对象触发的安全异常)
Expand All @@ -37,10 +31,10 @@ export function getTargetValue(target: any, value: any): any {

// copy prototype if bound function not have
// mostly a bound function have no own prototype, but it not absolute in some old version browser, see https://github.com/umijs/qiankun/issues/1121
if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype'))
if (value.hasOwnProperty('prototype') && !boundValue.hasOwnProperty('prototype')) {
boundValue.prototype = value.prototype;
}

functionBoundedValueMap.set(value, boundValue);
return boundValue;
}

Expand Down
50 changes: 36 additions & 14 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,21 +22,34 @@ export function nextTick(cb: () => void): void {
Promise.resolve().then(cb);
}

const constructableMap = new WeakMap<any | FunctionConstructor, boolean>();
const fnRegexCheckCacheMap = new WeakMap<any | FunctionConstructor, boolean>();
export function isConstructable(fn: () => any | FunctionConstructor) {
if (constructableMap.has(fn)) {
return constructableMap.get(fn);
// prototype methods might be added while code running, so we need check it every time
const hasPrototypeMethods =
fn.prototype && fn.prototype.constructor === fn && Object.getOwnPropertyNames(fn.prototype).length > 1;

if (hasPrototypeMethods) return true;

if (fnRegexCheckCacheMap.has(fn)) {
return fnRegexCheckCacheMap.get(fn);
}

const constructableFunctionRegex = /^function\b\s[A-Z].*/;
const classRegex = /^class\b/;
/*
1. 有 prototype 并且 prototype 上有定义一系列非 constructor 属性
2. 函数名大写开头
3. class 函数
满足其一则可认定为构造函数
*/
let constructable = hasPrototypeMethods;
if (!constructable) {
// fn.toString has a significant performance overhead, if hasPrototypeMethods check not passed, we will check the function string with regex
const fnString = fn.toString();
const constructableFunctionRegex = /^function\b\s[A-Z].*/;
const classRegex = /^class\b/;
constructable = constructableFunctionRegex.test(fnString) || classRegex.test(fnString);
}

// 有 prototype 并且 prototype 上有定义一系列非 constructor 属性,则可以认为是一个构造函数
const constructable =
(fn.prototype && fn.prototype.constructor === fn && Object.getOwnPropertyNames(fn.prototype).length > 1) ||
constructableFunctionRegex.test(fn.toString()) ||
classRegex.test(fn.toString());
constructableMap.set(fn, constructable);
fnRegexCheckCacheMap.set(fn, constructable);
return constructable;
}

Expand All @@ -47,9 +60,18 @@ export function isConstructable(fn: () => any | FunctionConstructor) {
* We need to discriminate safari for better performance
*/
const naughtySafari = typeof document.all === 'function' && typeof document.all === 'undefined';
export const isCallable = naughtySafari
? (fn: any) => typeof fn === 'function' && typeof fn !== 'undefined'
: (fn: any) => typeof fn === 'function';
const callableFnCacheMap = new WeakMap<CallableFunction, boolean>();
export const isCallable = (fn: any) => {
if (callableFnCacheMap.has(fn)) {
return true;
}

const callable = naughtySafari ? typeof fn === 'function' && typeof fn !== 'undefined' : typeof fn === 'function';
if (callable) {
callableFnCacheMap.set(fn, callable);
}
return callable;
};

const boundedMap = new WeakMap<CallableFunction, boolean>();
export function isBoundedFunction(fn: CallableFunction) {
Expand Down

1 comment on commit 1f97acf

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deploy preview for qiankun ready!

✅ Preview
https://qiankun-njhad3ny0-umijs.vercel.app

Built with commit 1f97acf.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.