Skip to content

Commit

Permalink
🚀 amp-bind: Use querySelectorAll to quickly find all bound elements (#…
Browse files Browse the repository at this point in the history
…32851)

* amp-bind: Use querySelectorAll to quickly find all bound elements

* Update tests

* Fix tests

* Lint

* Fix running tests in debug.html doc

* Fix test

* Fix skipSubtree in querySelectorAll mode

* Fix test

* Lint

* Fix tests

* Use simple fastScan approach

Co-Authored-By: Jake Fried <samouri@users.noreply.github.com>

* Revert "Use simple fastScan approach"

This reverts commit e4f9bdd.

* Kick GH Actions

Co-authored-by: Jake Fried <samouri@users.noreply.github.com>
  • Loading branch information
jridgewell and samouri committed Mar 15, 2021
1 parent 72c2b41 commit b993c3f
Show file tree
Hide file tree
Showing 3 changed files with 457 additions and 227 deletions.
135 changes: 103 additions & 32 deletions extensions/amp-bind/0.1/bind-impl.js
Original file line number Diff line number Diff line change
Expand Up @@ -878,26 +878,13 @@ export class Bind {
scanNode_(node, limit) {
/** @type {!Array<!BindBindingDef>} */
const bindings = [];
const doc = devAssert(
node.nodeType == Node.DOCUMENT_NODE ? node : node.ownerDocument,
'ownerDocument is null.'
);
// Third and fourth params of `createTreeWalker` are not optional on IE11.
const walker = doc.createTreeWalker(
node,
NodeFilter.SHOW_ELEMENT,
null,
/* entityReferenceExpansion */ false
);
const walker = new BindWalker(node);
// Set to true if number of bindings in `node` exceeds `limit`.
let limitExceeded = false;
// Helper function for scanning the tree walker's next node.
// Returns true if the walker has no more nodes.
const scanNextNode_ = () => {
const node = walker.currentNode;
if (!node) {
return true;
}
// If `node` is a Document, it will be scanned first (despite
// NodeFilter.SHOW_ELEMENT). Skip it.
if (node.nodeType !== Node.ELEMENT_NODE) {
Expand All @@ -912,7 +899,7 @@ export class Bind {
// rescan() with {fast: true} for better performance. Note that only
// children are opted-out (e.g. amp-list children, not amp-list itself).
const next = FAST_RESCAN_TAGS.includes(node.nodeName)
? this.skipSubtree_(walker)
? walker.skipSubtree()
: walker.nextNode();
return !next || limitExceeded;
};
Expand Down Expand Up @@ -945,23 +932,6 @@ export class Bind {
});
}

/**
* Skips the subtree at the walker's current node and returns the next node
* in document order, if any. Otherwise, returns null.
* @param {!TreeWalker} walker
* @return {?Node}
* @private
*/
skipSubtree_(walker) {
for (let n = walker.currentNode; n; n = walker.parentNode()) {
const sibling = walker.nextSibling();
if (sibling) {
return sibling;
}
}
return null;
}

/**
* Scans the element for bindings and adds up to |quota| to `outBindings`.
* Also updates ivars `boundElements_` and `expressionToElements_`.
Expand Down Expand Up @@ -1813,3 +1783,104 @@ export class Bind {
}
}
}

class BindWalker {
/**
* @param {!Node} root
*/
constructor(root) {
const doc = devAssert(
root.nodeType == Node.DOCUMENT_NODE ? root : root.ownerDocument,
'ownerDocument is null.'
);

const useQuerySelector = doc.documentElement.hasAttribute(
'i-amphtml-binding'
);
/** @private @const {boolean} */
this.useQuerySelector_ = useQuerySelector;

/** @type {!Node} */
this.currentNode = root;

/** @private {number} */
this.index_ = 0;

/** @private @const {!Array<!Element>} */
this.nodeList_ = useQuerySelector
? toArray(root.querySelectorAll('[i-amphtml-binding]'))
: [];

// Confusingly, the old TreeWalker hit the root node. We need to match that behavior.
if (
useQuerySelector &&
root.nodeType === Node.ELEMENT_NODE &&
root.hasAttribute('i-amphtml-binding')
) {
this.nodeList_.unshift(root);
}

/**
* Third and fourth params of `createTreeWalker` are not optional on IE11.
* @private @const {?TreeWalker}
*/
this.treeWalker_ = useQuerySelector
? null
: doc.createTreeWalker(
root,
NodeFilter.SHOW_ELEMENT,
null,
/* entityReferenceExpansion */ false
);
}

/**
* Finds the next node in document order, if it exists. Returns that node, or null if it doesn't exist.
* Updates currentNode, if it exists, else currentNode stays the same.
*
* @return {?Node}
*/
nextNode() {
if (this.useQuerySelector_) {
if (this.index_ == this.nodeList_.length) {
return null;
}
const next = this.nodeList_[this.index_++];
this.currentNode = next;
return next;
}

const walker = this.treeWalker_;
const next = walker.nextNode();
// This matches the TreeWalker's behavior.
if (next !== null) {
this.currentNode = next;
}
return next;
}

/**
* Skips the remaining sibling nodes in the current parent. Returns the next node in document order.
* @return {?Node}
*/
skipSubtree() {
if (this.useQuerySelector_) {
const {currentNode} = this;
let next = null;
do {
next = this.nextNode();
} while (next !== null && currentNode.contains(next));
return next;
}

const walker = this.treeWalker_;
for (let n = walker.currentNode; n; n = walker.parentNode()) {
const sibling = walker.nextSibling();
if (sibling !== null) {
this.currentNode = sibling;
return sibling;
}
}
return null;
}
}

0 comments on commit b993c3f

Please sign in to comment.