Skip to content

Commit b5e4a22

Browse files
committed
refined async hydration + tests
1 parent 8262edc commit b5e4a22

File tree

2 files changed

+131
-19
lines changed

2 files changed

+131
-19
lines changed

Diff for: src/core/vdom/patch.js

+17-7
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,14 @@ function sameVnode (a, b) {
3535
return (
3636
a.key === b.key && (
3737
(
38-
a.isAsyncPlaceholder === true &&
39-
a.asyncFactory === b.asyncFactory
40-
) || (
4138
a.tag === b.tag &&
4239
a.isComment === b.isComment &&
4340
isDef(a.data) === isDef(b.data) &&
4441
sameInputType(a, b)
42+
) || (
43+
isTrue(a.isAsyncPlaceholder) &&
44+
a.asyncFactory === b.asyncFactory &&
45+
isUndef(b.asyncFactory.error)
4546
)
4647
)
4748
)
@@ -440,9 +441,18 @@ export function createPatchFunction (backend) {
440441
if (oldVnode === vnode) {
441442
return
442443
}
443-
if (oldVnode.isAsyncPlaceholder) {
444-
return hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
444+
445+
const elm = vnode.elm = oldVnode.elm
446+
447+
if (isTrue(oldVnode.isAsyncPlaceholder)) {
448+
if (isDef(vnode.asyncFactory.resolved)) {
449+
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
450+
} else {
451+
vnode.isAsyncPlaceholder = true
452+
}
453+
return
445454
}
455+
446456
// reuse element for static trees.
447457
// note we only do this if the vnode is cloned -
448458
// if the new node is not cloned it means the render functions have been
@@ -452,16 +462,16 @@ export function createPatchFunction (backend) {
452462
vnode.key === oldVnode.key &&
453463
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
454464
) {
455-
vnode.elm = oldVnode.elm
456465
vnode.componentInstance = oldVnode.componentInstance
457466
return
458467
}
468+
459469
let i
460470
const data = vnode.data
461471
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
462472
i(oldVnode, vnode)
463473
}
464-
const elm = vnode.elm = oldVnode.elm
474+
465475
const oldCh = oldVnode.children
466476
const ch = vnode.children
467477
if (isDef(data) && isPatchable(vnode)) {

Diff for: test/unit/modules/vdom/patch/hydration.spec.js

+114-12
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@ import VNode from 'core/vdom/vnode'
33
import { patch } from 'web/runtime/patch'
44
import { SSR_ATTR } from 'shared/constants'
55

6+
function createMockSSRDOM (innerHTML) {
7+
const dom = document.createElement('div')
8+
dom.setAttribute(SSR_ATTR, 'true')
9+
dom.innerHTML = innerHTML
10+
return dom
11+
}
12+
613
describe('vdom patch: hydration', () => {
714
let vnode0
815
beforeEach(() => {
@@ -89,9 +96,7 @@ describe('vdom patch: hydration', () => {
8996

9097
// component hydration is better off with a more e2e approach
9198
it('should hydrate components when server-rendered DOM tree is same as virtual DOM tree', done => {
92-
const dom = document.createElement('div')
93-
dom.setAttribute(SSR_ATTR, 'true')
94-
dom.innerHTML = '<span>foo</span><div class="b a"><span>foo qux</span></div><!---->'
99+
const dom = createMockSSRDOM('<span>foo</span><div class="b a"><span>foo qux</span></div><!---->')
95100
const originalNode1 = dom.children[0]
96101
const originalNode2 = dom.children[1]
97102

@@ -131,9 +136,7 @@ describe('vdom patch: hydration', () => {
131136
})
132137

133138
it('should warn failed hydration for non-matching DOM in child component', () => {
134-
const dom = document.createElement('div')
135-
dom.setAttribute(SSR_ATTR, 'true')
136-
dom.innerHTML = '<div><span></span></div>'
139+
const dom = createMockSSRDOM('<div><span></span></div>')
137140

138141
new Vue({
139142
template: '<div><test></test></div>',
@@ -148,9 +151,7 @@ describe('vdom patch: hydration', () => {
148151
})
149152

150153
it('should overwrite textNodes in the correct position but with mismatching text without warning', () => {
151-
const dom = document.createElement('div')
152-
dom.setAttribute(SSR_ATTR, 'true')
153-
dom.innerHTML = '<div><span>foo</span></div>'
154+
const dom = createMockSSRDOM('<div><span>foo</span></div>')
154155

155156
new Vue({
156157
template: '<div><test></test></div>',
@@ -169,9 +170,7 @@ describe('vdom patch: hydration', () => {
169170
})
170171

171172
it('should pick up elements with no children and populate without warning', done => {
172-
const dom = document.createElement('div')
173-
dom.setAttribute(SSR_ATTR, 'true')
174-
dom.innerHTML = '<div><span></span></div>'
173+
const dom = createMockSSRDOM('<div><span></span></div>')
175174
const span = dom.querySelector('span')
176175

177176
const vm = new Vue({
@@ -195,4 +194,107 @@ describe('vdom patch: hydration', () => {
195194
expect(vm.$el.innerHTML).toBe('<div><span>foo</span></div>')
196195
}).then(done)
197196
})
197+
198+
it('should hydrate async component', done => {
199+
const dom = createMockSSRDOM('<span>foo</span>')
200+
const span = dom.querySelector('span')
201+
202+
const Foo = resolve => setTimeout(() => {
203+
resolve({
204+
data: () => ({ msg: 'foo' }),
205+
template: `<span>{{ msg }}</span>`
206+
})
207+
}, 0)
208+
209+
const vm = new Vue({
210+
template: '<div><foo ref="foo" /></div>',
211+
components: { Foo }
212+
}).$mount(dom)
213+
214+
expect('not matching server-rendered content').not.toHaveBeenWarned()
215+
expect(dom.innerHTML).toBe('<span>foo</span>')
216+
expect(vm.$refs.foo).toBeUndefined()
217+
218+
setTimeout(() => {
219+
expect(dom.innerHTML).toBe('<span>foo</span>')
220+
expect(vm.$refs.foo).not.toBeUndefined()
221+
vm.$refs.foo.msg = 'bar'
222+
waitForUpdate(() => {
223+
expect(dom.innerHTML).toBe('<span>bar</span>')
224+
expect(dom.querySelector('span')).toBe(span)
225+
}).then(done)
226+
}, 0)
227+
})
228+
229+
it('should hydrate async component without showing loading', done => {
230+
const dom = createMockSSRDOM('<span>foo</span>')
231+
const span = dom.querySelector('span')
232+
233+
const Foo = () => ({
234+
component: new Promise(resolve => {
235+
setTimeout(() => {
236+
resolve({
237+
data: () => ({ msg: 'foo' }),
238+
template: `<span>{{ msg }}</span>`
239+
})
240+
}, 10)
241+
}),
242+
delay: 1,
243+
loading: {
244+
render: h => h('span', 'loading')
245+
}
246+
})
247+
248+
const vm = new Vue({
249+
template: '<div><foo ref="foo" /></div>',
250+
components: { Foo }
251+
}).$mount(dom)
252+
253+
expect('not matching server-rendered content').not.toHaveBeenWarned()
254+
expect(dom.innerHTML).toBe('<span>foo</span>')
255+
expect(vm.$refs.foo).toBeUndefined()
256+
257+
setTimeout(() => {
258+
expect(dom.innerHTML).toBe('<span>foo</span>')
259+
}, 1)
260+
261+
setTimeout(() => {
262+
expect(dom.innerHTML).toBe('<span>foo</span>')
263+
expect(vm.$refs.foo).not.toBeUndefined()
264+
vm.$refs.foo.msg = 'bar'
265+
waitForUpdate(() => {
266+
expect(dom.innerHTML).toBe('<span>bar</span>')
267+
expect(dom.querySelector('span')).toBe(span)
268+
}).then(done)
269+
}, 10)
270+
})
271+
272+
it('should hydrate async component by replacing DOM if error occurs', done => {
273+
const dom = createMockSSRDOM('<span>foo</span>')
274+
275+
const Foo = () => ({
276+
component: new Promise((resolve, reject) => {
277+
setTimeout(() => {
278+
reject('something went wrong')
279+
}, 10)
280+
}),
281+
error: {
282+
render: h => h('span', 'error')
283+
}
284+
})
285+
286+
new Vue({
287+
template: '<div><foo ref="foo" /></div>',
288+
components: { Foo }
289+
}).$mount(dom)
290+
291+
expect('not matching server-rendered content').not.toHaveBeenWarned()
292+
expect(dom.innerHTML).toBe('<span>foo</span>')
293+
294+
setTimeout(() => {
295+
expect('Failed to resolve async').toHaveBeenWarned()
296+
expect(dom.innerHTML).toBe('<span>error</span>')
297+
done()
298+
}, 10)
299+
})
198300
})

0 commit comments

Comments
 (0)