Pārlūkot izejas kodu

fix(hmr): fix hmr error for hoisted children array in v-for

fix #6978
close #7114
Evan You 2 gadi atpakaļ
vecāks
revīzija
733437691f

+ 12 - 0
packages/compiler-core/__tests__/transforms/hoistStatic.spec.ts

@@ -593,5 +593,17 @@ describe('compiler: hoistStatic transform', () => {
       expect(root.hoists.length).toBe(2)
       expect(generate(root).code).toMatchSnapshot()
     })
+
+    test('clone hoisted array children in HMR mode', () => {
+      const root = transformWithHoist(`<div><span class="hi"></span></div>`, {
+        hmr: true
+      })
+      expect(root.hoists.length).toBe(2)
+      expect(root.codegenNode).toMatchObject({
+        children: {
+          content: '[..._hoisted_2]'
+        }
+      })
+    })
   })
 })

+ 7 - 0
packages/compiler-core/src/options.ts

@@ -256,6 +256,13 @@ export interface TransformOptions
    * needed to render inline CSS variables on component root
    */
   ssrCssVars?: string
+  /**
+   * Whether to compile the template assuming it needs to handle HMR.
+   * Some edge cases may need to generate different code for HMR to work
+   * correctly, e.g. #6938, #7138
+   * @internal
+   */
+  hmr?: boolean
 }
 
 export interface CodegenOptions extends SharedTransformCodegenOptions {

+ 2 - 0
packages/compiler-core/src/transform.ts

@@ -129,6 +129,7 @@ export function createTransformContext(
     filename = '',
     prefixIdentifiers = false,
     hoistStatic = false,
+    hmr = false,
     cacheHandlers = false,
     nodeTransforms = [],
     directiveTransforms = {},
@@ -155,6 +156,7 @@ export function createTransformContext(
     selfName: nameMatch && capitalize(camelize(nameMatch[1])),
     prefixIdentifiers,
     hoistStatic,
+    hmr,
     cacheHandlers,
     nodeTransforms,
     directiveTransforms,

+ 8 - 1
packages/compiler-core/src/transforms/hoistStatic.ts

@@ -140,9 +140,16 @@ function walk(
     node.codegenNode.type === NodeTypes.VNODE_CALL &&
     isArray(node.codegenNode.children)
   ) {
-    node.codegenNode.children = context.hoist(
+    const hoisted = context.hoist(
       createArrayExpression(node.codegenNode.children)
     )
+    // #6978, #7138, #7114
+    // a hoisted children array inside v-for can caused HMR errors since
+    // it might be mutated when mounting the v-for list
+    if (context.hmr) {
+      hoisted.content = `[...${hoisted.content}]`
+    }
+    node.codegenNode.children = hoisted
   }
 }
 

+ 1 - 0
packages/compiler-sfc/src/compileTemplate.ts

@@ -212,6 +212,7 @@ function doCompileTemplate({
     slotted,
     sourceMap: true,
     ...compilerOptions,
+    hmr: !isProd,
     nodeTransforms: nodeTransforms.concat(compilerOptions.nodeTransforms || []),
     filename,
     onError: e => errors.push(e),

+ 37 - 1
packages/runtime-core/__tests__/hmr.spec.ts

@@ -20,7 +20,7 @@ const { createRecord, rerender, reload } = __VUE_HMR_RUNTIME__
 registerRuntimeCompiler(compileToFunction)
 
 function compileToFunction(template: string) {
-  const { code } = baseCompile(template)
+  const { code } = baseCompile(template, { hoistStatic: true, hmr: true })
   const render = new Function('Vue', code)(
     runtimeTest
   ) as InternalRenderFunction
@@ -567,4 +567,40 @@ describe('hot module replacement', () => {
     rerender(parentId, compileToFunction(`<Child>2</Child>`))
     expect(serializeInner(root)).toBe(`2`)
   })
+
+  // #6978, #7138, #7114
+  test('hoisted children array inside v-for', () => {
+    const root = nodeOps.createElement('div')
+    const appId = 'test-app-id'
+    const App: ComponentOptions = {
+      __hmrId: appId,
+      render: compileToFunction(
+        `<div v-for="item of 2">
+          <div>1</div>
+        </div>
+        <p>2</p>
+        <p>3</p>`
+      )
+    }
+    createRecord(appId, App)
+
+    render(h(App), root)
+    expect(serializeInner(root)).toBe(
+      `<div><div>1</div></div><div><div>1</div></div><p>2</p><p>3</p>`
+    )
+
+    // move the <p>3</p> into the <div>1</div>
+    rerender(
+      appId,
+      compileToFunction(
+        `<div v-for="item of 2">
+          <div>1<p>3</p></div>
+        </div>
+        <p>2</p>`
+      )
+    )
+    expect(serializeInner(root)).toBe(
+      `<div><div>1<p>3</p></div></div><div><div>1<p>3</p></div></div><p>2</p>`
+    )
+  })
 })