5

I would like to force the UI to update midway through an event loop cycle.

Vue.nextTick

Vue.nextTick seems to provide you with an updated version of vm.$el, but doesn't actually cause the UI to update.

CodePen: https://codepen.io/adamzerner/pen/RMexgJ?editors=1010

HTML:

<div id="example">
  <p>Value: {{ message }}</p>
  <button v-on:click="change()">Change</button>
</div>

JS:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'A'
  },
  methods: {
    change: change
  }
})

function change () {
  vm.message = 'B';
  // vm.$el.children[0].textContent === "Value: A"
  Vue.nextTick(function () {
    // vm.$el.children[0].textContent === "Value: B"
    // but the UI hasn't actually updated
    for (var i = 0; i < 10000000; i++) {}
    vm.message = 'C';
  });
}

vm.$forceUpdate

vm.$forceUpdate doesn't appear to do anything at all.

  1. It doesn't appear to change the value of vm.$el.
  2. It doesn't appear to update the UI.

CodePen: https://codepen.io/adamzerner/pen/rdqpJW?editors=1010

HTML:

<div id="example">
  <p>Value: {{ message }}</p>
  <button v-on:click="change()">Change</button>
</div>

JS:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'A'
  },
  methods: {
    change: change
  }
})

function change () {
  vm.message = 'B';
  // vm.$el.children[0].textContent === "Value: A"
  vm.$forceUpdate();
  // vm.$el.children[0].textContent === "Value: A" still
  // and the UI hasn't actually updated
  for (var i = 0; i < 10000000; i++) {}
  vm.message = 'C';
}

v-bind:key

v-bind:key also doesn't appear to do anything at all:

  1. It doesn't appear to change the value of vm.$el.
  2. It doesn't appear to update the UI.

Codepen: https://codepen.io/adamzerner/pen/WzadKN?editors=1010

HTML:

<div id="example">
  <p v-bind:key="message">Value: {{ message }}</p>
  <button v-on:click="change()">Change</button>
</div>

JS:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'A'
  },
  methods: {
    change: change
  }
})

function change () {
  // vm.$el.children[0].textContent === "Value: A"
  vm.message = 'B';
  // vm.$el.children[0].textContent === "Value: A" still
  // and the UI hasn't actually updated
  for (var i = 0; i < 10000000; i++) {}
  vm.message = 'C';
}

computed

Using a computed property, as this popular answer recommends, also doesn't appear to do anything:

  1. It doesn't appear to change the value of vm.$el.
  2. It doesn't appear to update the UI.

CodePen: https://codepen.io/adamzerner/pen/EEdoeX?editors=1010

HTML:

<div id="example">
  <p>Value: {{ computedMessage }}</p>
  <button v-on:click="change()">Change</button>
</div>

JS:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'A'
  },
  computed: {
    computedMessage: function () {
      return this.message;
    },
  },
  methods: {
    change: change
  }
})

function change () {
  // vm.$el.children[0].textContent === "Value: A"
  vm.message = 'B';
  // vm.$el.children[0].textContent === "Value: A" still
  // and the UI hasn't actually updated
  for (var i = 0; i < 10000000; i++) {}
  vm.message = 'C';
}

Promise (added in edit)

Using promises doesn't work either.

CodePen: https://codepen.io/adamzerner/pen/oqaEpV?editors=1010

HTML:

<div id="example">
  <p>Value: {{ message }}</p>
  <button v-on:click="change()">Change</button>
</div>

JS:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'A'
  },
  methods: {
    change: change
  }
})

function change () {
  // vm.$el.children[0].textContent === "Value: A"
  vm.message = 'B';
  // vm.$el.children[0].textContent === "Value: A" still
  // and the UI hasn't actually updated
  var promise = new Promise(function (resolve, reject) {
    for (var i = 0; i < 10000000; i++) {}
    resolve();
  });
  promise.then(function () {
    vm.message = 'C';
  });
}

setTimeout

setTimeout is the only thing that seems to work. But it only works consistently when the delay is 100. When the delay is 0, it works sometimes, but doesn't work consistently.

  1. vm.$el updates.
  2. The UI updates.

CodePen: https://codepen.io/adamzerner/pen/PRyExg?editors=1010

HTML:

<div id="example">
  <p>Value: {{ message }}</p>
  <button v-on:click="change()">Change</button>
</div>

JS:

var vm = new Vue({
  el: '#example',
  data: {
    message: 'A'
  },
  methods: {
    change: change
  }
})

function change () {
  // vm.$el.children[0].textContent === "Value: A"
  vm.message = 'B';
  setTimeout(function () {
    // vm.$el.children[0].textContent === "Value: B"
    // the UI has updated
    for (var i = 0; i < 10000000; i++) {}
    vm.message = 'C';
  }, 100);
}

Questions

  1. Why don't Vue.nextTick, vm.$forceUpdate, v-bind:key, or computed properties work?
  2. Why does setTimeout work inconsistently when the delay is 0?
  3. setTimeout seems hacky. Is there a "propper" way to force a UI update?
tony19
  • 125,647
  • 18
  • 229
  • 307
Adam Zerner
  • 17,797
  • 15
  • 90
  • 156
  • 1
    The change function is synchronous, so by definition blocks. Nothing else is going to happen. setTimeout doesn't work because the execution context is still the synchronous function. There is a simple solution, but it depends on your use case for counting to 10MM. – Randy Casburn Apr 06 '18 at 04:32
  • Would you mind elaborating on a few things @RandyCasburn? Regarding the execution context inside of `setTimeout`, I'm not accessing `this` so I don't see how that's relevant. And the execution context is the same regardless of whether the delay is `0` or `100`, yet changing the delay to `100` causes `setTimeout` to work. Suppose that my use case is simply to get the UI to show "B" immediately after clicking "Change", and then to "C" a few moments later. Can you provide the simple solution that you have in mind? – Adam Zerner Apr 06 '18 at 04:45
  • You have two options: 1: set a watch property and watch `message`: watch : { message: function(){} } [Watchers](https://vuejs.org/v2/guide/computed.html#Watchers); or 2: the $watch API method [$watch](https://vuejs.org/v2/api/#vm-watch). I recommend #1 since it directly addresses your property. These simply inject an async capability into the synchronous function. – Randy Casburn Apr 06 '18 at 04:54
  • Here you go: https://codepen.io/anon/pen/OvBQmV?editors=1010 – Randy Casburn Apr 06 '18 at 05:03
  • @RandyCasburn that isn't working for me. `watch` is only firing after `C` is assigned to `message`. – Adam Zerner Apr 06 '18 at 05:14
  • I tried getting the value in DOM using document.querySelector. Turns out that the value is correct. But the display is not updated. So Vue did update the DOM immediately. But the for loop is blocking the rendering. – Jacob Goh Apr 06 '18 at 07:05
  • @AdamZerner - OK, finally got some time to investigate and post an explanation. Hope you find it helpful. – Randy Casburn Apr 06 '18 at 16:58
  • `setTimeout` is the solution. It essentially delays the new data changes gives the event loop and async update queue some time to pick up previous data changes and update UI. It doesn't have to be as long as 100ms, I tested 10ms is more than enough, on my PC though. – Marshal Jan 23 '20 at 02:02

3 Answers3

5

Synopsis

The illusion of B not being updated/displayed in the UI is caused by a combination of Vue's Async Update Queue and JavaScript's Event Loop Process model. For details and proof read on.

#Summary of Findings#

These actually do what you want (but don't seem to)

  • Vue.nextTick
  • setTimeout - (but doesn't seem to with short timeout)

These work as expected (but require explanation)

  • v-bind:key
  • vm.$forceUpdate
  • Promise

Note: The but doesn't seem to above is an acknowledgment that Vue is doing what it is supposed to but the expected visual output does not appear. Therefore, the code doesn't produce the expected output is accurate.

Discussion

First Two Work

Proving the first two do what you want is quite easy. The idea of 'B' not being placed in the view will be disproved. But further discussion is required to address the lack of visible change.

  • Open each of the Pens in Chrome
  • In dev tools, set a break point in vue.js on line 1789
  • Step through the sequence

While you step through the sequence you will notice the UI is updated with the value 'B' as it should (regardless of length of timeout). Dispelled.

So what about the lack of visibility? This is caused by JavaScript's Event Loop process model and is specifically related to a principle called Run-to-Completion. The MDN Event Loop Documentation states:

A downside of this model is that if a message takes too long to complete, the web application is unable to process user interactions like click or scroll.

or run the render/paint browser processes. So when the stack is executed, B is rendered then C immediately thereafter, which seems like B is never rendered. One can see this exact problem when using an animated GIF with a JavaScript heavy task, such as bootstrapping a SPA. The animated GIF either will stutter or will not animate at all - the Run-to-Completion is in the way.

So Vue does what it is supposed to and JavaScript does what it is supposed to correctly. But the long running loop is troublesome. This is the reason tools like lodash _debounce or simple setTimout are helpful.

Last Three Work?

Yes. Using the same breakpoint in vue.js will show the only break happens when Vue is flushing its queue of updates. As discussed in Vue's documentation about Async Update Queue each update is queued and only the last update for each property is rendered. So although message is actually changed to B during processing, it is never rendered because of the way the Vue Async Queue works:

In case you haven’t noticed yet, Vue performs DOM updates asynchronously. Whenever a data change is observed, it will open a queue and buffer all the data changes that happen in the same event loop.

tony19
  • 125,647
  • 18
  • 229
  • 307
Randy Casburn
  • 13,840
  • 1
  • 16
  • 31
  • Additionally: `$forceUpdate()` is odd. It doesn't update immediately even though it seems like ti should. Debugging shows it runs asynchronously for `data` attributes. But, it can run synchronously for `props` that are bound synchronously. Seems odd to me. – Randy Casburn Apr 06 '18 at 17:16
  • I feel kinda stupid... the issue is with JavaScript/the event loop, not with Vue.js. Thanks for explaining this. – Adam Zerner Apr 06 '18 at 18:29
  • Glad to! It is an interesting problem that affects all of us in so many ways. When I first posted my solution I ran through the debugger and saw 'B'. The you told me it didn't work - duh, dawned on me right then. – Randy Casburn Apr 06 '18 at 18:39
0

I would look at using Vue Instance Lifecycle Hooks.

Take a look at this Vue Instance Lifecycle Diagram as well.

Tying in with a Lifecycle Hook should give you the ability to disrupt something midway if you need to update it. Maybe do some sort of check with the beforeMount hook.

tony19
  • 125,647
  • 18
  • 229
  • 307
  • Would you mind providing a code example of that approach working successfully? – Adam Zerner Apr 06 '18 at 04:46
  • I have not really done much work with tying in to the lifecyle myself, but I am familiar with how it works. [Alligator.io](https://alligator.io/vuejs/component-lifecycle/) has a pretty good explanation of how it works. I would assume it would look something like this: `` – Marvin WordWeaver Parsons Apr 06 '18 at 05:03
0

I'm not sure what you want but try this...

var vm = new Vue({
      el: '#example',
      data: function(){
        return {
          message: 'A'
        }
      },
      methods: {
        change: function(){
          this.message = 'B';
          let el = this;
          setTimeout(()=>{
            el.$nextTick(function () {
              el.message = 'C';
            })
          },2000)
        }
      }
    })
  • 1
    I am aware that `setTimeout` works (it doesn't require `$nextTick` to work btw). See the last section with the heading "Questions" in my OP for the explicit questions that I am looking for answers to. – Adam Zerner Apr 06 '18 at 08:15
  • [Async update que](https://vuejs.org/v2/guide/reactivity.html#Async-Update-Queue) have you tried `vm.$nextTick().then(()=>{/*your code*/})` or try `Vue.nextTick().then(()=>{/*your code*/})` _use breakpoint to see that it is updating a ,b, c accrodingly._ – Ravi Vasoya Apr 06 '18 at 08:46