From 0726c830de87bc7d1712813060330b02bc6b078c Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 16 Nov 2017 11:19:52 -0500 Subject: [PATCH] fix(ssr): ensure hydrated class & style bindings are reactive fix #7063 --- src/core/observer/traverse.js | 39 +++++++++++++++ src/core/observer/watcher.js | 47 +++---------------- src/core/util/env.js | 6 +-- src/core/vdom/patch.js | 11 ++++- .../unit/modules/vdom/patch/hydration.spec.js | 35 ++++++++++++++ 5 files changed, 94 insertions(+), 44 deletions(-) create mode 100644 src/core/observer/traverse.js diff --git a/src/core/observer/traverse.js b/src/core/observer/traverse.js new file mode 100644 index 0000000000..ca6c314eb7 --- /dev/null +++ b/src/core/observer/traverse.js @@ -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) + } +} diff --git a/src/core/observer/watcher.js b/src/core/observer/watcher.js index 40253149c3..3727bd53fb 100644 --- a/src/core/observer/watcher.js +++ b/src/core/observer/watcher.js @@ -1,8 +1,5 @@ /* @flow */ -import { queueWatcher } from './scheduler' -import Dep, { pushTarget, popTarget } from './dep' - import { warn, remove, @@ -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 @@ -34,8 +35,8 @@ export default class Watcher { active: boolean; deps: Array; newDeps: Array; - depIds: ISet; - newDepIds: ISet; + depIds: SimpleSet; + newDepIds: SimpleSet; getter: Function; value: any; @@ -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) - } -} diff --git a/src/core/util/env.js b/src/core/util/env.js index 41d8b5c005..c2e80df6cd 100644 --- a/src/core/util/env.js +++ b/src/core/util/env.js @@ -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) @@ -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 } diff --git a/src/core/vdom/patch.js b/src/core/vdom/patch.js index 1ad425e00a..f8f2858542 100644 --- a/src/core/vdom/patch.js +++ b/src/core/vdom/patch.js @@ -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' @@ -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) { @@ -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 diff --git a/test/unit/modules/vdom/patch/hydration.spec.js b/test/unit/modules/vdom/patch/hydration.spec.js index 6b9df2354f..930ad47cc8 100644 --- a/test/unit/modules/vdom/patch/hydration.spec.js +++ b/test/unit/modules/vdom/patch/hydration.spec.js @@ -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('
') + + const vm = new Vue({ + data: { + style: { paddingLeft: '0px' } + }, + template: `
` + }).$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('
') + + const vm = new Vue({ + data: { + cls: [{ foo: true }, 'bar'] + }, + template: `
` + }).$mount(dom) + + // should update + vm.cls[0].foo = false + waitForUpdate(() => { + expect(dom.children[0].className).toBe('bar') + }).then(done) + }) })