Repository: vuejs/vue
Pull request: fix: beforeUpdate should be called before render and allow state mutation

fix #7481 

Changed files:
filename: src/core/instance/lifecycle.js
patch: @@ -50,9 +50,6 @@ export function initLifecycle (vm: Component) {
 export function lifecycleMixin (Vue: Class<Component>) {
   Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
     const vm: Component = this
-    if (vm._isMounted) {
-      callHook(vm, 'beforeUpdate')
-    }
     const prevEl = vm.$el
     const prevVnode = vm._vnode
     const prevActiveInstance = activeInstance
@@ -197,7 +194,13 @@ export function mountComponent (
   // we set this to vm._watcher inside the watcher's constructor
   // since the watcher's initial patch may call $forceUpdate (e.g. inside child
   // component's mounted hook), which relies on vm._watcher being already defined
-  new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
+  new Watcher(vm, updateComponent, noop, {
+    before () {
+      if (vm._isMounted) {
+        callHook(vm, 'beforeUpdate')
+      }
+    }
+  }, true /* isRenderWatcher */)
   hydrating = false
 
   // manually mounted instance, call mounted on self

filename: src/core/observer/scheduler.js
patch: @@ -53,6 +53,9 @@ function flushSchedulerQueue () {
   // as we run existing watchers
   for (index = 0; index < queue.length; index++) {
     watcher = queue[index]
+    if (watcher.before) {
+      watcher.before()
+    }
     id = watcher.id
     has[id] = null
     watcher.run()

filename: src/core/observer/watcher.js
patch: @@ -37,6 +37,7 @@ export default class Watcher {
   newDeps: Array<Dep>;
   depIds: SimpleSet;
   newDepIds: SimpleSet;
+  before: ?Function;
   getter: Function;
   value: any;
 
@@ -58,6 +59,7 @@ export default class Watcher {
       this.user = !!options.user
       this.lazy = !!options.lazy
       this.sync = !!options.sync
+      this.before = options.before
     } else {
       this.deep = this.user = this.lazy = this.sync = false
     }

filename: src/platforms/web/runtime/components/transition-group.js
patch: @@ -33,6 +33,21 @@ delete props.mode
 export default {
   props,
 
+  beforeMount () {
+    const update = this._update
+    this._update = (vnode, hydrating) => {
+      // force removing pass
+      this.__patch__(
+        this._vnode,
+        this.kept,
+        false, // hydrating
+        true // removeOnly (!important, avoids unnecessary moves)
+      )
+      this._vnode = this.kept
+      update.call(this, vnode, hydrating)
+    }
+  },
+
   render (h: Function) {
     const tag: string = this.tag || this.$vnode.data.tag || 'span'
     const map: Object = Object.create(null)
@@ -76,17 +91,6 @@ export default {
     return h(tag, null, children)
   },
 
-  beforeUpdate () {
-    // force removing pass
-    this.__patch__(
-      this._vnode,
-      this.kept,
-      false, // hydrating
-      true // removeOnly (!important, avoids unnecessary moves)
-    )
-    this._vnode = this.kept
-  },
-
   updated () {
     const children: Array<VNode> = this.prevChildren
     const moveClass: string = this.moveClass || ((this.name || 'v') + '-move')

filename: test/unit/features/options/lifecycle.spec.js
patch: @@ -137,6 +137,21 @@ describe('Options lifecycle hooks', () => {
         expect(spy).toHaveBeenCalled()
       }).then(done)
     })
+
+    it('should be called before render and allow mutating state', done => {
+      const vm = new Vue({
+        template: '<div>{{ msg }}</div>',
+        data: { msg: 'foo' },
+        beforeUpdate () {
+          this.msg += '!'
+        }
+      }).$mount()
+      expect(vm.$el.textContent).toBe('foo')
+      vm.msg = 'bar'
+      waitForUpdate(() => {
+        expect(vm.$el.textContent).toBe('bar!')
+      }).then(done)
+    })
   })
 
   describe('updated', () => {