Reactivity.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. type TEffect = () => any | Promise<any>;
  2. export type TReactiveObj = Record<string | number | symbol, any>;
  3. const effects = new Map<TReactiveObj, Record<string | symbol, TEffect[]>>();
  4. let currentEffect: TEffect = null;
  5. import Debugger from "../util/Debugger";
  6. const debug = Debugger.extend("reactivity");
  7. const ProxySymbol = Symbol("$Proxy");
  8. /**
  9. * Merges multiple proxies / objects into one.
  10. * @param objects All proxies / objects to be merged
  11. * @returns
  12. */
  13. export function mergeProxies(objects: Record<string, any>[]) {
  14. return reactive(
  15. objects.reduce((carrier, obj) => {
  16. for(let key in obj) {
  17. carrier[key] = obj[key];
  18. }
  19. return carrier;
  20. }, {})
  21. );
  22. }
  23. export async function effect(effect: TEffect) {
  24. currentEffect = effect;
  25. // Calling the effect immediately will make it
  26. // be detected and registered at the effects handler.
  27. await effect();
  28. currentEffect = null;
  29. return () => {
  30. effects.forEach((val, key) => {
  31. for(let prop in val) {
  32. if (val[prop].includes(effect)) {
  33. val[prop].splice(val[prop].indexOf(effect), 1);
  34. effects.set(key, val);
  35. }
  36. }
  37. });
  38. };
  39. }
  40. export function reactive(obj: TReactiveObj) {
  41. for(let property in obj) {
  42. // Proxy subobjects
  43. if ((typeof obj[property] === "object" || Array.isArray(obj[property])) && obj[ProxySymbol] === undefined) {
  44. obj[property] = reactive(obj[property]);
  45. }
  46. }
  47. obj[ProxySymbol] = true;
  48. return new Proxy(obj, {
  49. get(target, property) {
  50. // If detected no current effect
  51. // or this property is somehow undefined
  52. if (currentEffect === null || target[property] === undefined) {
  53. // Ignore
  54. return target[property];
  55. }
  56. // Ignore functions and proxies
  57. if (typeof target[property] === "function" || property === ProxySymbol) {
  58. return target[property];
  59. }
  60. // If this target has no effects yet
  61. if (!effects.has(target)) {
  62. // Add a new effect handler to it
  63. effects.set(target, {} as any);
  64. }
  65. // Retrieves the effects for the current target
  66. const targetEffects = effects.get(target);
  67. // If has no effect handler for this property yet
  68. if (!targetEffects[property]) {
  69. // Create a new one
  70. targetEffects[property] = [
  71. currentEffect
  72. ];
  73. } else {
  74. // Add the bubble to it
  75. targetEffects[property].push(currentEffect);
  76. }
  77. debug("effect access property %s from %O", property, target);
  78. return target[property];
  79. },
  80. set(target, property, value) {
  81. // JavaScript, for some reason, treats "null" as an object
  82. if (typeof value === null) {
  83. target[property] = null;
  84. } else
  85. // Only objects / arrays can be reactive
  86. if (typeof value === "object") {
  87. // If it's not proxied yet
  88. if (value[ProxySymbol] === undefined) {
  89. target[property] = reactive(value);
  90. }
  91. } else {
  92. target[property] = value;
  93. }
  94. // If has any effects for the given target
  95. if (effects.has(target)) {
  96. const targetEffects = effects.get(target);
  97. let propEffects = targetEffects[property];
  98. // If it's a valid array
  99. if (Array.isArray(propEffects)) {
  100. (async () => {
  101. for(let effect of propEffects) {
  102. await effect();
  103. }
  104. })();
  105. }
  106. }
  107. return true;
  108. }
  109. })
  110. }