浏览代码

refactor: speed up object functions

* mapEntry + separate kv transformers
* objectGet/Set + retParent in Set
* deepCopy/Equal + deepCopyDiff
tophf 3 年之前
父节点
当前提交
b5ad586d2c
共有 6 个文件被更改,包括 94 次插入51 次删除
  1. 1 0
      .babelrc.js
  2. 2 2
      src/background/utils/options.js
  3. 1 1
      src/background/utils/script.js
  4. 88 46
      src/common/object.js
  5. 1 1
      src/options/views/edit/values.vue
  6. 1 1
      src/popup/index.js

+ 1 - 0
.babelrc.js

@@ -18,5 +18,6 @@ module.exports = {
   ],
   plugins: [
     './scripts/babel-plugin-safe-bind.js',
+    ['@babel/plugin-transform-for-of', { assumeArray: true }],
   ],
 };

+ 2 - 2
src/background/utils/options.js

@@ -11,7 +11,7 @@ Object.assign(commands, {
   },
   /** @return {Object} */
   GetOptions(data) {
-    return data::mapEntry(([key]) => getOption(key));
+    return data::mapEntry((_, key) => getOption(key));
   },
   /** @return {void} */
   SetOptions(data) {
@@ -72,7 +72,7 @@ export function getOption(key, def) {
   const keys = normalizeKeys(key);
   const mainKey = keys[0];
   const value = options[mainKey] ?? deepCopy(defaults[mainKey]) ?? def;
-  return keys.length > 1 ? objectGet(value, keys.slice(1), def) : value;
+  return keys.length > 1 ? objectGet(value, keys.slice(1)) ?? def : value;
 }
 
 export async function setOption(key, value) {

+ 1 - 1
src/background/utils/script.js

@@ -65,7 +65,7 @@ const metaOptionalTypes = {
 };
 export function parseMeta(code) {
   // initialize meta
-  const meta = metaTypes::mapEntry(([, value]) => value.default());
+  const meta = metaTypes::mapEntry(value => value.default());
   const metaBody = code.match(METABLOCK_RE)[1] || '';
   metaBody.replace(/(?:^|\n)\s*\/\/\x20(@\S+)(.*)/g, (_match, rawKey, rawValue) => {
     const [keyName, locale] = rawKey.slice(1).split(':');

+ 88 - 46
src/common/object.js

@@ -1,38 +1,40 @@
+/** @type {boolean} */
+let deepDiff;
+
 export function normalizeKeys(key) {
   if (key == null) return [];
   if (Array.isArray(key)) return key;
   return `${key}`.split('.').filter(Boolean);
 }
 
-export function objectGet(obj, rawKey, def) {
-  const keys = normalizeKeys(rawKey);
-  let res = obj;
-  keys.every((key) => {
-    if (res && typeof res === 'object' && (key in res)) {
-      res = res[key];
-      return true;
-    }
-    res = def;
-    return false;
-  });
-  return res;
+export function objectGet(obj, rawKey) {
+  for (const key of normalizeKeys(rawKey)) {
+    if (!obj || typeof obj !== 'object') break;
+    obj = obj[key];
+  }
+  return obj;
 }
 
-export function objectSet(obj, rawKey, val) {
-  const keys = normalizeKeys(rawKey);
-  if (!keys.length) return val;
-  const root = obj || {};
-  let sub = root;
-  const lastKey = keys.pop();
-  keys.forEach((key) => {
-    sub = sub[key] || (sub[key] = {});
-  });
-  if (typeof val === 'undefined') {
-    delete sub[lastKey];
+/**
+ * @param {Object} [obj = {}]
+ * @param {string|string[]} [rawKey]
+ * @param {?} [val] - if `undefined` or omitted the value is deleted
+ * @param {boolean} [retParent]
+ * @return {Object} the original object or the parent of `val` if retParent is set
+ */
+export function objectSet(obj, rawKey, val, retParent) {
+  rawKey = normalizeKeys(rawKey);
+  let res = obj || {};
+  let key;
+  for (let i = 0; (key = rawKey[i], i < rawKey.length - 1); i += 1) {
+    res = res[key] || (res[key] = {});
+  }
+  if (val === undefined) {
+    delete res[key];
   } else {
-    sub[lastKey] = val;
+    res[key] = val;
   }
-  return root;
+  return retParent ? res : obj;
 }
 
 /**
@@ -42,20 +44,30 @@ export function objectSet(obj, rawKey, val) {
  * @returns {{}}
  */
 export function objectPick(obj, keys, transform) {
-  return keys.reduce((res, key) => {
+  const res = {};
+  for (const key of keys) {
     let value = obj?.[key];
     if (transform) value = transform(value, key);
-    if (value != null) res[key] = value;
-    return res;
-  }, {});
+    if (value !== undefined) res[key] = value;
+  }
+  return res;
 }
 
-// invoked as obj::mapEntry(([key, value], i, allEntries) => transformedValue)
-export function mapEntry(func) {
-  return Object.entries(this).reduce((res, entry, i, allEntries) => {
-    res[entry[0]] = func(entry, i, allEntries);
-    return res;
-  }, {});
+/**
+ * @param {function} [fnValue] - (value, newKey, obj) => newValue
+ * @param {function} [fnKey] - (key, val, obj) => newKey (if newKey is falsy the key is skipped)
+ * @param {Object} [thisObj] - passed as `this` to both functions
+ * @return {Object}
+ */
+export function mapEntry(fnValue, fnKey, thisObj) {
+  const res = {};
+  for (let key of Object.keys(this)) {
+    const val = this[key];
+    if (!fnKey || (key = thisObj::fnKey(key, val, this))) {
+      res[key] = fnValue ? thisObj::fnValue(val, key, this) : val;
+    }
+  }
+  return res;
 }
 
 // invoked as obj::forEachEntry(([key, value], i, allEntries) => {})
@@ -73,16 +85,12 @@ export function forEachValue(func, thisObj) {
   if (this) Object.values(this).forEach(func, thisObj);
 }
 
-// Needed for Firefox's browser.storage API which fails on Vue observables
 export function deepCopy(src) {
-  return src && (
-    /* Not using `map` because its result belongs to the `window` of the source,
-     * so it becomes "dead object" in Firefox after GC collects it. */
-    Array.isArray(src) && Array.from(src, deepCopy)
-    // Used in safe context
-    // eslint-disable-next-line no-restricted-syntax
-    || typeof src === 'object' && src::mapEntry(([, val]) => deepCopy(val))
-  ) || src;
+  if (!src || typeof src !== 'object') return src;
+  /* Not using `map` because its result belongs to the `window` of the source,
+   * so it becomes "dead object" in Firefox after GC collects it. */
+  if (Array.isArray(src)) return Array.from(src, deepCopy);
+  return src::mapEntry(deepCopy);
 }
 
 // Simplified deep equality checker
@@ -91,8 +99,7 @@ export function deepEqual(a, b) {
   if (!a || !b || typeof a !== typeof b || typeof a !== 'object') {
     res = a === b;
   } else if (Array.isArray(a)) {
-    res = a.length === b.length
-      && a.every((item, i) => deepEqual(item, b[i]));
+    res = a.length === b.length && a.every((item, i) => deepEqual(item, b[i]));
   } else {
     const keysA = Object.keys(a);
     const keysB = Object.keys(b);
@@ -101,3 +108,38 @@ export function deepEqual(a, b) {
   }
   return res;
 }
+
+/** @return {?} `undefined` if equal */
+export function deepCopyDiff(src, sample) {
+  if (src === sample) return;
+  if (!src || typeof src !== 'object') return src;
+  if (!sample || typeof sample !== 'object') return deepCopy(src);
+  if ((deepDiff = false, src = deepCopyDiffObjects(src, sample), deepDiff)) return src;
+}
+
+function deepCopyDiffObjects(src, sample) {
+  const isArr = Array.isArray(src);
+  const arr1 = isArr ? src : Object.keys(src);
+  const arr2 = isArr ? sample : Object.keys(sample);
+  const res = isArr ? [] : {};
+  if (arr1.length !== arr2.length) {
+    deepDiff = true;
+  }
+  for (let i = 0, key, a, b; i < arr1.length; i += 1) {
+    key = isArr ? i : arr1[i];
+    a = src[key];
+    b = isArr || arr2.includes(key) ? sample[key] : !a;
+    if (a && typeof a === 'object') {
+      if (b && typeof b === 'object') {
+        a = deepCopyDiffObjects(a, b);
+      } else {
+        a = deepCopy(a);
+        deepDiff = true;
+      }
+    } else if (a !== b) {
+      deepDiff = true;
+    }
+    res[key] = a;
+  }
+  return res;
+}

+ 1 - 1
src/options/views/edit/values.vue

@@ -298,7 +298,7 @@ export default {
           where: {
             id: this.script.props.id,
           },
-          store: current.jsonValue::mapEntry(([, val]) => dumpScriptValue(val) || ''),
+          store: current.jsonValue::mapEntry(val => dumpScriptValue(val) || ''),
         }]);
       } else {
         await this.updateValue(current);

+ 1 - 1
src/popup/index.js

@@ -28,7 +28,7 @@ Object.assign(handlers, {
     store.scriptIds.push(...ids);
     if (isTop) {
       mutex.resolve();
-      store.commands = data.menus::mapEntry(([, value]) => Object.keys(value));
+      store.commands = data.menus::mapEntry(Object.keys);
       // executeScript may(?) fail in a discarded or lazy-loaded tab, which is actually injectable
       store.injectable = true;
     }