Răsfoiți Sursa

use comment node as empty placeholder (fix <transition> SSR hydration)

Evan You 10 ani în urmă
părinte
comite
351aef3cb4

+ 15 - 8
src/core/vdom/patch.js

@@ -30,6 +30,7 @@ function sameVnode (vnode1, vnode2) {
   return (
     vnode1.key === vnode2.key &&
     vnode1.tag === vnode2.tag &&
+    vnode1.isComment === vnode2.isComment &&
     !vnode1.data === !vnode2.data
   )
 }
@@ -87,12 +88,7 @@ export function createPatchFunction (backend) {
       // component also has set the placeholder vnode's elm.
       // in that case we can just return the element and be done.
       if (isDef(i = vnode.child)) {
-        if (vnode.data.pendingInsert) {
-          insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
-        }
-        vnode.elm = vnode.child.$el
-        invokeCreateHooks(vnode, insertedVnodeQueue)
-        setScope(vnode)
+        initComponent(vnode, insertedVnodeQueue)
         return vnode.elm
       }
     }
@@ -127,6 +123,8 @@ export function createPatchFunction (backend) {
       if (isDef(data)) {
         invokeCreateHooks(vnode, insertedVnodeQueue)
       }
+    } else if (vnode.isComment) {
+      elm = vnode.elm = nodeOps.createComment(vnode.text)
     } else {
       elm = vnode.elm = nodeOps.createTextNode(vnode.text)
     }
@@ -144,6 +142,15 @@ export function createPatchFunction (backend) {
     }
   }
 
+  function initComponent (vnode, insertedVnodeQueue) {
+    if (vnode.data.pendingInsert) {
+      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
+    }
+    vnode.elm = vnode.child.$el
+    invokeCreateHooks(vnode, insertedVnodeQueue)
+    setScope(vnode)
+  }
+
   // set scope id attribute for scoped CSS.
   // this is implemented as a special case to avoid the overhead
   // of going through the normal attribute patching process.
@@ -360,7 +367,7 @@ export function createPatchFunction (backend) {
       if (isDef(i = data.hook) && isDef(i = i.init)) i(vnode, true /* hydrating */)
       if (isDef(i = vnode.child)) {
         // child component. it should have hydrated its own tree.
-        invokeCreateHooks(vnode, insertedVnodeQueue)
+        initComponent(vnode, insertedVnodeQueue)
         return true
       }
     }
@@ -425,7 +432,7 @@ export function createPatchFunction (backend) {
           // mounting to a real element
           // check if this is server-rendered content and if we can perform
           // a successful hydration.
-          if (oldVnode.hasAttribute('server-rendered')) {
+          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute('server-rendered')) {
             oldVnode.removeAttribute('server-rendered')
             hydrating = true
           }

+ 8 - 1
src/core/vdom/vnode.js

@@ -16,6 +16,7 @@ export default class VNode {
   raw: ?boolean; // contains raw HTML
   isStatic: ?boolean; // hoisted static node
   isRootInsert: boolean; // necessary for enter transition check
+  isComment: boolean;
 
   constructor (
     tag?: string,
@@ -43,6 +44,7 @@ export default class VNode {
     this.raw = false
     this.isStatic = false
     this.isRootInsert = true
+    this.isComment = false
     // apply construct hook.
     // this is applied during render, before patch happens.
     // unlike other hooks, this is applied on both client and server.
@@ -53,4 +55,9 @@ export default class VNode {
   }
 }
 
-export const emptyVNode = () => new VNode(undefined, undefined, undefined, '')
+export const emptyVNode = () => {
+  const node = new VNode()
+  node.text = ''
+  node.isComment = true
+  return node
+}

+ 4 - 0
src/platforms/web/runtime/node-ops.js

@@ -14,6 +14,10 @@ export function createTextNode (text: string): Text {
   return document.createTextNode(text)
 }
 
+export function createComment (text: string): Comment {
+  return document.createComment(text)
+}
+
 export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
   parentNode.insertBefore(newNode, referenceNode)
 }

+ 2 - 0
src/server/render.js

@@ -83,6 +83,8 @@ export function createRenderFunction (
     } else {
       if (node.tag) {
         renderElement(node, write, next, isRoot)
+      } else if (node.isComment) {
+        write(`<!--${node.text}-->`, next)
       } else {
         write(node.raw ? node.text : encodeHTML(String(node.text)), next)
       }

+ 9 - 0
test/ssr/ssr-string.spec.js

@@ -500,6 +500,15 @@ describe('SSR: renderToString', () => {
     })
   })
 
+  it('comment nodes', done => {
+    renderVmWithOptions({
+      template: '<div><transition><div v-if="false"></test></transition></div>'
+    }, result => {
+      expect(result).toContain(`<div server-rendered="true"><!----></div>`)
+      done()
+    })
+  })
+
   it('should catch error', done => {
     renderToString(new Vue({
       render () {

+ 1 - 1
test/unit/features/component/component-async.spec.js

@@ -40,7 +40,7 @@ describe('Component async', () => {
         }
       }
     }).$mount()
-    expect(vm.$el.nodeType).toBe(3)
+    expect(vm.$el.nodeType).toBe(8)
     expect(vm.$children.length).toBe(0)
     function next () {
       expect(vm.$el.nodeType).toBe(1)

+ 6 - 6
test/unit/features/component/component-keep-alive.spec.js

@@ -105,16 +105,16 @@ describe('Component keep-alive', () => {
       vm.view = 'two'
       waitForUpdate(() => {
         expect(vm.$el.innerHTML).toBe(
-          '<div class="test test-leave test-leave-active">one</div>'
+          '<div class="test test-leave test-leave-active">one</div><!---->'
         )
         assertHookCalls(one, [1, 1, 1, 1, 0])
         assertHookCalls(two, [0, 0, 0, 0, 0])
       }).thenWaitFor(nextFrame).then(() => {
         expect(vm.$el.innerHTML).toBe(
-          '<div class="test test-leave-active">one</div>'
+          '<div class="test test-leave-active">one</div><!---->'
         )
       }).thenWaitFor(_next => { next = _next }).then(() => {
-        expect(vm.$el.innerHTML).toBe('')
+        expect(vm.$el.innerHTML).toBe('<!---->')
       }).thenWaitFor(nextFrame).then(() => {
         expect(vm.$el.innerHTML).toBe(
           '<div class="test test-enter test-enter-active">two</div>'
@@ -135,16 +135,16 @@ describe('Component keep-alive', () => {
         vm.view = 'one'
       }).then(() => {
         expect(vm.$el.innerHTML).toBe(
-          '<div class="test test-leave test-leave-active">two</div>'
+          '<div class="test test-leave test-leave-active">two</div><!---->'
         )
         assertHookCalls(one, [1, 1, 1, 1, 0])
         assertHookCalls(two, [1, 1, 1, 1, 0])
       }).thenWaitFor(nextFrame).then(() => {
         expect(vm.$el.innerHTML).toBe(
-          '<div class="test test-leave-active">two</div>'
+          '<div class="test test-leave-active">two</div><!---->'
         )
       }).thenWaitFor(_next => { next = _next }).then(() => {
-        expect(vm.$el.innerHTML).toBe('')
+        expect(vm.$el.innerHTML).toBe('<!---->')
       }).thenWaitFor(nextFrame).then(() => {
         expect(vm.$el.innerHTML).toBe(
           '<div class="test test-enter test-enter-active">one</div>'

+ 1 - 1
test/unit/features/component/component.spec.js

@@ -107,7 +107,7 @@ describe('Component', () => {
       vm.view = ''
     })
     .then(() => {
-      expect(vm.$el.nodeType).toBe(3)
+      expect(vm.$el.nodeType).toBe(8)
       expect(vm.$el.data).toBe('')
     }).then(done)
   })

+ 6 - 6
test/unit/features/transition/transition-mode.spec.js

@@ -68,14 +68,14 @@ if (!isIE9) {
       vm.view = 'two'
       waitForUpdate(() => {
         expect(vm.$el.innerHTML).toBe(
-          '<div class="test test-leave test-leave-active">one</div>'
+          '<div class="test test-leave test-leave-active">one</div><!---->'
         )
       }).thenWaitFor(nextFrame).then(() => {
         expect(vm.$el.innerHTML).toBe(
-          '<div class="test test-leave-active">one</div>'
+          '<div class="test test-leave-active">one</div><!---->'
         )
       }).thenWaitFor(_next => { next = _next }).then(() => {
-        expect(vm.$el.innerHTML).toBe('')
+        expect(vm.$el.innerHTML).toBe('<!---->')
       }).thenWaitFor(nextFrame).then(() => {
         expect(vm.$el.innerHTML).toBe(
           '<div class="test test-enter test-enter-active">two</div>'
@@ -257,14 +257,14 @@ if (!isIE9) {
       vm.view = 'two'
       waitForUpdate(() => {
         expect(vm.$el.innerHTML).toBe(
-          '<div class="test test-leave test-leave-active">one</div>'
+          '<div class="test test-leave test-leave-active">one</div><!---->'
         )
       }).thenWaitFor(nextFrame).then(() => {
         expect(vm.$el.innerHTML).toBe(
-          '<div class="test test-leave-active">one</div>'
+          '<div class="test test-leave-active">one</div><!---->'
         )
       }).thenWaitFor(_next => { next = _next }).then(() => {
-        expect(vm.$el.innerHTML).toBe('')
+        expect(vm.$el.innerHTML).toBe('<!---->')
       }).thenWaitFor(nextFrame).then(() => {
         expect(vm.$el.innerHTML).toBe(
           '<div class="test test-enter test-enter-active">two</div>'

+ 5 - 5
test/unit/features/transition/transition.spec.js

@@ -316,7 +316,7 @@ if (!isIE9) {
       vm.ok = false
       waitForUpdate(() => {
         expect(leaveSpy).toHaveBeenCalled()
-        expect(vm.$el.innerHTML).toBe('')
+        expect(vm.$el.innerHTML).toBe('<!---->')
         vm.ok = true
       }).then(() => {
         expect(enterSpy).toHaveBeenCalled()
@@ -339,9 +339,9 @@ if (!isIE9) {
       vm.ok = false
       waitForUpdate(() => {
         expect(leaveSpy).toHaveBeenCalled()
-        expect(vm.$el.innerHTML).toBe('<div class="nope-leave nope-leave-active">foo</div>')
+        expect(vm.$el.innerHTML).toBe('<div class="nope-leave nope-leave-active">foo</div><!---->')
       }).thenWaitFor(nextFrame).then(() => {
-        expect(vm.$el.innerHTML).toBe('')
+        expect(vm.$el.innerHTML).toBe('<!---->')
         vm.ok = true
       }).then(() => {
         expect(enterSpy).toHaveBeenCalled()
@@ -367,7 +367,7 @@ if (!isIE9) {
         }
       }).$mount(el)
 
-      expect(vm.$el.innerHTML).toBe('')
+      expect(vm.$el.innerHTML).toBe('<!---->')
       vm.ok = true
       waitForUpdate(() => {
         expect(vm.$el.children[0].className).toBe('test test-enter test-enter-active')
@@ -652,7 +652,7 @@ if (!isIE9) {
         expect(vm.$el.childNodes[0].getAttribute('class')).toBe('test v-leave-active')
       }).thenWaitFor(duration + 10).then(() => {
         expect(vm.$el.childNodes.length).toBe(1)
-        expect(vm.$el.childNodes[0].nodeType).toBe(3) // should be an empty text node
+        expect(vm.$el.childNodes[0].nodeType).toBe(8) // should be an empty comment node
         expect(vm.$el.childNodes[0].textContent).toBe('')
         vm.ok = true
       }).then(() => {