# All files / lib/tasks reconcile-trees.js

95.9% Statements 117/122
95.65% Branches 22/23
100% Functions 1/1
95.9% Lines 117/122

Press n or j to go to the next uncovered block, b, p or k for the previous block.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    1232x
    2x
    2x
    2x
    2x
    2x
    2x
    2x
    2x
    2x
    2x
    2x
    2x
    2x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x

    234x
    234x
    234x
    234x
    234x
    234x
    234x
    162x
    162x
    162x
    162x
    152x
    152x
    152x
    152x
    152x
    152x
    152x
    162x
    162x
    162x
    162x
    162x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    73x
    73x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    41x
    41x
    234x
    40x
    40x
    40x
    40x
    45x
    45x
    45x
    45x
    45x
    45x
    40x
    40x
    45x
    40x
    40x
    40x
    36x
    36x
    4x
    4x
    2x
    2x
    40x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    234x
    177x
    177x
    177x
    177x
    177x
    177x
    234x
     import { StateCache, NODE_TYPE, VTree } from '../util/types';
    import { protectVTree } from '../util/memory';
    import createTree from '../tree/create';
    import Transaction from '../transaction';
    import release from '../release';

    /**
     * This task ensures that the Virtual DOM matches the Browser DOM. If any of
     * the markup changes between renders, the old tree is recalculated to ensure
     * accuracy.
     *
     * @param {Transaction} transaction
     */
    export default function reconcileTrees(transaction) {
      const { state, mount, input, config: options } = transaction;
      const { inner } = options;
      const mountAsHTMLEl = /** @type {HTMLElement} */ (mount);

      // Look if any changes happened before the async mutation callback.
      if (state.mutationObserver && !state.isDirty) {
        state.isDirty = Boolean(state.mutationObserver.takeRecords().length);
      }
      // If no mutation observer exists, then we cannot determine if the root is
      // dirty. Disable the isDirty check in these cases.
      else if (!state.mutationObserver) {
        state.isDirty = false;
      }

      // We rebuild the tree whenever the DOM Node changes, including the first
      // time we patch a DOM Node. This is currently problematic when someone
      // passes a DOM Node as an interpolated value we empty the VTree of its
      // contents, but the newTree still has a reference and is expecting
      // populated values.
      if (state.isDirty || !state.oldTree) {
        release(mountAsHTMLEl);

        // Ensure the mutation observer is reconnected.
        if (mountAsHTMLEl.ownerDocument && state.mutationObserver) {
          state.mutationObserver.observe(mountAsHTMLEl, {
            subtree: true,
            childList: true,
            attributes: true,
            characterData: true,
          });
        }

        state.oldTree = createTree(mountAsHTMLEl);
        protectVTree(state.oldTree);
        StateCache.set(mount, state);
      }

      const { nodeName, attributes } = state.oldTree;

      // If we are in a render transaction where no markup was previously parsed
      // then reconcile trees will attempt to create a tree based on the incoming
      // markup (JSX/html/etc).
      if (!transaction.newTree) {
        transaction.newTree = createTree(input);
      }

      const inputAsVTree = /** @type {VTree} */(transaction.newTree);

      // When `inner === false` this means we are doing outerHTML operation.  The
      // way this works is that anything that doesn't match the oldTree element
      // gets diffed internally. Anything that matches the root element at the top
      // level gets absorbed into the root element. Order is not important.
      // Elements which are matched subsequently are merged, but only the first
      // occurance of an attribute is counted. The rules are complicated, but if we
      // match the browser behavior here, it will be significantly easier to
      // convince of it's validity and to document.
      //
      // To mimic browser behavior, we loop the input and take any tree that matches
      // the root element and unwrap into the root element. We take the attributes
      // from that element and apply to the root element. This ultimately renders a
      // flat tree and allows for whitespace to be provided in the `html` function
      // without needing to trim.
      if (
        !inner &&
        inputAsVTree.nodeType === NODE_TYPE.FRAGMENT &&
        // Do not modify the new children when comparing two fragments.
        state.oldTree.nodeType !== NODE_TYPE.FRAGMENT
      ) {
        /** @type {VTree[]} */
        let foundElements = [];

        for (let i = 0; i < inputAsVTree.childNodes.length; i++) {
          const value = inputAsVTree.childNodes[i];
          const isText = value.nodeType === NODE_TYPE.TEXT;

          // This is most likely the element that is requested to compare to. Will
          // need to keep checking or more input though to be totally sure.
          if (!isText || value.nodeValue.trim()) {
            foundElements.push(value);
          }
        }

        // If only one element is found, we can use this directly.
        if (foundElements.length === 1) {
          transaction.newTree = foundElements[0];
        }
        // Otherwise consider the entire fragment.
        else if (foundElements.length > 1) {
          transaction.newTree = createTree(inputAsVTree.childNodes);
        }
      }

      // Associate the old tree with this brand new transaction.
      transaction.oldTree = state.oldTree;

      const { oldTree, newTree } = transaction;

      // If we are diffing only the parent's childNodes, then adjust the newTree to
      // be a replica of the oldTree except with the childNodes changed.
      if (inner && oldTree && newTree) {
        const isUnknown = typeof newTree.rawNodeName !== 'string';
        const isFragment = newTree.nodeType === NODE_TYPE.FRAGMENT;
        const children = isFragment && !isUnknown ? newTree.childNodes : newTree;

        transaction.newTree = createTree(nodeName, attributes, children);
      }
    }