diff --git a/lib/runtime/entry.js b/lib/runtime/entry.js index 272052a0..f1b6a78e 100644 --- a/lib/runtime/entry.js +++ b/lib/runtime/entry.js @@ -26,6 +26,9 @@ function Entry(id) { // The normalized namespace object that importers receive when they use // `import * as namespace from "..."` syntax. this.namespace = utils.createNamespace(); + // Map from local names to snapshots of the corresponding local values, used + // to determine when local values have changed and need to be re-broadcast. + this.snapshots = Object.create(null); } var Ep = utils.setPrototypeOf(Entry.prototype, null); @@ -205,20 +208,46 @@ function syncExportsToNamespace(entry, names) { // Called whenever module.exports might have changed, to trigger any // setters associated with the newly exported values. The names parameter // is optional; without it, all getters and setters will run. -Ep.runSetters = function (names) { +// If the '*' setter needs to be run, but not the '*' getter (names includes +// all exports/getters that changed), the runNsSetter option can be enabled. +Ep.runSetters = function (names, runNsSetter) { // Make sure entry.namespace and module.exports are up to date before we // call getExportByName(entry, name). this.runGetters(names); + if (runNsSetter && names !== void 0) { + names.push('*'); + } + // Lazily-initialized object mapping parent module identifiers to parent // module objects whose setters we might need to run. var parents; + var parentNames; forEachSetter(this, names, function (setter, name, value) { if (parents === void 0) { parents = Object.create(null); } - parents[setter.parent.id] = setter.parent; + + if (parentNames === void 0) { + parentNames = Object.create(null); + } + + var parentId = setter.parent.id; + + // When setters use the shorthand for re-exporting values, we know + // which exports in the parent module were modified, and can do less work + // when running the parent setters. + // parentNames[parentId] is set to false if there are any setters that we do + // not know which exports they modify + if (setter.exportAs !== void 0 && parentNames[parentId] !== false) { + parentNames[parentId] = parentNames[parentId] || []; + parentNames[parentId].push(setter.exportAs); + } else if (parentNames[parentId] !== false) { + parentNames[parentId] = false; + } + + parents[parentId] = setter.parent; // The param order for setters is `value` then `name` because the `name` // param is only used by namespace exports. @@ -243,119 +272,116 @@ Ep.runSetters = function (names) { var parent = parents[parentIDs[i]]; var parentEntry = entryMap[parent.id]; if (parentEntry) { - parentEntry.runSetters(); + parentEntry.runSetters( + parentNames[parentIDs[i]] || void 0, + !!parentNames[parentIDs[i]] + ); } } }; -function callSetterIfNecessary(setter, name, value, callback) { - if (name === "__esModule") { - // Ignore setters asking for module.exports.__esModule. - return; - } - - var shouldCall = false; - - if (setter.last === void 0) { - setter.last = Object.create(null); - // Always call the setter if it has never been called before. - shouldCall = true; - } - - function changed(name, value) { - var valueToCompare = value; - if (valueToCompare !== valueToCompare) { - valueToCompare = NAN; - } else if (valueToCompare === void 0) { - valueToCompare = UNDEFINED; - } - - if (setter.last[name] === valueToCompare) { - return false; - } - - setter.last[name] = valueToCompare; - return true; - } +function updateSnapshot(entry, name, newValue) { + var newSnapshot = Object.create(null); + var newKeys = []; if (name === "*") { - var keys = safeKeys(value); - var keyCount = keys.length; - for (var i = 0; i < keyCount; ++i) { - var key = keys[i]; + safeKeys(newValue).forEach(keyOfValue => { // Evaluating value[key] is risky because the property might be // defined by a getter function that logs a deprecation warning (or - // worse) when evaluated. For example, Node uses this trick to - // display a deprecation warning whenever crypto.createCredentials - // is accessed. Fortunately, when value[key] is defined by a getter + // worse) when evaluated. For example, Node uses this trick to display + // a deprecation warning whenever crypto.createCredentials is + // accessed. Fortunately, when value[key] is defined by a getter // function, it's enough to check whether the getter function itself // has changed, since we are careful elsewhere to preserve getters // rather than prematurely evaluating them. - if (changed(key, utils.valueOrGetter(value, key))) { - shouldCall = true; - } - } - } else if (changed(name, value)) { - shouldCall = true; + newKeys.push(keyOfValue); + newSnapshot[keyOfValue] = normalizeSnapshotValue( + utils.valueOrGetter(newValue, keyOfValue) + ); + }); + } else { + newKeys.push(name); + newSnapshot[name] = normalizeSnapshotValue(newValue); } - if (shouldCall) { - // Only invoke the callback if we have not called this setter - // (with a value of this name) before, or the current value is - // different from the last value we passed to this setter. - return callback(setter, name, value); + var oldSnapshot = entry.snapshots[name]; + if ( + oldSnapshot && + newKeys.every(key => oldSnapshot[key] === newSnapshot[key]) && + newKeys.length === Object.keys(oldSnapshot).length + ) { + return oldSnapshot; } + + return entry.snapshots[name] = newSnapshot; +} + +function normalizeSnapshotValue(value) { + if (value === void 0) return UNDEFINED; + if (value !== value && isNaN(value)) return NAN; + return value; } // Invoke the given callback once for every (setter, name, value) that needs to // be called. Note that forEachSetter does not call any setters itself, only the // given callback. function forEachSetter(entry, names, callback) { - var needToCheckNames = true; - if (names === void 0) { names = Object.keys(entry.setters); - needToCheckNames = false; } - var nameCount = names.length; - - for (var i = 0; i < nameCount; ++i) { - var name = names[i]; - - if (needToCheckNames && - ! hasOwn.call(entry.setters, name)) { - continue; - } - - var setters = entry.setters[name]; - var keys = Object.keys(setters); - var keyCount = keys.length; - - for (var j = 0; j < keyCount; ++j) { - var key = keys[j]; - var value = getExportByName(entry, name); - - callSetterIfNecessary(setters[key], name, value, callback); - - var getter = entry.getters[name]; - if (typeof getter === "function" && - // Sometimes a getter function will throw because it's called - // before the variable it's supposed to return has been - // initialized, so we need to know that the getter function has - // run to completion at least once. - getter.runCount > 0 && - getter.constant) { - // If we happen to know that this getter function has run - // successfully, and will never return a different value, then we - // can forget the corresponding setter, because we've already - // reported that constant value. Note that we can't forget the - // getter, because we need to remember the original value in case - // anyone tampers with entry.module.exports[name]. - delete setters[key]; + names.forEach(name => { + // Ignore setters asking for module.exports.__esModule. + if (name === "__esModule") return; + + var settersByKey = entry.setters[name]; + if (!settersByKey) return; + + var getter = entry.getters[name]; + var alreadyCalledConstantGetter = + typeof getter === "function" && + // Sometimes a getter function will throw because it's called + // before the variable it's supposed to return has been + // initialized, so we need to know that the getter function has + // run to completion at least once. + getter.runCount > 0 && + getter.constant; + + var value = getExportByName(entry, name); + + // Although we may have multiple setter functions with different keys in + // settersByKey, we can compute a snapshot of value and check it against + // entry.snapshots[name] before iterating over the individual setter + // functions, which is convenient because then all the setter.snapshot + // properties will end up referring to the same snapshot object. + var snapshot = updateSnapshot(entry, name, value); + + Object.keys(settersByKey).forEach(key => { + var setter = settersByKey[key]; + + // If value has not changed since the last time we broadcast it, then + // snapshot === entry.snapshots[name], so there's a good chance we can + // skip most/all of the setters that already have setter.snapshot === + // snapshot. If value has changed, snapshot !== entry.snapshots[name], and + // we need to broadcast the new value to every setter. + if (setter.snapshot !== snapshot) { + setter.snapshot = snapshot; + + // Invoke the setter function with the updated value. + callback(setter, name, value); + + if (alreadyCalledConstantGetter) { + // If we happen to know this getter function has run successfully + // (getter.runCount > 0), and will never return a different value + // (getter.constant), then we can forget the corresponding setter, + // because we've already reported that constant value. Note that we + // can't forget the getter, because we need to remember the original + // value in case anyone tampers with entry.module.exports[name]. + delete settersByKey[key]; + } } - } - } + }); + }); } function getExportByName(entry, name) { diff --git a/lib/runtime/index.js b/lib/runtime/index.js index fe8bc453..76efab54 100644 --- a/lib/runtime/index.js +++ b/lib/runtime/index.js @@ -94,7 +94,7 @@ function moduleExportDefault(value) { function moduleExportAs(name) { var entry = this; var includeDefault = name === "*+"; - return function (value) { + var setter = function (value) { if (name === "*" || name === "*+") { Object.keys(value).forEach(function (key) { if (includeDefault || key !== "default") { @@ -105,6 +105,12 @@ function moduleExportAs(name) { entry.exports[name] = value; } }; + + if (name !== '*+' && name !== "*") { + setter.exportAs = name; + } + + return setter; } // Platform-specific code should find a way to call this method whenever