Skip to content

Commit

Permalink
fix(ssr): ensure hydrated class & style bindings are reactive
Browse files Browse the repository at this point in the history
fix #7063
  • Loading branch information
yyx990803 committed Nov 16, 2017
1 parent 6b79919 commit 5db86b4
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 44 deletions.
39 changes: 39 additions & 0 deletions src/core/observer/traverse.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* @flow */

import { _Set as Set, isObject } from '../util/index'
import type { SimpleSet } from '../util/index'

const seenObjects = new Set()

/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
47 changes: 7 additions & 40 deletions src/core/observer/watcher.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
/* @flow */

import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import {
warn,
remove,
Expand All @@ -12,7 +9,11 @@ import {
handleError
} from '../util/index'

import type { ISet } from '../util/index'
import { traverse } from './traverse'
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'

import type { SimpleSet } from '../util/index'

let uid = 0

Expand All @@ -34,8 +35,8 @@ export default class Watcher {
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: ISet;
newDepIds: ISet;
depIds: SimpleSet;
newDepIds: SimpleSet;
getter: Function;
value: any;

Expand Down Expand Up @@ -233,37 +234,3 @@ export default class Watcher {
}
}
}

/**
* Recursively traverse an object to evoke all converted
* getters, so that every nested property inside the object
* is collected as a "deep" dependency.
*/
const seenObjects = new Set()
function traverse (val: any) {
seenObjects.clear()
_traverse(val, seenObjects)
}

function _traverse (val: any, seen: ISet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
return
}
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}
6 changes: 3 additions & 3 deletions src/core/util/env.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ if (typeof Set !== 'undefined' && isNative(Set)) {
_Set = Set
} else {
// a non-standard Set polyfill that only works with primitive keys.
_Set = class Set implements ISet {
_Set = class Set implements SimpleSet {
set: Object;
constructor () {
this.set = Object.create(null)
Expand All @@ -86,11 +86,11 @@ if (typeof Set !== 'undefined' && isNative(Set)) {
}
}

interface ISet {
interface SimpleSet {
has(key: string | number): boolean;
add(key: string | number): mixed;
clear(): void;
}

export { _Set }
export type { ISet }
export type { SimpleSet }
11 changes: 10 additions & 1 deletion src/core/vdom/patch.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import VNode from './vnode'
import config from '../config'
import { SSR_ATTR } from 'shared/constants'
import { registerRef } from './modules/ref'
import { traverse } from '../observer/traverse'
import { activeInstance } from '../instance/lifecycle'
import { isTextInputType } from 'web/util/element'

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

// Note: this is a browser-only function so we can assume elms are DOM nodes.
function hydrate (elm, vnode, insertedVnodeQueue, inVPre) {
Expand Down Expand Up @@ -611,12 +614,18 @@ export function createPatchFunction (backend) {
}
}
if (isDef(data)) {
let fullInvoke = false
for (const key in data) {
if (!isRenderedModule(key)) {
fullInvoke = true
invokeCreateHooks(vnode, insertedVnodeQueue)
break
}
}
if (!fullInvoke && data['class']) {
// ensure collecting deps for deep class bindings for future updates
traverse(data['class'])
}
}
} else if (elm.data !== vnode.text) {
elm.data = vnode.text
Expand Down
35 changes: 35 additions & 0 deletions test/unit/modules/vdom/patch/hydration.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -353,4 +353,39 @@ describe('vdom patch: hydration', () => {
}).$mount(dom)
expect('not matching server-rendered content').not.toHaveBeenWarned()
})

// #7063
it('should properly initialize dynamic style bindings for future updates', done => {
const dom = createMockSSRDOM('<div style="padding-left:0px"></div>')

const vm = new Vue({
data: {
style: { paddingLeft: '0px' }
},
template: `<div><div :style="style"></div></div>`
}).$mount(dom)

// should update
vm.style.paddingLeft = '100px'
waitForUpdate(() => {
expect(dom.children[0].style.paddingLeft).toBe('100px')
}).then(done)
})

it('should properly initialize dynamic class bindings for future updates', done => {
const dom = createMockSSRDOM('<div class="foo bar"></div>')

const vm = new Vue({
data: {
cls: [{ foo: true }, 'bar']
},
template: `<div><div :class="cls"></div></div>`
}).$mount(dom)

// should update
vm.cls[0].foo = false
waitForUpdate(() => {
expect(dom.children[0].className).toBe('bar')
}).then(done)
})
})

0 comments on commit 5db86b4

Please sign in to comment.