Procházet zdrojové kódy

fix(ssr): ensure hydrated class & style bindings are reactive

fix #7063
Evan You před 8 roky
rodič
revize
5db86b4e94

+ 39 - 0
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)
+  }
+}

+ 7 - 40
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<Dep>;
   newDeps: Array<Dep>;
-  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)
-  }
-}

+ 3 - 3
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 }

+ 10 - 1
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

+ 35 - 0
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('<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)
+  })
 })