haori 0.6.0 → 0.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.ja.md +3 -1
  2. package/README.md +3 -1
  3. package/dist/haori.cjs.js +13 -13
  4. package/dist/haori.cjs.js.map +1 -1
  5. package/dist/haori.es.js +2603 -1778
  6. package/dist/haori.es.js.map +1 -1
  7. package/dist/haori.iife.js +13 -13
  8. package/dist/haori.iife.js.map +1 -1
  9. package/dist/index.d.ts +242 -1
  10. package/dist/package.json +1 -1
  11. package/dist/src/core.d.ts +158 -1
  12. package/dist/src/core.d.ts.map +1 -1
  13. package/dist/src/core.js +647 -79
  14. package/dist/src/core.js.map +1 -1
  15. package/dist/src/event.d.ts +2 -2
  16. package/dist/src/event.js +2 -2
  17. package/dist/src/event_dispatcher.d.ts.map +1 -1
  18. package/dist/src/event_dispatcher.js.map +1 -1
  19. package/dist/src/expression.d.ts +49 -3
  20. package/dist/src/expression.d.ts.map +1 -1
  21. package/dist/src/expression.js +148 -33
  22. package/dist/src/expression.js.map +1 -1
  23. package/dist/src/form.d.ts.map +1 -1
  24. package/dist/src/form.js +2 -1
  25. package/dist/src/form.js.map +1 -1
  26. package/dist/src/fragment.d.ts +84 -0
  27. package/dist/src/fragment.d.ts.map +1 -1
  28. package/dist/src/fragment.js +490 -75
  29. package/dist/src/fragment.js.map +1 -1
  30. package/dist/src/observer.d.ts.map +1 -1
  31. package/dist/src/observer.js +0 -7
  32. package/dist/src/observer.js.map +1 -1
  33. package/dist/src/procedure.d.ts.map +1 -1
  34. package/dist/src/procedure.js +1 -3
  35. package/dist/src/procedure.js.map +1 -1
  36. package/dist/tests/core.test.js +288 -4
  37. package/dist/tests/core.test.js.map +1 -1
  38. package/dist/tests/data-derive.test.js +159 -0
  39. package/dist/tests/data-derive.test.js.map +1 -1
  40. package/dist/tests/evaluation-profile.test.d.ts +2 -0
  41. package/dist/tests/evaluation-profile.test.d.ts.map +1 -0
  42. package/dist/tests/evaluation-profile.test.js +92 -0
  43. package/dist/tests/evaluation-profile.test.js.map +1 -0
  44. package/dist/tests/expression.test.js +35 -1
  45. package/dist/tests/expression.test.js.map +1 -1
  46. package/dist/tests/fragment.test.js +31 -0
  47. package/dist/tests/fragment.test.js.map +1 -1
  48. package/package.json +1 -1
package/dist/src/core.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * アプリケーションの中心的な機能を提供します。
6
6
  */
7
7
  import Env from './env';
8
+ import Dev from './dev';
8
9
  import Expression from './expression';
9
10
  import Form from './form';
10
11
  import Fragment, { ElementFragment, TextFragment } from './fragment';
@@ -296,20 +297,80 @@ class Core {
296
297
  if (!fragment) {
297
298
  return Promise.resolve();
298
299
  }
299
- // DOMに組み込まれている場合はmountedをtrueにする
300
- if (element.parentNode) {
301
- const parentFragment = Fragment.get(element.parentNode);
302
- if (parentFragment?.isMounted()) {
303
- fragment.setMounted(true);
304
- }
305
- else if (document.body.contains(element)) {
306
- // document.bodyに含まれている場合はマウント済みとする
307
- fragment.setMounted(true);
300
+ return Core.initializeElementFragment(fragment, false);
301
+ }
302
+ /**
303
+ * 新規 each 行を局所初期化します。
304
+ * 既存 scan の属性順序を保ちつつ、Fragment 木を直接たどります。
305
+ *
306
+ * @param fragment 新規挿入された行フラグメント
307
+ * @returns 初期化完了の Promise
308
+ */
309
+ static initializeFreshEachRow(fragment) {
310
+ return Core.initializeElementFragment(fragment, true).then(() => {
311
+ if (Core.needsScheduledEvaluateAll(fragment)) {
312
+ Core.scheduleEvaluateAll(fragment);
308
313
  }
309
- else {
310
- fragment.setMounted(false);
314
+ return undefined;
315
+ });
316
+ }
317
+ /**
318
+ * ElementFragment とその子孫を初期化します。
319
+ *
320
+ * @param fragment 対象フラグメント
321
+ * @param stopAtEach true の場合、data-each 要素では通常再帰を止める
322
+ * @returns 初期化完了の Promise
323
+ */
324
+ static initializeElementFragment(fragment, stopAtEach) {
325
+ Core.syncMountedState(fragment);
326
+ if (stopAtEach && fragment.isFreshInitializationSkippable()) {
327
+ return Promise.resolve();
328
+ }
329
+ return Core.initializeElementAttributes(fragment).then(() => {
330
+ if (Core.shouldSkipChildInitialization(fragment, stopAtEach)) {
331
+ Core.refreshDerivedSubtreeSignature(fragment);
332
+ return undefined;
311
333
  }
334
+ const childPromises = [];
335
+ fragment.getChildren().forEach(child => {
336
+ if (child instanceof ElementFragment) {
337
+ childPromises.push(Core.initializeElementFragment(child, stopAtEach));
338
+ }
339
+ else if (child instanceof TextFragment) {
340
+ childPromises.push(Core.evaluateText(child));
341
+ }
342
+ });
343
+ return Promise.all(childPromises).then(() => {
344
+ Core.refreshDerivedSubtreeSignature(fragment);
345
+ return undefined;
346
+ });
347
+ });
348
+ }
349
+ /**
350
+ * 要素初期化時の mounted 状態を同期します。
351
+ *
352
+ * @param fragment 対象フラグメント
353
+ */
354
+ static syncMountedState(fragment) {
355
+ const parent = fragment.getParent();
356
+ if (parent?.isMounted()) {
357
+ fragment.setMounted(true);
358
+ return;
312
359
  }
360
+ const target = fragment.getTarget();
361
+ if (target.parentNode && document.body.contains(target)) {
362
+ fragment.setMounted(true);
363
+ return;
364
+ }
365
+ fragment.setMounted(false);
366
+ }
367
+ /**
368
+ * scan と fresh clone 初期化で共有する属性初期化を行います。
369
+ *
370
+ * @param fragment 対象フラグメント
371
+ * @returns 属性初期化完了の Promise
372
+ */
373
+ static initializeElementAttributes(fragment) {
313
374
  let attributeChain = Promise.resolve();
314
375
  const processedAttributes = new Set();
315
376
  for (const suffix of Core.PRIORITY_ATTRIBUTE_SUFFIXES) {
@@ -338,28 +399,25 @@ class Core {
338
399
  processedAttributes.add(name);
339
400
  }
340
401
  }
341
- return attributeChain
342
- .then(() => {
343
- const condition = fragment.getAttribute(`${Env.prefix}if`);
344
- if (fragment.hasAttribute(`${Env.prefix}if`) &&
345
- (condition === false ||
346
- condition === undefined ||
347
- condition === null ||
348
- Number.isNaN(condition))) {
349
- return undefined;
350
- }
351
- const childPromises = [];
352
- fragment.getChildren().forEach(child => {
353
- if (child instanceof ElementFragment) {
354
- childPromises.push(Core.scan(child.getTarget()));
355
- }
356
- else if (child instanceof TextFragment) {
357
- childPromises.push(Core.evaluateText(child));
358
- }
359
- });
360
- return Promise.all(childPromises).then(() => undefined);
361
- })
362
- .then(() => undefined);
402
+ return attributeChain.then(() => undefined);
403
+ }
404
+ /**
405
+ * 子孫初期化をスキップすべきかどうかを返します。
406
+ *
407
+ * @param fragment 対象フラグメント
408
+ * @param stopAtEach true の場合、data-each 要素で通常再帰を止める
409
+ * @returns 子孫初期化をスキップするなら true
410
+ */
411
+ static shouldSkipChildInitialization(fragment, stopAtEach) {
412
+ const condition = fragment.getAttribute(`${Env.prefix}if`);
413
+ if (fragment.hasAttribute(`${Env.prefix}if`) &&
414
+ (condition === false ||
415
+ condition === undefined ||
416
+ condition === null ||
417
+ Number.isNaN(condition))) {
418
+ return true;
419
+ }
420
+ return stopAtEach && fragment.hasAttribute(`${Env.prefix}each`);
363
421
  }
364
422
  /**
365
423
  * エレメントに属性を設定します。
@@ -380,6 +438,8 @@ class Core {
380
438
  return fragment.setAliasedAttribute(name, aliasedAttributeName, value, fromObserver);
381
439
  }
382
440
  const promises = [];
441
+ let deriveChangedPromise = null;
442
+ let nextDeriveInputSignature = null;
383
443
  switch (name) {
384
444
  case `${Env.prefix}bind`: {
385
445
  if (value === null) {
@@ -392,10 +452,14 @@ class Core {
392
452
  break;
393
453
  }
394
454
  case `${Env.prefix}derive`:
395
- promises.push(Core.evaluateDerive(fragment, value, fragment.getRawAttribute(`${Env.prefix}derive-name`)));
455
+ nextDeriveInputSignature = Core.createDeriveInputSignature(fragment, value, fragment.getRawAttribute(`${Env.prefix}derive-name`));
456
+ deriveChangedPromise = Core.evaluateDerive(fragment, value, fragment.getRawAttribute(`${Env.prefix}derive-name`));
457
+ promises.push(deriveChangedPromise.then(() => undefined));
396
458
  break;
397
459
  case `${Env.prefix}derive-name`:
398
- promises.push(Core.evaluateDerive(fragment, fragment.getRawAttribute(`${Env.prefix}derive`), value));
460
+ nextDeriveInputSignature = Core.createDeriveInputSignature(fragment, fragment.getRawAttribute(`${Env.prefix}derive`), value);
461
+ deriveChangedPromise = Core.evaluateDerive(fragment, fragment.getRawAttribute(`${Env.prefix}derive`), value);
462
+ promises.push(deriveChangedPromise.then(() => undefined));
399
463
  break;
400
464
  case `${Env.prefix}if`:
401
465
  promises.push(Core.evaluateIf(fragment));
@@ -433,9 +497,14 @@ class Core {
433
497
  }
434
498
  return Promise.all(promises)
435
499
  .then(() => {
436
- if (name === `${Env.prefix}derive` ||
437
- name === `${Env.prefix}derive-name`) {
438
- return Core.reevaluateChildren(fragment);
500
+ if (deriveChangedPromise !== null) {
501
+ fragment.setDeriveInputSignature(nextDeriveInputSignature);
502
+ return deriveChangedPromise.then(changed => {
503
+ if (!changed) {
504
+ return undefined;
505
+ }
506
+ return Core.reevaluateChildren(fragment);
507
+ });
439
508
  }
440
509
  return undefined;
441
510
  })
@@ -628,28 +697,90 @@ class Core {
628
697
  const hasDerive = fragment.hasAttribute(`${Env.prefix}derive`);
629
698
  const hasIf = fragment.hasAttribute(`${Env.prefix}if`);
630
699
  const hasEach = fragment.hasAttribute(`${Env.prefix}each`);
700
+ const deriveExpression = fragment.getRawAttribute(`${Env.prefix}derive`);
701
+ const deriveName = fragment.getRawAttribute(`${Env.prefix}derive-name`);
702
+ let shouldSkipDerivedSubtree = false;
703
+ let shouldRecordDerivedSubtreeSignature = false;
704
+ let nextDerivedSubtreeSignature = null;
705
+ if (!hasDerive && fragment.getDeriveSubtreeSignature() !== null) {
706
+ fragment.setDeriveSubtreeSignature(null);
707
+ }
708
+ if (!hasDerive && fragment.getDeriveInputSignature() !== null) {
709
+ fragment.setDeriveInputSignature(null);
710
+ }
631
711
  if (hasDerive) {
632
- chain = chain.then(() => Core.evaluateDerive(fragment));
712
+ const nextDeriveInputSignature = Core.createDeriveInputSignature(fragment, deriveExpression, deriveName);
713
+ if (nextDeriveInputSignature === null) {
714
+ if (fragment.getDeriveInputSignature() !== null) {
715
+ fragment.setDeriveInputSignature(null);
716
+ }
717
+ chain = chain.then(() => Core.evaluateDerive(fragment, deriveExpression, deriveName).then(() => undefined));
718
+ }
719
+ else if (fragment.getDeriveInputSignature() !== nextDeriveInputSignature) {
720
+ chain = chain.then(() => {
721
+ return Core.evaluateDerive(fragment, deriveExpression, deriveName).then(() => {
722
+ fragment.setDeriveInputSignature(nextDeriveInputSignature);
723
+ return undefined;
724
+ });
725
+ });
726
+ }
633
727
  }
634
728
  if (hasIf) {
635
729
  chain = chain.then(() => Core.evaluateIf(fragment));
636
730
  }
637
731
  if (hasEach) {
732
+ if (fragment.getDeriveSubtreeSignature() !== null) {
733
+ fragment.setDeriveSubtreeSignature(null);
734
+ }
638
735
  return chain.then(() => Core.evaluateEach(fragment));
639
736
  }
640
737
  if (hasIf) {
738
+ if (fragment.getDeriveSubtreeSignature() !== null) {
739
+ fragment.setDeriveSubtreeSignature(null);
740
+ }
641
741
  return chain.then(() => undefined);
642
742
  }
643
- const promises = [];
644
- fragment.getChildren().forEach(child => {
645
- if (child instanceof ElementFragment) {
646
- promises.push(Core.evaluateAll(child, skipFragments));
743
+ if (hasDerive) {
744
+ chain = chain.then(() => {
745
+ if (!Core.canSkipStableDerivedSubtree(fragment)) {
746
+ fragment.setDeriveSubtreeSignature(null);
747
+ Core.logDerivedSubtreeProfileSnapshot(fragment, 'skip-ineligible');
748
+ return;
749
+ }
750
+ nextDerivedSubtreeSignature = Core.createDescendantBindingSignature(fragment, 'evaluateAll');
751
+ shouldRecordDerivedSubtreeSignature = true;
752
+ shouldSkipDerivedSubtree =
753
+ fragment.getDeriveSubtreeSignature() !== null &&
754
+ fragment.getDeriveSubtreeSignature() === nextDerivedSubtreeSignature;
755
+ Core.logDerivedSubtreeProfileSnapshot(fragment, shouldSkipDerivedSubtree ? 'skip-hit' : 'skip-miss');
756
+ });
757
+ }
758
+ return chain
759
+ .then(() => {
760
+ if (shouldSkipDerivedSubtree) {
761
+ return undefined;
647
762
  }
648
- else if (child instanceof TextFragment) {
649
- promises.push(Core.evaluateText(child));
763
+ const promises = [];
764
+ fragment.getChildren().forEach(child => {
765
+ if (child instanceof ElementFragment) {
766
+ if (Core.canSkipUnchangedNestedEach(child)) {
767
+ return;
768
+ }
769
+ promises.push(Core.evaluateAll(child, skipFragments));
770
+ }
771
+ else if (child instanceof TextFragment) {
772
+ promises.push(Core.evaluateText(child));
773
+ }
774
+ });
775
+ return Promise.all(promises).then(() => undefined);
776
+ })
777
+ .then(() => {
778
+ if (shouldRecordDerivedSubtreeSignature &&
779
+ nextDerivedSubtreeSignature !== null) {
780
+ fragment.setDeriveSubtreeSignature(nextDerivedSubtreeSignature);
650
781
  }
782
+ return undefined;
651
783
  });
652
- return chain.then(() => Promise.all(promises)).then(() => undefined);
653
784
  }
654
785
  /**
655
786
  * data-derive / data-derive-name を評価し、子孫要素向けの派生値を更新します。
@@ -660,22 +791,32 @@ class Core {
660
791
  * @returns Promise (評価完了時に解決)
661
792
  */
662
793
  static evaluateDerive(fragment, deriveExpression = fragment.getRawAttribute(`${Env.prefix}derive`), deriveName = fragment.getRawAttribute(`${Env.prefix}derive-name`)) {
663
- const normalizedName = typeof deriveName === 'string'
664
- ? deriveName.trim()
665
- : '';
794
+ const previousDerivedBindingData = fragment.getRawDerivedBindingData();
795
+ const normalizedName = typeof deriveName === 'string' ? deriveName.trim() : '';
666
796
  if (!deriveExpression || normalizedName === '') {
797
+ if (previousDerivedBindingData === null) {
798
+ return Promise.resolve(false);
799
+ }
667
800
  fragment.setDerivedBindingData(null);
668
- return Promise.resolve();
801
+ return Promise.resolve(true);
669
802
  }
670
803
  const result = Expression.evaluateDetailed(deriveExpression, fragment.getBindingData());
671
804
  if (result.unresolvedReference) {
805
+ if (previousDerivedBindingData === null) {
806
+ return Promise.resolve(false);
807
+ }
672
808
  fragment.setDerivedBindingData(null);
673
- return Promise.resolve();
809
+ return Promise.resolve(true);
674
810
  }
675
- fragment.setDerivedBindingData({
811
+ const nextDerivedBindingData = {
676
812
  [normalizedName]: result.value,
677
- });
678
- return Promise.resolve();
813
+ };
814
+ if (Core.createBindingSignature(previousDerivedBindingData) ===
815
+ Core.createBindingSignature(nextDerivedBindingData)) {
816
+ return Promise.resolve(false);
817
+ }
818
+ fragment.setDerivedBindingData(nextDerivedBindingData);
819
+ return Promise.resolve(true);
679
820
  }
680
821
  /**
681
822
  * テキストフラグメントを評価します。
@@ -739,6 +880,11 @@ class Core {
739
880
  return Promise.reject(new Error('Invalid each attribute.'));
740
881
  }
741
882
  let template = fragment.getTemplate();
883
+ const keyArg = fragment.getAttribute(`${Env.prefix}each-key`);
884
+ const nextEachInputSignature = Core.createBindingSignature({
885
+ key: keyArg ? String(keyArg) : null,
886
+ items: data,
887
+ });
742
888
  if (template === null) {
743
889
  // テンプレートの作成
744
890
  let found = false;
@@ -753,6 +899,7 @@ class Core {
753
899
  }
754
900
  // 最初のElementFragmentをテンプレートとして採用
755
901
  template = child.clone();
902
+ Core.markFreshInitializationSkippable(template);
756
903
  fragment.setTemplate(template);
757
904
  found = true;
758
905
  // 元のchildはchildrenから除外
@@ -767,9 +914,16 @@ class Core {
767
914
  // TextNodeやCommentNodeはテンプレートにならないので無視
768
915
  });
769
916
  // テンプレートのunmount完了後にupdateDiffを実行
770
- return this.updateDiff(fragment, data);
917
+ return this.updateDiff(fragment, data).then(() => {
918
+ fragment.setEachInputSignature(nextEachInputSignature);
919
+ });
771
920
  }
772
- return this.updateDiff(fragment, data);
921
+ if (fragment.getEachInputSignature() === nextEachInputSignature) {
922
+ return Promise.resolve();
923
+ }
924
+ return this.updateDiff(fragment, data).then(() => {
925
+ fragment.setEachInputSignature(nextEachInputSignature);
926
+ });
773
927
  }
774
928
  /**
775
929
  * data-each 属性値を仕様に従って配列へ正規化します。
@@ -792,6 +946,307 @@ class Core {
792
946
  Log.error('[Haori]', 'Invalid each attribute:', data);
793
947
  return null;
794
948
  }
949
+ /**
950
+ * nested data-each の入力が同値で、要素自身に他の動的要素が無い場合は
951
+ * evaluateAll の子走査を省略できるかどうかを返します。
952
+ *
953
+ * @param fragment 判定対象フラグメント
954
+ * @returns 省略可能なら true
955
+ */
956
+ static canSkipUnchangedNestedEach(fragment) {
957
+ if (!fragment.hasAttribute(`${Env.prefix}each`)) {
958
+ return false;
959
+ }
960
+ if (fragment.getEachInputSignature() === null) {
961
+ return false;
962
+ }
963
+ const parent = fragment.getParent();
964
+ if (parent?.closestByAttribute(`${Env.prefix}derive`) ||
965
+ parent?.closestByAttribute(`${Env.prefix}derive-name`) ||
966
+ parent?.closestByAttribute(`${Env.prefix}if`) ||
967
+ parent?.closestByAttribute(`${Env.prefix}fetch`) ||
968
+ parent?.closestByAttribute(`${Env.prefix}import`)) {
969
+ return false;
970
+ }
971
+ if (Core.hasNonEachDynamicElementState(fragment)) {
972
+ return false;
973
+ }
974
+ const data = Core.resolveEachItems(fragment);
975
+ if (data === null) {
976
+ return false;
977
+ }
978
+ const keyArg = fragment.getAttribute(`${Env.prefix}each-key`);
979
+ const nextEachInputSignature = Core.createBindingSignature({
980
+ key: keyArg ? String(keyArg) : null,
981
+ items: data,
982
+ });
983
+ return fragment.getEachInputSignature() === nextEachInputSignature;
984
+ }
985
+ /**
986
+ * data-derive subtree の入力が同値で、保守条件も満たす場合に
987
+ * 子走査を省略できるかどうかを返します。
988
+ *
989
+ * @param fragment 判定対象フラグメント
990
+ * @returns 省略可能なら true
991
+ */
992
+ static canSkipStableDerivedSubtree(fragment) {
993
+ if (!fragment.hasAttribute(`${Env.prefix}derive`)) {
994
+ return false;
995
+ }
996
+ if (fragment.hasAttribute(`${Env.prefix}if`) ||
997
+ fragment.hasAttribute(`${Env.prefix}each`) ||
998
+ fragment.hasAttribute(`${Env.prefix}fetch`) ||
999
+ fragment.hasAttribute(`${Env.prefix}import`)) {
1000
+ return false;
1001
+ }
1002
+ return !Core.hasDisallowedDerivedSubtreeDescendant(fragment);
1003
+ }
1004
+ /**
1005
+ * data-derive subtree skip の初期 PoC で扱わない子孫要素を含むかを返します。
1006
+ *
1007
+ * @param fragment 判定対象フラグメント
1008
+ * @returns 含むなら true
1009
+ */
1010
+ static hasDisallowedDerivedSubtreeDescendant(fragment) {
1011
+ return fragment.getChildren().some(child => {
1012
+ if (!(child instanceof ElementFragment)) {
1013
+ return false;
1014
+ }
1015
+ if (child.hasAttribute(`${Env.prefix}derive`) ||
1016
+ child.hasAttribute(`${Env.prefix}derive-name`) ||
1017
+ child.hasAttribute(`${Env.prefix}fetch`) ||
1018
+ child.hasAttribute(`${Env.prefix}import`)) {
1019
+ return true;
1020
+ }
1021
+ return Core.hasDisallowedDerivedSubtreeDescendant(child);
1022
+ });
1023
+ }
1024
+ /**
1025
+ * data-derive host が子孫要素へ公開している binding の署名を返します。
1026
+ *
1027
+ * @param fragment 対象フラグメント
1028
+ * @returns binding 署名
1029
+ */
1030
+ static createDescendantBindingSignature(fragment, source) {
1031
+ Core.recordDerivedSubtreeSignatureComputation(fragment, source);
1032
+ return Core.createBindingSignature(fragment.getDescendantBindingData());
1033
+ }
1034
+ /**
1035
+ * data-derive 実行前の入力署名を返します。
1036
+ *
1037
+ * @param fragment 対象フラグメント
1038
+ * @param deriveExpression 導出式
1039
+ * @param deriveName 導出名
1040
+ * @returns 入力署名。導出が無効なら null
1041
+ */
1042
+ static createDeriveInputSignature(fragment, deriveExpression, deriveName) {
1043
+ const normalizedName = typeof deriveName === 'string' ? deriveName.trim() : '';
1044
+ if (!deriveExpression || normalizedName === '') {
1045
+ return null;
1046
+ }
1047
+ return Core.createBindingSignature({
1048
+ expression: deriveExpression,
1049
+ name: normalizedName,
1050
+ scope: fragment.getBindingData(),
1051
+ });
1052
+ }
1053
+ /**
1054
+ * data-derive subtree skip 用の署名を現在状態で更新します。
1055
+ *
1056
+ * @param fragment 対象フラグメント
1057
+ */
1058
+ static refreshDerivedSubtreeSignature(fragment) {
1059
+ if (!Core.canSkipStableDerivedSubtree(fragment)) {
1060
+ fragment.setDeriveSubtreeSignature(null);
1061
+ Core.logDerivedSubtreeProfileSnapshot(fragment, 'skip-ineligible');
1062
+ return;
1063
+ }
1064
+ fragment.setDeriveSubtreeSignature(Core.createDescendantBindingSignature(fragment, 'refresh'));
1065
+ Core.logDerivedSubtreeProfileSnapshot(fragment, 'refresh');
1066
+ }
1067
+ /**
1068
+ * data-derive subtree skip のプロファイルを取得または初期化します。
1069
+ *
1070
+ * @param fragment 対象フラグメント
1071
+ * @returns プロファイル
1072
+ */
1073
+ static getOrCreateDerivedSubtreeProfile(fragment) {
1074
+ if (!Dev.isEnabled() || !fragment.hasAttribute(`${Env.prefix}derive`)) {
1075
+ return null;
1076
+ }
1077
+ const existing = Core.DERIVE_SUBTREE_PROFILES.get(fragment);
1078
+ if (existing) {
1079
+ return existing;
1080
+ }
1081
+ const profile = {
1082
+ hostId: Core.createDerivedSubtreeHostId(fragment),
1083
+ signatureComputeTotal: 0,
1084
+ signatureComputeFromEvaluateAll: 0,
1085
+ signatureComputeFromRefresh: 0,
1086
+ skipHitCount: 0,
1087
+ skipMissCount: 0,
1088
+ skipIneligibleCount: 0,
1089
+ };
1090
+ Core.DERIVE_SUBTREE_PROFILES.set(fragment, profile);
1091
+ return profile;
1092
+ }
1093
+ /**
1094
+ * data-derive subtree host の識別子を作成します。
1095
+ *
1096
+ * @param fragment 対象フラグメント
1097
+ * @returns host 識別子
1098
+ */
1099
+ static createDerivedSubtreeHostId(fragment) {
1100
+ const segments = [];
1101
+ let current = fragment;
1102
+ while (current) {
1103
+ const target = current.getTarget();
1104
+ if (!(target instanceof HTMLElement)) {
1105
+ break;
1106
+ }
1107
+ let segment = target.tagName.toLowerCase();
1108
+ if (target.id.trim() !== '') {
1109
+ segment += `#${target.id.trim()}`;
1110
+ segments.unshift(segment);
1111
+ break;
1112
+ }
1113
+ const deriveName = current.getRawAttribute(`${Env.prefix}derive-name`);
1114
+ if (typeof deriveName === 'string' && deriveName.trim() !== '') {
1115
+ segment += `[${Env.prefix}derive-name="${deriveName.trim()}"]`;
1116
+ }
1117
+ const parent = current.getParent();
1118
+ if (parent) {
1119
+ const siblingIndex = parent
1120
+ .getChildren()
1121
+ .filter(child => child instanceof ElementFragment)
1122
+ .findIndex(child => child === current);
1123
+ segment += `:nth-child(${siblingIndex + 1})`;
1124
+ }
1125
+ segments.unshift(segment);
1126
+ current = parent;
1127
+ }
1128
+ return segments.join(' > ');
1129
+ }
1130
+ /**
1131
+ * data-derive subtree の署名計算回数を記録します。
1132
+ *
1133
+ * @param fragment 対象フラグメント
1134
+ * @param source 計算元
1135
+ */
1136
+ static recordDerivedSubtreeSignatureComputation(fragment, source) {
1137
+ const profile = Core.getOrCreateDerivedSubtreeProfile(fragment);
1138
+ if (profile === null) {
1139
+ return;
1140
+ }
1141
+ profile.signatureComputeTotal += 1;
1142
+ if (source === 'refresh') {
1143
+ profile.signatureComputeFromRefresh += 1;
1144
+ return;
1145
+ }
1146
+ profile.signatureComputeFromEvaluateAll += 1;
1147
+ }
1148
+ /**
1149
+ * data-derive subtree の現在プロファイルをログ出力します。
1150
+ *
1151
+ * @param fragment 対象フラグメント
1152
+ * @param reason ログ理由
1153
+ */
1154
+ static logDerivedSubtreeProfileSnapshot(fragment, reason) {
1155
+ const profile = Core.getOrCreateDerivedSubtreeProfile(fragment);
1156
+ if (profile === null) {
1157
+ return;
1158
+ }
1159
+ if (reason === 'skip-hit') {
1160
+ profile.skipHitCount += 1;
1161
+ }
1162
+ else if (reason === 'skip-miss') {
1163
+ profile.skipMissCount += 1;
1164
+ }
1165
+ else if (reason === 'skip-ineligible') {
1166
+ profile.skipIneligibleCount += 1;
1167
+ }
1168
+ Log.info('[Haori][derive-profile]', {
1169
+ reason,
1170
+ hostId: profile.hostId,
1171
+ signatureComputeTotal: profile.signatureComputeTotal,
1172
+ signatureComputeFromEvaluateAll: profile.signatureComputeFromEvaluateAll,
1173
+ signatureComputeFromRefresh: profile.signatureComputeFromRefresh,
1174
+ skipHitCount: profile.skipHitCount,
1175
+ skipMissCount: profile.skipMissCount,
1176
+ skipIneligibleCount: profile.skipIneligibleCount,
1177
+ });
1178
+ }
1179
+ /**
1180
+ * data-each 以外の動的要素状態を持つかどうかを返します。
1181
+ *
1182
+ * @param fragment 判定対象フラグメント
1183
+ * @returns 該当するなら true
1184
+ */
1185
+ static hasNonEachDynamicElementState(fragment) {
1186
+ const allowedEachAttributes = new Set([
1187
+ `${Env.prefix}each`,
1188
+ `${Env.prefix}each-key`,
1189
+ `${Env.prefix}each-arg`,
1190
+ `${Env.prefix}each-index`,
1191
+ ]);
1192
+ const hasDynamicAttributes = fragment.getAttributeNames().some(name => {
1193
+ if (allowedEachAttributes.has(name)) {
1194
+ return false;
1195
+ }
1196
+ if (name.startsWith(`${Env.prefix}attr-`)) {
1197
+ return true;
1198
+ }
1199
+ if (name.startsWith(Env.prefix)) {
1200
+ return true;
1201
+ }
1202
+ const value = fragment.getRawAttribute(name);
1203
+ return typeof value === 'string' && value.includes('{{');
1204
+ });
1205
+ if (hasDynamicAttributes) {
1206
+ return true;
1207
+ }
1208
+ return fragment.getChildren().some(child => child instanceof TextFragment && child.hasDynamicContent());
1209
+ }
1210
+ /**
1211
+ * fresh clone 初期化を subtree ごと省略できるかどうかを事前計算します。
1212
+ *
1213
+ * @param fragment 判定対象フラグメント
1214
+ * @returns subtree 全体を省略可能なら true
1215
+ */
1216
+ static markFreshInitializationSkippable(fragment) {
1217
+ const hasDynamicAttributes = fragment
1218
+ .getAttributeNames()
1219
+ .some(name => Core.isFreshInitializationDynamicAttribute(fragment, name));
1220
+ const hasDynamicChildren = fragment.getChildren().some(child => {
1221
+ if (child instanceof ElementFragment) {
1222
+ return !Core.markFreshInitializationSkippable(child);
1223
+ }
1224
+ if (child instanceof TextFragment) {
1225
+ return child.hasDynamicContent();
1226
+ }
1227
+ return false;
1228
+ });
1229
+ const skippable = !hasDynamicAttributes && !hasDynamicChildren;
1230
+ fragment.setFreshInitializationSkippable(skippable);
1231
+ return skippable;
1232
+ }
1233
+ /**
1234
+ * fresh clone 初期化で再評価が必要な属性かどうかを返します。
1235
+ *
1236
+ * @param fragment 判定対象フラグメント
1237
+ * @param name 属性名
1238
+ * @returns 再評価が必要なら true
1239
+ */
1240
+ static isFreshInitializationDynamicAttribute(fragment, name) {
1241
+ if (name.startsWith(`${Env.prefix}attr-`)) {
1242
+ return true;
1243
+ }
1244
+ if (name.startsWith(Env.prefix)) {
1245
+ return true;
1246
+ }
1247
+ const value = fragment.getRawAttribute(name);
1248
+ return typeof value === 'string' && value.includes('{{');
1249
+ }
795
1250
  /**
796
1251
  * 差分を更新します。
797
1252
  *
@@ -817,50 +1272,59 @@ class Core {
817
1272
  newKeys.push(listKey);
818
1273
  keyDataMap.set(listKey, { item, itemIndex });
819
1274
  });
1275
+ const newKeySet = new Set(newKeys);
820
1276
  const removalPromises = [];
821
1277
  let childElements = parent
822
1278
  .getChildren()
823
1279
  .filter(child => child instanceof ElementFragment)
824
1280
  .filter(child => !child.hasAttribute(`${Env.prefix}each-before`) &&
825
1281
  !child.hasAttribute(`${Env.prefix}each-after`));
1282
+ const previousKeys = childElements.map(child => child.getListKey());
826
1283
  childElements = childElements.filter(child => {
827
- const index = newKeys.indexOf(String(child.getListKey()));
828
- if (index === -1) {
1284
+ if (!newKeySet.has(String(child.getListKey()))) {
829
1285
  removalPromises.push(child.remove());
830
1286
  return false;
831
1287
  }
832
1288
  return true;
833
1289
  });
834
1290
  const srcKeys = childElements.map(child => child.getListKey());
835
- const baseInsertIndex = parent
836
- .getChildren()
837
- .filter(child => child instanceof ElementFragment)
838
- .filter(child => child.hasAttribute(`${Env.prefix}each-before`)).length;
1291
+ const childElementsByKey = new Map();
1292
+ childElements.forEach(child => {
1293
+ const listKey = child.getListKey();
1294
+ if (listKey !== null && !childElementsByKey.has(listKey)) {
1295
+ childElementsByKey.set(listKey, child);
1296
+ }
1297
+ });
1298
+ const insertTargets = parent.getChildElementFragments().slice();
1299
+ const baseInsertIndex = insertTargets.filter(child => child.hasAttribute(`${Env.prefix}each-before`)).length;
839
1300
  let chain = Promise.resolve();
840
1301
  newKeys.forEach((newKey, loopIndex) => {
841
- const srcIndex = srcKeys.indexOf(newKey);
842
1302
  const { item, itemIndex } = keyDataMap.get(newKey);
843
1303
  let child;
844
- if (srcIndex !== -1) {
1304
+ const reusedChild = childElementsByKey.get(newKey);
1305
+ if (reusedChild) {
845
1306
  // 既存の要素を再利用
846
- child = childElements[srcIndex];
847
- // 既存要素にも必ずバインドデータを再セットし、キャッシュもクリア
848
- chain = chain.then(() => Core.updateRowFragment(child, item, indexKey, itemIndex, itemArg ? String(itemArg) : null, newKey)
849
- .then(() => Core.evaluateAll(child))
850
- .then(() => Core.scheduleEvaluateAll(child)));
1307
+ child = reusedChild;
1308
+ // 行の入力が同一なら子孫の再評価をスキップする。
1309
+ chain = chain.then(() => Core.updateRowFragment(child, item, indexKey, itemIndex, itemArg ? String(itemArg) : null, newKey).then(changed => {
1310
+ if (!changed) {
1311
+ return undefined;
1312
+ }
1313
+ return Core.evaluateAll(child);
1314
+ }));
851
1315
  }
852
1316
  else {
853
1317
  // 新しい要素を追加
854
1318
  child = template.clone();
855
1319
  const currentInsertIndex = baseInsertIndex + loopIndex;
856
1320
  chain = chain.then(() => Core.updateRowFragment(child, item, indexKey, itemIndex, itemArg ? String(itemArg) : null, newKey).then(() => {
857
- const referenceChild = parent
858
- .getChildren()
859
- .filter(currentChild => currentChild instanceof ElementFragment)[currentInsertIndex] || null;
1321
+ const referenceChild = insertTargets[currentInsertIndex] ?? null;
860
1322
  return parent
861
1323
  .insertBefore(child, referenceChild)
862
- .then(() => Core.evaluateAll(child))
863
- .then(() => Core.scheduleEvaluateAll(child));
1324
+ .then(() => {
1325
+ insertTargets.splice(currentInsertIndex, 0, child);
1326
+ })
1327
+ .then(() => Core.initializeFreshEachRow(child));
864
1328
  }));
865
1329
  }
866
1330
  });
@@ -870,8 +1334,10 @@ class Core {
870
1334
  // eachupdateイベントを発火
871
1335
  const validNewKeys = newKeys.filter((key) => key !== null);
872
1336
  const validSrcKeys = srcKeys.filter((key) => key !== null);
873
- const addedKeys = validNewKeys.filter(key => !validSrcKeys.includes(key));
874
- const removedKeys = validSrcKeys.filter(key => !validNewKeys.includes(key));
1337
+ const validSrcKeySet = new Set(validSrcKeys);
1338
+ const addedKeys = validNewKeys.filter(key => !validSrcKeySet.has(key));
1339
+ const previousValidKeys = previousKeys.filter((key) => key !== null);
1340
+ const removedKeys = previousValidKeys.filter(key => !newKeySet.has(key));
875
1341
  HaoriEvent.eachUpdate(parent.getTarget(), addedKeys, removedKeys, validNewKeys);
876
1342
  return undefined;
877
1343
  });
@@ -944,12 +1410,112 @@ class Core {
944
1410
  }
945
1411
  else {
946
1412
  Log.error('[Haori]', `Primitive value requires '${Env.prefix}each-arg' attribute: ${data}`);
947
- return Promise.resolve();
1413
+ return Promise.resolve(false);
948
1414
  }
949
1415
  }
1416
+ const normalizedBindingData = bindingData;
1417
+ const nextRenderSignature = Core.createBindingSignature({
1418
+ listKey,
1419
+ bindingData: normalizedBindingData,
1420
+ });
1421
+ if (rowFragment.getListKey() === listKey &&
1422
+ rowFragment.getRenderSignature() === nextRenderSignature) {
1423
+ return Promise.resolve(false);
1424
+ }
950
1425
  rowFragment.setListKey(listKey);
951
- rowFragment.setBindingData(bindingData);
952
- return rowFragment.setAttribute(`${Env.prefix}row`, listKey);
1426
+ rowFragment.setRenderSignature(nextRenderSignature);
1427
+ rowFragment.setBindingData(normalizedBindingData);
1428
+ return rowFragment
1429
+ .setAttribute(`${Env.prefix}row`, listKey)
1430
+ .then(() => true);
1431
+ }
1432
+ /**
1433
+ * 新規挿入行に遅延再評価が必要かどうかを判定します。
1434
+ *
1435
+ * @param fragment 判定対象の行フラグメント
1436
+ * @returns 遅延再評価が必要なら true
1437
+ */
1438
+ static needsScheduledEvaluateAll(fragment) {
1439
+ const stack = [fragment];
1440
+ while (stack.length > 0) {
1441
+ const current = stack.pop();
1442
+ current.getChildElementFragments().forEach(child => {
1443
+ stack.push(child);
1444
+ });
1445
+ if (current !== fragment &&
1446
+ !current.isMounted() &&
1447
+ Core.hasMountSensitiveAttribute(current)) {
1448
+ return true;
1449
+ }
1450
+ }
1451
+ return false;
1452
+ }
1453
+ /**
1454
+ * mounted 状態に依存して再評価が必要になりやすい属性を持つかどうかを返します。
1455
+ *
1456
+ * @param fragment 判定対象フラグメント
1457
+ * @returns 該当属性を持つなら true
1458
+ */
1459
+ static hasMountSensitiveAttribute(fragment) {
1460
+ return ['fetch', 'import'].some(suffix => fragment.hasAttribute(`${Env.prefix}${suffix}`));
1461
+ }
1462
+ /**
1463
+ * バインド値が同一かどうかを再帰的に判定します。
1464
+ *
1465
+ * @param left 比較元の値
1466
+ * @param right 比較先の値
1467
+ * @param visited 循環参照対策用の訪問済みペア
1468
+ * @returns 同一なら true
1469
+ */
1470
+ static createBindingSignature(value, seen = new WeakMap(), nextId = { value: 0 }) {
1471
+ if (value === null) {
1472
+ return 'null';
1473
+ }
1474
+ if (value === undefined) {
1475
+ return 'undefined';
1476
+ }
1477
+ if (typeof value === 'string') {
1478
+ return JSON.stringify(value);
1479
+ }
1480
+ if (typeof value === 'number' ||
1481
+ typeof value === 'boolean' ||
1482
+ typeof value === 'bigint') {
1483
+ return String(value);
1484
+ }
1485
+ if (typeof value === 'function') {
1486
+ return `[Function:${value.name || 'anonymous'}]`;
1487
+ }
1488
+ if (typeof value === 'symbol') {
1489
+ return value.toString();
1490
+ }
1491
+ if (value instanceof Date) {
1492
+ return `[Date:${value.toISOString()}]`;
1493
+ }
1494
+ if (Array.isArray(value)) {
1495
+ if (seen.has(value)) {
1496
+ return `[Circular:${seen.get(value)}]`;
1497
+ }
1498
+ const marker = `array-${nextId.value}`;
1499
+ nextId.value += 1;
1500
+ seen.set(value, marker);
1501
+ return `[${value
1502
+ .map(item => Core.createBindingSignature(item, seen, nextId))
1503
+ .join(',')}]`;
1504
+ }
1505
+ if (typeof value === 'object') {
1506
+ if (seen.has(value)) {
1507
+ return `[Circular:${seen.get(value)}]`;
1508
+ }
1509
+ const marker = `object-${nextId.value}`;
1510
+ nextId.value += 1;
1511
+ seen.set(value, marker);
1512
+ const record = value;
1513
+ return `{${Object.keys(record)
1514
+ .sort()
1515
+ .map(key => `${JSON.stringify(key)}:${Core.createBindingSignature(record[key], seen, nextId)}`)
1516
+ .join(',')}}`;
1517
+ }
1518
+ return String(value);
953
1519
  }
954
1520
  /**
955
1521
  * フラグメントの再評価を次のイベントループで実行します。
@@ -992,5 +1558,7 @@ Core.ATTRIBUTE_PLACEHOLDER_REGEX = /\{\{\{[\s\S]+?\}\}\}|\{\{[\s\S]+?\}\}/;
992
1558
  Core.REACTIVE_FETCH_STATES = new WeakMap();
993
1559
  /** data-import の自動再評価状態 */
994
1560
  Core.REACTIVE_IMPORT_STATES = new WeakMap();
1561
+ /** data-derive subtree skip の開発用プロファイル */
1562
+ Core.DERIVE_SUBTREE_PROFILES = new WeakMap();
995
1563
  export default Core;
996
1564
  //# sourceMappingURL=core.js.map