Răsfoiți Sursa

refined async hydration + tests

Evan You 9 ani în urmă
părinte
comite
b5e4a22a8d
2 a modificat fișierele cu 131 adăugiri și 19 ștergeri
  1. 17 7
      src/core/vdom/patch.js
  2. 114 12
      test/unit/modules/vdom/patch/hydration.spec.js

+ 17 - 7
src/core/vdom/patch.js

@@ -35,13 +35,14 @@ function sameVnode (a, b) {
   return (
     a.key === b.key && (
       (
-        a.isAsyncPlaceholder === true &&
-        a.asyncFactory === b.asyncFactory
-      ) || (
         a.tag === b.tag &&
         a.isComment === b.isComment &&
         isDef(a.data) === isDef(b.data) &&
         sameInputType(a, b)
+      ) || (
+        isTrue(a.isAsyncPlaceholder) &&
+        a.asyncFactory === b.asyncFactory &&
+        isUndef(b.asyncFactory.error)
       )
     )
   )
@@ -440,9 +441,18 @@ export function createPatchFunction (backend) {
     if (oldVnode === vnode) {
       return
     }
-    if (oldVnode.isAsyncPlaceholder) {
-      return hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
+
+    const elm = vnode.elm = oldVnode.elm
+
+    if (isTrue(oldVnode.isAsyncPlaceholder)) {
+      if (isDef(vnode.asyncFactory.resolved)) {
+        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
+      } else {
+        vnode.isAsyncPlaceholder = true
+      }
+      return
     }
+
     // reuse element for static trees.
     // note we only do this if the vnode is cloned -
     // if the new node is not cloned it means the render functions have been
@@ -452,16 +462,16 @@ export function createPatchFunction (backend) {
       vnode.key === oldVnode.key &&
       (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
     ) {
-      vnode.elm = oldVnode.elm
       vnode.componentInstance = oldVnode.componentInstance
       return
     }
+
     let i
     const data = vnode.data
     if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
       i(oldVnode, vnode)
     }
-    const elm = vnode.elm = oldVnode.elm
+
     const oldCh = oldVnode.children
     const ch = vnode.children
     if (isDef(data) && isPatchable(vnode)) {

+ 114 - 12
test/unit/modules/vdom/patch/hydration.spec.js

@@ -3,6 +3,13 @@ import VNode from 'core/vdom/vnode'
 import { patch } from 'web/runtime/patch'
 import { SSR_ATTR } from 'shared/constants'
 
+function createMockSSRDOM (innerHTML) {
+  const dom = document.createElement('div')
+  dom.setAttribute(SSR_ATTR, 'true')
+  dom.innerHTML = innerHTML
+  return dom
+}
+
 describe('vdom patch: hydration', () => {
   let vnode0
   beforeEach(() => {
@@ -89,9 +96,7 @@ describe('vdom patch: hydration', () => {
 
   // component hydration is better off with a more e2e approach
   it('should hydrate components when server-rendered DOM tree is same as virtual DOM tree', done => {
-    const dom = document.createElement('div')
-    dom.setAttribute(SSR_ATTR, 'true')
-    dom.innerHTML = '<span>foo</span><div class="b a"><span>foo qux</span></div><!---->'
+    const dom = createMockSSRDOM('<span>foo</span><div class="b a"><span>foo qux</span></div><!---->')
     const originalNode1 = dom.children[0]
     const originalNode2 = dom.children[1]
 
@@ -131,9 +136,7 @@ describe('vdom patch: hydration', () => {
   })
 
   it('should warn failed hydration for non-matching DOM in child component', () => {
-    const dom = document.createElement('div')
-    dom.setAttribute(SSR_ATTR, 'true')
-    dom.innerHTML = '<div><span></span></div>'
+    const dom = createMockSSRDOM('<div><span></span></div>')
 
     new Vue({
       template: '<div><test></test></div>',
@@ -148,9 +151,7 @@ describe('vdom patch: hydration', () => {
   })
 
   it('should overwrite textNodes in the correct position but with mismatching text without warning', () => {
-    const dom = document.createElement('div')
-    dom.setAttribute(SSR_ATTR, 'true')
-    dom.innerHTML = '<div><span>foo</span></div>'
+    const dom = createMockSSRDOM('<div><span>foo</span></div>')
 
     new Vue({
       template: '<div><test></test></div>',
@@ -169,9 +170,7 @@ describe('vdom patch: hydration', () => {
   })
 
   it('should pick up elements with no children and populate without warning', done => {
-    const dom = document.createElement('div')
-    dom.setAttribute(SSR_ATTR, 'true')
-    dom.innerHTML = '<div><span></span></div>'
+    const dom = createMockSSRDOM('<div><span></span></div>')
     const span = dom.querySelector('span')
 
     const vm = new Vue({
@@ -195,4 +194,107 @@ describe('vdom patch: hydration', () => {
       expect(vm.$el.innerHTML).toBe('<div><span>foo</span></div>')
     }).then(done)
   })
+
+  it('should hydrate async component', done => {
+    const dom = createMockSSRDOM('<span>foo</span>')
+    const span = dom.querySelector('span')
+
+    const Foo = resolve => setTimeout(() => {
+      resolve({
+        data: () => ({ msg: 'foo' }),
+        template: `<span>{{ msg }}</span>`
+      })
+    }, 0)
+
+    const vm = new Vue({
+      template: '<div><foo ref="foo" /></div>',
+      components: { Foo }
+    }).$mount(dom)
+
+    expect('not matching server-rendered content').not.toHaveBeenWarned()
+    expect(dom.innerHTML).toBe('<span>foo</span>')
+    expect(vm.$refs.foo).toBeUndefined()
+
+    setTimeout(() => {
+      expect(dom.innerHTML).toBe('<span>foo</span>')
+      expect(vm.$refs.foo).not.toBeUndefined()
+      vm.$refs.foo.msg = 'bar'
+      waitForUpdate(() => {
+        expect(dom.innerHTML).toBe('<span>bar</span>')
+        expect(dom.querySelector('span')).toBe(span)
+      }).then(done)
+    }, 0)
+  })
+
+  it('should hydrate async component without showing loading', done => {
+    const dom = createMockSSRDOM('<span>foo</span>')
+    const span = dom.querySelector('span')
+
+    const Foo = () => ({
+      component: new Promise(resolve => {
+        setTimeout(() => {
+          resolve({
+            data: () => ({ msg: 'foo' }),
+            template: `<span>{{ msg }}</span>`
+          })
+        }, 10)
+      }),
+      delay: 1,
+      loading: {
+        render: h => h('span', 'loading')
+      }
+    })
+
+    const vm = new Vue({
+      template: '<div><foo ref="foo" /></div>',
+      components: { Foo }
+    }).$mount(dom)
+
+    expect('not matching server-rendered content').not.toHaveBeenWarned()
+    expect(dom.innerHTML).toBe('<span>foo</span>')
+    expect(vm.$refs.foo).toBeUndefined()
+
+    setTimeout(() => {
+      expect(dom.innerHTML).toBe('<span>foo</span>')
+    }, 1)
+
+    setTimeout(() => {
+      expect(dom.innerHTML).toBe('<span>foo</span>')
+      expect(vm.$refs.foo).not.toBeUndefined()
+      vm.$refs.foo.msg = 'bar'
+      waitForUpdate(() => {
+        expect(dom.innerHTML).toBe('<span>bar</span>')
+        expect(dom.querySelector('span')).toBe(span)
+      }).then(done)
+    }, 10)
+  })
+
+  it('should hydrate async component by replacing DOM if error occurs', done => {
+    const dom = createMockSSRDOM('<span>foo</span>')
+
+    const Foo = () => ({
+      component: new Promise((resolve, reject) => {
+        setTimeout(() => {
+          reject('something went wrong')
+        }, 10)
+      }),
+      error: {
+        render: h => h('span', 'error')
+      }
+    })
+
+    new Vue({
+      template: '<div><foo ref="foo" /></div>',
+      components: { Foo }
+    }).$mount(dom)
+
+    expect('not matching server-rendered content').not.toHaveBeenWarned()
+    expect(dom.innerHTML).toBe('<span>foo</span>')
+
+    setTimeout(() => {
+      expect('Failed to resolve async').toHaveBeenWarned()
+      expect(dom.innerHTML).toBe('<span>error</span>')
+      done()
+    }, 10)
+  })
 })