diff --git a/src/api/tojs.ts b/src/api/tojs.ts index 9529a3349..d455c6030 100644 --- a/src/api/tojs.ts +++ b/src/api/tojs.ts @@ -15,62 +15,79 @@ const defaultOptions: ToJSOptions = { exportMapsAsObjects: true } -/** - * Basically, a deep clone, so that no reactive property will exist anymore. - */ -export function toJS(source: T, options?: ToJSOptions): T -export function toJS(source: any, options?: ToJSOptions): any -export function toJS(source, options: ToJSOptions, __alreadySeen: [any, any][]) // internal overload -export function toJS(source, options?: ToJSOptions, __alreadySeen: [any, any][] = []) { - // backward compatibility - if (typeof options === "boolean") options = { detectCycles: options } +function cache(map: Map, key: K, value: V, options: ToJSOptions): V { + if (options.detectCycles) map.set(key, value) + return value +} + +function toJSHelper(source, options: ToJSOptions, __alreadySeen: Map) { + if (!isObservable(source)) return source - if (!options) options = defaultOptions const detectCycles = options.detectCycles === true - // optimization: using ES6 map would be more efficient! - // optimization: lift this function outside toJS, this makes recursion expensive - function cache(value) { - if (detectCycles) __alreadySeen.push([source, value]) - return value + + if ( + detectCycles && + source !== null && + typeof source === "object" && + __alreadySeen.has(source) + ) { + return __alreadySeen.get(source) + } + + if (isObservableArray(source)) { + const res = cache(__alreadySeen, source, [] as any, options) + const toAdd = source.map(value => toJSHelper(value, options!, __alreadySeen)) + res.length = toAdd.length + for (let i = 0, l = toAdd.length; i < l; i++) res[i] = toAdd[i] + return res } - if (isObservable(source)) { - if (detectCycles && __alreadySeen === null) __alreadySeen = [] - if (detectCycles && source !== null && typeof source === "object") { - for (let i = 0, l = __alreadySeen.length; i < l; i++) - if (__alreadySeen[i][0] === source) return __alreadySeen[i][1] + + if (isObservableObject(source)) { + const res = cache(__alreadySeen, source, {}, options) + keys(source) // make sure we track the keys of the object + for (let key in source) { + res[key] = toJSHelper(source[key], options!, __alreadySeen) } + return res + } - if (isObservableArray(source)) { - const res = cache([]) - const toAdd = source.map(value => toJS(value, options!, __alreadySeen)) - res.length = toAdd.length - for (let i = 0, l = toAdd.length; i < l; i++) res[i] = toAdd[i] + if (isObservableMap(source)) { + if (options.exportMapsAsObjects === false) { + const res = cache(__alreadySeen, source, new Map(), options) + source.forEach((value, key) => { + res.set(key, toJSHelper(value, options!, __alreadySeen)) + }) return res - } - if (isObservableObject(source)) { - const res = cache({}) - keys(source) // make sure we track the keys of the object - for (let key in source) { - res[key] = toJS(source[key], options!, __alreadySeen) - } + } else { + const res = cache(__alreadySeen, source, {}, options) + source.forEach((value, key) => { + res[key] = toJSHelper(value, options!, __alreadySeen) + }) return res } - if (isObservableMap(source)) { - if (options.exportMapsAsObjects === false) { - const res = cache(new Map()) - source.forEach((value, key) => { - res.set(key, toJS(value, options!, __alreadySeen)) - }) - return res - } else { - const res = cache({}) - source.forEach((value, key) => { - res[key] = toJS(value, options!, __alreadySeen) - }) - return res - } - } - if (isObservableValue(source)) return toJS(source.get(), options!, __alreadySeen) } + + if (isObservableValue(source)) return toJSHelper(source.get(), options!, __alreadySeen) + return source } + +/** + * Basically, a deep clone, so that no reactive property will exist anymore. + */ +export function toJS(source: T, options?: ToJSOptions): T +export function toJS(source: any, options?: ToJSOptions): any +export function toJS(source, options: ToJSOptions) // internal overload +export function toJS(source, options: ToJSOptions) { + if (!isObservable(source)) return source + + // backward compatibility + if (typeof options === "boolean") options = { detectCycles: options } + if (!options) options = defaultOptions + const detectCycles = options.detectCycles === true + + let __alreadySeen + if (detectCycles) __alreadySeen = new Map() + + return toJSHelper(source, options, __alreadySeen) +} diff --git a/src/core/spy.ts b/src/core/spy.ts index 6d5c83494..458276943 100644 --- a/src/core/spy.ts +++ b/src/core/spy.ts @@ -26,7 +26,6 @@ export function spyReportEnd(change?) { export function spy(listener: (change: any) => void): Lambda { globalState.spyListeners.push(listener) return once(() => { - globalState.spyListeners = globalState.spyListeners - .filter(l => l !== listener) + globalState.spyListeners = globalState.spyListeners.filter(l => l !== listener) }) }