瀏覽代碼

Adjust mutation observer to be faster and still account for moving and wrapping nodes

Caleb Porzio 5 月之前
父節點
當前提交
edd3adfdf0
共有 2 個文件被更改,包括 37 次插入31 次删除
  1. 12 1
      packages/alpinejs/src/lifecycle.js
  2. 25 30
      packages/alpinejs/src/mutation.js

+ 12 - 1
packages/alpinejs/src/lifecycle.js

@@ -82,12 +82,22 @@ let initInterceptors = []
 
 export function interceptInit(callback) { initInterceptors.push(callback) }
 
+let markerDispenser = 1
+
 export function initTree(el, walker = walk, intercept = () => {}) {
-    // Don't init a tree within a parent that is being ignored.
+    // Don't init a tree within a parent that is being ignored...
     if (findClosest(el, i => i._x_ignore)) return
 
     deferHandlingDirectives(() => {
         walker(el, (el, skip) => {
+            // If the element has a marker, it's already been initialized...
+            if (el._x_marker) return
+
+            // Add a marker to the element so we can tell if it's been initialized...
+            // This is important so that we can prevent double-initialization of
+            // elements that are moved around on the page.
+            el._x_marker = markerDispenser++
+
             intercept(el, skip)
 
             initInterceptors.forEach(i => i(el, skip))
@@ -103,6 +113,7 @@ export function destroyTree(root, walker = walk) {
     walker(root, el => {
         cleanupElement(el)
         cleanupAttributes(el)
+        delete el._x_marker
     })
 }
 

+ 25 - 30
packages/alpinejs/src/mutation.js

@@ -118,8 +118,8 @@ function onMutate(mutations) {
         return
     }
 
-    let addedNodes = new Set
-    let removedNodes = new Set
+    let addedNodes = []
+    let removedNodes = []
     let addedAttributes = new Map
     let removedAttributes = new Map
 
@@ -127,8 +127,19 @@ function onMutate(mutations) {
         if (mutations[i].target._x_ignoreMutationObserver) continue
 
         if (mutations[i].type === 'childList') {
-            mutations[i].addedNodes.forEach(node => node.nodeType === 1 && addedNodes.add(node))
-            mutations[i].removedNodes.forEach(node => node.nodeType === 1 && removedNodes.add(node))
+            mutations[i].removedNodes.forEach(node => {
+                if (node.nodeType !== 1) return
+                if (! node._x_marker) return
+
+                removedNodes.push(node)
+            })
+
+            mutations[i].addedNodes.forEach(node => {
+                if (node.nodeType !== 1) return
+                if (node._x_marker) return
+
+                addedNodes.push(node)
+            })
         }
 
         if (mutations[i].type === 'attributes') {
@@ -170,42 +181,26 @@ function onMutate(mutations) {
         onAttributeAddeds.forEach(i => i(el, attrs))
     })
 
+    // There are two special scenarios we need to account for when using the mutation
+    // observer to init and destroy elements. First, when a node is "moved" on the page,
+    // it's registered as both an "add" and a "remove", so we want to skip those.
+    // (This is handled above by the ._x_marker conditionals...)
+    // Second, when a node is "wrapped", it gets registered as a "removal" and the wrapper
+    // as an "addition". We don't want to remove, then re-initialize the node, so we look
+    // and see if it's inside any added nodes (wrappers) and skip it.
+    // (This is handled below by the .contains conditional...)
+
     for (let node of removedNodes) {
-        // If an element gets moved on a page, it's registered
-        // as both an "add" and "remove", so we want to skip those.
-        if (addedNodes.has(node)) continue
+        if (addedNodes.some(i => i.contains(node))) continue
 
         onElRemoveds.forEach(i => i(node))
     }
 
-    // Mutations are bundled together by the browser but sometimes
-    // for complex cases, there may be javascript code adding a wrapper
-    // and then an alpine component as a child of that wrapper in the same
-    // function and the mutation observer will receive 2 different mutations.
-    // when it comes time to run them, the dom contains both changes so the child
-    // element would be processed twice as Alpine calls initTree on
-    // both mutations. We mark all nodes as _x_ignored and only remove the flag
-    // when processing the node to avoid those duplicates.
-    addedNodes.forEach((node) => {
-        node._x_ignoreSelf = true
-        node._x_ignore = true
-    })
     for (let node of addedNodes) {
-        // If the node was eventually removed as part of one of his
-        // parent mutations, skip it
-        if (removedNodes.has(node)) continue
         if (! node.isConnected) continue
 
-        delete node._x_ignoreSelf
-        delete node._x_ignore
         onElAddeds.forEach(i => i(node))
-        node._x_ignore = true
-        node._x_ignoreSelf = true
     }
-    addedNodes.forEach((node) => {
-        delete node._x_ignoreSelf
-        delete node._x_ignore
-    })
 
     addedNodes = null
     removedNodes = null