Repository: vuejs/vue
Pull request: Ensure Vue instance's vnode and element is up to date

Fix #4284 , to recap the bug, in one sentence: 

`vm.$el` should always be the same as `vm.$vnode.elm`

More detail:

1. If a component changes its root element during rendering, all ancestors' placeholder root element's element should be all updated.

2. If one component is the root element of another component, for example `wrapper` in the test case, and wrapper's parent is updated (which triggers wrapper's `updateFromParent`), `wrapper`'s $vnode is not the same as `comp`'s $vnode.parent. While in the initial rendering, these two vnodes are the same one.

To simplify these two statements, we have to keep one invariant: `vm.$el` should always be the same as `vm.$vnode.elm`. Otherwise vdom patching will mistakenly insert new dom node.

If we correctly sync vnode's element when child tree's root changes its tag (1), and update vm's vnode when parent is updated(2), we can ensure $el is up to date.

I hope I made myself understood. it is quite perplexing.

I don't think this is the only way to fix the edge case. If @yyx990803 and other members have better alternatives, I'm happy to see it.

Changed files:
filename: src/core/instance/lifecycle.js
patch: @@ -116,6 +116,10 @@ export function lifecycleMixin (Vue: Class<Component>) {
     const vm: Component = this
     const hasChildren = !!(vm.$options._renderChildren || renderChildren)
     vm.$options._parentVnode = parentVnode
+    vm.$vnode = parentVnode // update vm's placeholder node without re-render
+    if (vm._vnode) { // update child tree's parent
+      vm._vnode.parent = parentVnode
+    }
     vm.$options._renderChildren = renderChildren
     // update props
     if (propsData && vm.$options.props) {

filename: src/core/vdom/patch.js
patch: @@ -505,9 +505,13 @@ export function createPatchFunction (backend) {
         createElm(vnode, insertedVnodeQueue)
 
         // component root element replaced.
-        // update parent placeholder node element.
+        // update parent placeholder node element, recursively
         if (vnode.parent) {
-          vnode.parent.elm = vnode.elm
+          let ancestor = vnode.parent
+          while (ancestor) {
+            ancestor.elm = vnode.elm
+            ancestor = ancestor.parent
+          }
           if (isPatchable(vnode)) {
             for (let i = 0; i < cbs.create.length; ++i) {
               cbs.create[i](emptyNode, vnode.parent)

filename: test/unit/modules/vdom/patch/edge-cases.spec.js
patch: @@ -57,4 +57,61 @@ describe('vdom patch: edge cases', () => {
       expect(vm.$el.querySelector('.d').textContent).toBe('2')
     }).then(done)
   })
+
+  it('should synchronize vm\' vnode', done => {
+    const comp = {
+      data: () => ({ swap: true }),
+      render (h) {
+        return this.swap
+          ? h('a', 'atag')
+          : h('span', 'span')
+      }
+    }
+
+    const wrapper = {
+      render: h => h('comp'),
+      components: { comp }
+    }
+
+    const vm = new Vue({
+      render (h) {
+        const children = [
+          h('wrapper'),
+          h('div', 'row')
+        ]
+        if (this.swap) {
+          children.reverse()
+        }
+        return h('div', children)
+      },
+      data: () => ({ swap: false }),
+      components: { wrapper }
+    }).$mount()
+
+    expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
+    const wrapperVm = vm.$children[0]
+    const compVm = wrapperVm.$children[0]
+    vm.swap = true
+    waitForUpdate(() => {
+      expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
+      expect(vm.$el.innerHTML).toBe('<div>row</div><a>atag</a>')
+      vm.swap = false
+    })
+    .then(() => {
+      expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
+      expect(vm.$el.innerHTML).toBe('<a>atag</a><div>row</div>')
+      compVm.swap = false
+    })
+    .then(() => {
+      expect(vm.$el.innerHTML).toBe('<span>span</span><div>row</div>')
+      expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
+      vm.swap = true
+    })
+    .then(() => {
+      expect(vm.$el.innerHTML).toBe('<div>row</div><span>span</span>')
+      expect(compVm.$vnode.parent).toBe(wrapperVm.$vnode)
+      vm.swap = true
+    })
+    .then(done)
+  })
 })