Skip to content

Commit 5db86b4

Browse files
committed
fix(ssr): ensure hydrated class & style bindings are reactive
fix #7063
1 parent 6b79919 commit 5db86b4

File tree

5 files changed

+94
-44
lines changed

5 files changed

+94
-44
lines changed

src/core/observer/traverse.js

+39
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/* @flow */
2+
3+
import { _Set as Set, isObject } from '../util/index'
4+
import type { SimpleSet } from '../util/index'
5+
6+
const seenObjects = new Set()
7+
8+
/**
9+
* Recursively traverse an object to evoke all converted
10+
* getters, so that every nested property inside the object
11+
* is collected as a "deep" dependency.
12+
*/
13+
export function traverse (val: any) {
14+
_traverse(val, seenObjects)
15+
seenObjects.clear()
16+
}
17+
18+
function _traverse (val: any, seen: SimpleSet) {
19+
let i, keys
20+
const isA = Array.isArray(val)
21+
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
22+
return
23+
}
24+
if (val.__ob__) {
25+
const depId = val.__ob__.dep.id
26+
if (seen.has(depId)) {
27+
return
28+
}
29+
seen.add(depId)
30+
}
31+
if (isA) {
32+
i = val.length
33+
while (i--) _traverse(val[i], seen)
34+
} else {
35+
keys = Object.keys(val)
36+
i = keys.length
37+
while (i--) _traverse(val[keys[i]], seen)
38+
}
39+
}

src/core/observer/watcher.js

+7-40
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
/* @flow */
22

3-
import { queueWatcher } from './scheduler'
4-
import Dep, { pushTarget, popTarget } from './dep'
5-
63
import {
74
warn,
85
remove,
@@ -12,7 +9,11 @@ import {
129
handleError
1310
} from '../util/index'
1411

15-
import type { ISet } from '../util/index'
12+
import { traverse } from './traverse'
13+
import { queueWatcher } from './scheduler'
14+
import Dep, { pushTarget, popTarget } from './dep'
15+
16+
import type { SimpleSet } from '../util/index'
1617

1718
let uid = 0
1819

@@ -34,8 +35,8 @@ export default class Watcher {
3435
active: boolean;
3536
deps: Array<Dep>;
3637
newDeps: Array<Dep>;
37-
depIds: ISet;
38-
newDepIds: ISet;
38+
depIds: SimpleSet;
39+
newDepIds: SimpleSet;
3940
getter: Function;
4041
value: any;
4142

@@ -233,37 +234,3 @@ export default class Watcher {
233234
}
234235
}
235236
}
236-
237-
/**
238-
* Recursively traverse an object to evoke all converted
239-
* getters, so that every nested property inside the object
240-
* is collected as a "deep" dependency.
241-
*/
242-
const seenObjects = new Set()
243-
function traverse (val: any) {
244-
seenObjects.clear()
245-
_traverse(val, seenObjects)
246-
}
247-
248-
function _traverse (val: any, seen: ISet) {
249-
let i, keys
250-
const isA = Array.isArray(val)
251-
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
252-
return
253-
}
254-
if (val.__ob__) {
255-
const depId = val.__ob__.dep.id
256-
if (seen.has(depId)) {
257-
return
258-
}
259-
seen.add(depId)
260-
}
261-
if (isA) {
262-
i = val.length
263-
while (i--) _traverse(val[i], seen)
264-
} else {
265-
keys = Object.keys(val)
266-
i = keys.length
267-
while (i--) _traverse(val[keys[i]], seen)
268-
}
269-
}

src/core/util/env.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ if (typeof Set !== 'undefined' && isNative(Set)) {
6969
_Set = Set
7070
} else {
7171
// a non-standard Set polyfill that only works with primitive keys.
72-
_Set = class Set implements ISet {
72+
_Set = class Set implements SimpleSet {
7373
set: Object;
7474
constructor () {
7575
this.set = Object.create(null)
@@ -86,11 +86,11 @@ if (typeof Set !== 'undefined' && isNative(Set)) {
8686
}
8787
}
8888

89-
interface ISet {
89+
interface SimpleSet {
9090
has(key: string | number): boolean;
9191
add(key: string | number): mixed;
9292
clear(): void;
9393
}
9494

9595
export { _Set }
96-
export type { ISet }
96+
export type { SimpleSet }

src/core/vdom/patch.js

+10-1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import VNode from './vnode'
1414
import config from '../config'
1515
import { SSR_ATTR } from 'shared/constants'
1616
import { registerRef } from './modules/ref'
17+
import { traverse } from '../observer/traverse'
1718
import { activeInstance } from '../instance/lifecycle'
1819
import { isTextInputType } from 'web/util/element'
1920

@@ -534,7 +535,9 @@ export function createPatchFunction (backend) {
534535
let hydrationBailed = false
535536
// list of modules that can skip create hook during hydration because they
536537
// are already rendered on the client or has no need for initialization
537-
const isRenderedModule = makeMap('attrs,style,class,staticClass,staticStyle,key')
538+
// Note: style is excluded because it relies on initial clone for future
539+
// deep updates (#7063).
540+
const isRenderedModule = makeMap('attrs,class,staticClass,staticStyle,key')
538541

539542
// Note: this is a browser-only function so we can assume elms are DOM nodes.
540543
function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {
@@ -611,12 +614,18 @@ export function createPatchFunction (backend) {
611614
}
612615
}
613616
if (isDef(data)) {
617+
let fullInvoke = false
614618
for (const key in data) {
615619
if (!isRenderedModule(key)) {
620+
fullInvoke = true
616621
invokeCreateHooks(vnode, insertedVnodeQueue)
617622
break
618623
}
619624
}
625+
if (!fullInvoke && data['class']) {
626+
// ensure collecting deps for deep class bindings for future updates
627+
traverse(data['class'])
628+
}
620629
}
621630
} else if (elm.data !== vnode.text) {
622631
elm.data = vnode.text

test/unit/modules/vdom/patch/hydration.spec.js

+35
Original file line numberDiff line numberDiff line change
@@ -353,4 +353,39 @@ describe('vdom patch: hydration', () => {
353353
}).$mount(dom)
354354
expect('not matching server-rendered content').not.toHaveBeenWarned()
355355
})
356+
357+
// #7063
358+
it('should properly initialize dynamic style bindings for future updates', done => {
359+
const dom = createMockSSRDOM('<div style="padding-left:0px"></div>')
360+
361+
const vm = new Vue({
362+
data: {
363+
style: { paddingLeft: '0px' }
364+
},
365+
template: `<div><div :style="style"></div></div>`
366+
}).$mount(dom)
367+
368+
// should update
369+
vm.style.paddingLeft = '100px'
370+
waitForUpdate(() => {
371+
expect(dom.children[0].style.paddingLeft).toBe('100px')
372+
}).then(done)
373+
})
374+
375+
it('should properly initialize dynamic class bindings for future updates', done => {
376+
const dom = createMockSSRDOM('<div class="foo bar"></div>')
377+
378+
const vm = new Vue({
379+
data: {
380+
cls: [{ foo: true }, 'bar']
381+
},
382+
template: `<div><div :class="cls"></div></div>`
383+
}).$mount(dom)
384+
385+
// should update
386+
vm.cls[0].foo = false
387+
waitForUpdate(() => {
388+
expect(dom.children[0].className).toBe('bar')
389+
}).then(done)
390+
})
356391
})

0 commit comments

Comments
 (0)