named-properties-tracker.js 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158
  1. "use strict";
  2. // https://heycam.github.io/webidl/#idl-named-properties
  3. const IS_NAMED_PROPERTY = Symbol("is named property");
  4. const TRACKER = Symbol("named property tracker");
  5. /**
  6. * Create a new NamedPropertiesTracker for the given `object`.
  7. *
  8. * Named properties are used in DOM to let you lookup (for example) a Node by accessing a property on another object.
  9. * For example `window.foo` might resolve to an image element with id "foo".
  10. *
  11. * This tracker is a workaround because the ES6 Proxy feature is not yet available.
  12. *
  13. * @param {Object} object Object used to write properties to
  14. * @param {Object} objectProxy Object used to check if a property is already defined
  15. * @param {Function} resolverFunc Each time a property is accessed, this function is called to determine the value of
  16. * the property. The function is passed 3 arguments: (object, name, values).
  17. * `object` is identical to the `object` parameter of this `create` function.
  18. * `name` is the name of the property.
  19. * `values` is a function that returns a Set with all the tracked values for this name. The order of these
  20. * values is undefined.
  21. *
  22. * @returns {NamedPropertiesTracker}
  23. */
  24. exports.create = function (object, objectProxy, resolverFunc) {
  25. if (object[TRACKER]) {
  26. throw Error("A NamedPropertiesTracker has already been created for this object");
  27. }
  28. const tracker = new NamedPropertiesTracker(object, objectProxy, resolverFunc);
  29. object[TRACKER] = tracker;
  30. return tracker;
  31. };
  32. exports.get = function (object) {
  33. if (!object) {
  34. return null;
  35. }
  36. return object[TRACKER] || null;
  37. };
  38. function NamedPropertiesTracker(object, objectProxy, resolverFunc) {
  39. this.object = object;
  40. this.objectProxy = objectProxy;
  41. this.resolverFunc = resolverFunc;
  42. this.trackedValues = new Map(); // Map<Set<value>>
  43. }
  44. function newPropertyDescriptor(tracker, name) {
  45. const emptySet = new Set();
  46. function getValues() {
  47. return tracker.trackedValues.get(name) || emptySet;
  48. }
  49. const descriptor = {
  50. enumerable: true,
  51. configurable: true,
  52. get() {
  53. return tracker.resolverFunc(tracker.object, name, getValues);
  54. },
  55. set(value) {
  56. Object.defineProperty(tracker.object, name, {
  57. enumerable: true,
  58. configurable: true,
  59. writable: true,
  60. value
  61. });
  62. }
  63. };
  64. descriptor.get[IS_NAMED_PROPERTY] = true;
  65. descriptor.set[IS_NAMED_PROPERTY] = true;
  66. return descriptor;
  67. }
  68. /**
  69. * Track a value (e.g. a Node) for a specified name.
  70. *
  71. * Values can be tracked eagerly, which means that not all tracked values *have* to appear in the output. The resolver
  72. * function that was passed to the output may filter the value.
  73. *
  74. * Tracking the same `name` and `value` pair multiple times has no effect
  75. *
  76. * @param {String} name
  77. * @param {*} value
  78. */
  79. NamedPropertiesTracker.prototype.track = function (name, value) {
  80. if (name === undefined || name === null || name === "") {
  81. return;
  82. }
  83. let valueSet = this.trackedValues.get(name);
  84. if (!valueSet) {
  85. valueSet = new Set();
  86. this.trackedValues.set(name, valueSet);
  87. }
  88. valueSet.add(value);
  89. if (name in this.objectProxy) {
  90. // already added our getter or it is not a named property (e.g. "addEventListener")
  91. return;
  92. }
  93. const descriptor = newPropertyDescriptor(this, name);
  94. Object.defineProperty(this.object, name, descriptor);
  95. };
  96. /**
  97. * Stop tracking a previously tracked `name` & `value` pair, see track().
  98. *
  99. * Untracking the same `name` and `value` pair multiple times has no effect
  100. *
  101. * @param {String} name
  102. * @param {*} value
  103. */
  104. NamedPropertiesTracker.prototype.untrack = function (name, value) {
  105. if (name === undefined || name === null || name === "") {
  106. return;
  107. }
  108. const valueSet = this.trackedValues.get(name);
  109. if (!valueSet) {
  110. // the value is not present
  111. return;
  112. }
  113. if (!valueSet.delete(value)) {
  114. // the value was not present
  115. return;
  116. }
  117. if (valueSet.size === 0) {
  118. this.trackedValues.delete(name);
  119. }
  120. if (valueSet.size > 0) {
  121. // other values for this name are still present
  122. return;
  123. }
  124. // at this point there are no more values, delete the property
  125. const descriptor = Object.getOwnPropertyDescriptor(this.object, name);
  126. if (!descriptor || !descriptor.get || descriptor.get[IS_NAMED_PROPERTY] !== true) {
  127. // Not defined by NamedPropertyTracker
  128. return;
  129. }
  130. // note: delete puts the object in dictionary mode.
  131. // if this turns out to be a performance issue, maybe add:
  132. // https://github.com/petkaantonov/bluebird/blob/3e36fc861ac5795193ba37935333eb6ef3716390/src/util.js#L177
  133. delete this.object[name];
  134. };