Repository: vuejs/vue
Pull request: fix(patch): clone insert hooks to avoid being mutated during iteration

<!--
Please make sure to read the Pull Request Guidelines:
https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#pull-request-guidelines
-->

<!-- PULL REQUEST TEMPLATE -->
<!-- (Update "[ ]" to "[x]" to check a box) -->

**What kind of change does this PR introduce?** (check at least one)

- [x] Bugfix
- [ ] Feature
- [ ] Code style update
- [ ] Refactor
- [ ] Build-related changes
- [ ] Other, please describe:

**Does this PR introduce a breaking change?** (check one)

- [ ] Yes
- [x] No

If yes, please describe the impact and migration path for existing applications:

**The PR fulfills these requirements:**

- [x] It's submitted to the `main` branch for v2.x (or to a previous version branch)
- [ ] When resolving a specific issue, it's referenced in the PR's title (e.g. `fix #xxx[,#xxx]`, where "xxx" is the issue number)
- [x] All tests are passing: https://github.com/vuejs/vue/blob/dev/.github/CONTRIBUTING.md#development-setup
- [x] New/updated tests are included

If adding a **new feature**, the PR's description includes:
- [ ] A convincing reason for adding this feature (to avoid wasting your time, it's best to open a suggestion issue first and wait for approval before working on it)

**Other information:**
repro: https://jsbin.com/zobarureye/edit?html,js,console,output

There is a component called `v-switch` that is placed inside a `transition-group` element. The `v-switch` component has a directive called `v-foo`, accepts a boolean prop called `isOn` and switches its template root according to the value of `isOn`. The `v-foo` directive has an inserted hook that should be called to ensure that the binding logic is refreshed whenever the template root changes.


Changed files:
filename: src/core/vdom/patch.ts
patch: @@ -878,8 +878,11 @@ export function createPatchFunction(backend) {
               const insert = ancestor.data.hook.insert
               if (insert.merged) {
                 // start at index 1 to avoid re-invoking component mounted hook
-                for (let i = 1; i < insert.fns.length; i++) {
-                  insert.fns[i]()
+                // clone insert hooks to avoid being mutated during iteration.
+                // e.g. for customed directives under transition group.
+                const cloned = insert.fns.slice(1)
+                for (let i = 0; i < cloned.length; i++) {
+                  cloned[i]()
                 }
               }
             } else {

filename: test/unit/modules/vdom/patch/edge-cases.spec.ts
patch: @@ -467,4 +467,42 @@ describe('vdom patch: edge cases', () => {
     vm.visible = false
     vm.$nextTick(done)
   })
+
+  it('should call directive\'s inserted hook correctly when template root is changed', done => {
+    let insertCalled = false
+    const vm = new Vue({
+      data: {
+        isOn: false
+      },
+      components: {
+        'v-switch': {
+          props: {
+            on: Boolean
+          },
+          template: `
+            <div v-if="on" key="on">On</div>
+            <div v-else key="off">Off</div>`
+        }
+      },
+      directives: {
+        foo: {
+          inserted() {
+            insertCalled = true
+          }
+        }
+      },
+      template: `
+        <transition-group>
+          <v-switch key="swicth" v-foo :on="isOn"/>
+        </transition-group>
+      `
+    }).$mount()
+
+    vm.isOn = true
+    insertCalled = false
+    waitForUpdate(() => {
+      expect(vm.$el.textContent).toMatch('On')
+      expect(insertCalled).toBe(true)
+    }).then(done)
+  })
 })