Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can you force the UI to update in the middle of an event loop cycle in Vue.js?

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?
like image 589
Adam Zerner Avatar asked Apr 06 '18 04:04

Adam Zerner


1 Answers

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.

like image 165
Randy Casburn Avatar answered Sep 30 '22 04:09

Randy Casburn