5

What is nextTick

$nextTick: According to the official documentation, it can execute a callback function after the DOM is updated and return a Promise (if supported)

// 修改数据
vm.msg = "Hello";

// DOM 还没有更新
Vue.nextTick(function() {
  // DOM 更新了
});

This piece of understanding of EventLoop should be understood at a glance. In fact, it starts to update the DOM at the beginning of the next event loop to avoid frequent operations in the middle that cause page redrawing and reflow.

This piece quotes official documents:

Maybe you haven't noticed that when Vue updates the DOM, executes asynchronously. As long as it listens to data changes, Vue will open a queue and buffer all data changes that occur in the same event loop. If the same watcher is triggered multiple times, it will only be pushed into the queue once.
This removal of duplicate data during buffering is very important to avoid unnecessary calculations and DOM operations. Then, in the next event loop "tick", Vue flushes the queue and performs the actual (de-duplicated) work. Vue internally tries to use the native Promise.then , MutationObserver and setImmediate for asynchronous queues. If the execution environment does not support it, it will use setTimeout(fn, 0) instead.

For example, when vm.text = 'new value' is set, the component will not be re-rendered immediately. When the queue is refreshed, the component will be updated in the next event loop'tick'.

<div id="example">{{message}}</div>
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

$nextTick will be used when the latest DOM data is obtained immediately after setting the this.xx='xx' data. Because the DOM update is performed asynchronously, this method is required to obtain it.

Update process (source code analysis)

  1. When the data is modified, the watcher will detect the change and then enqueue the change:
/*
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
Watcher.prototype.update = function update() {
  /* istanbul ignore else */
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};
  1. And use the nextTick method to add a flushScheduleQueue callback
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
function queueWatcher(watcher) {
  var id = watcher.id;
  if (has[id] == null) {
    has[id] = true;
    if (!flushing) {
      queue.push(watcher);
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      var i = queue.length - 1;
      while (i > index && queue[i].id > watcher.id) {
        i--;
      }
      queue.splice(i + 1, 0, watcher);
    }
    // queue the flush
    if (!waiting) {
      waiting = true;

      if (!config.async) {
        flushSchedulerQueue();
        return;
      }
      nextTick(flushSchedulerQueue);
    }
  }
}
  1. flushScheduleQueue is added to the callback array and executed asynchronously
function nextTick(cb, ctx) {
  var _resolve;
  callbacks.push(function() {
    if (cb) {
      try {
        cb.call(ctx); // !! cb 就是加入的回调
      } catch (e) {
        handleError(e, ctx, "nextTick");
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });
  if (!pending) {
    // 异步执行 操作 见timerFunc
    pending = true;
    timerFunc();
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== "undefined") {
    return new Promise(function(resolve) {
      _resolve = resolve;
    });
  }
}
  1. The timerFunc operation is executed asynchronously and sequentially judged and used: Promise.then=>MutationObserver=>setImmediate=>setTimeout
var timerFunc;

if (typeof Promise !== "undefined" && isNative(Promise)) {
  var p = Promise.resolve();
  timerFunc = function() {
    p.then(flushCallbacks);
    // 1. Promise.then
    if (isIOS) {
      setTimeout(noop);
    }
  };
  isUsingMicroTask = true;
} else if (
  !isIE &&
  typeof MutationObserver !== "undefined" &&
  (isNative(MutationObserver) ||
    MutationObserver.toString() === "[object MutationObserverConstructor]")
) {
  // 2.  MutationObserver
  var counter = 1;
  var observer = new MutationObserver(flushCallbacks);
  var textNode = document.createTextNode(String(counter));
  observer.observe(textNode, {
    characterData: true,
  });
  timerFunc = function() {
    counter = (counter + 1) % 2;
    textNode.data = String(counter);
  };
  isUsingMicroTask = true;
} else if (typeof setImmediate !== "undefined" && isNative(setImmediate)) {
  // 3. setImmediate
  timerFunc = function() {
    setImmediate(flushCallbacks);
  };
} else {
  //4. setTimeout
  timerFunc = function() {
    setTimeout(flushCallbacks, 0);
  };
}
  1. flushCallbacks traverse all callbacks and execute
function flushCallbacks() {
  pending = false;
  var copies = callbacks.slice(0);
  callbacks.length = 0;
  for (var i = 0; i < copies.length; i++) {
    copies[i]();
  }
}
  1. Among them is the flushScheduleQueue added earlier, which uses the run method of the watcher in the queue to update the component
for (index = 0; index < queue.length; index++) {
  watcher = queue[index];
  watcher.run();
}

to sum up

The above is the realization principle of vue's nextTick method. To sum it up, it is:

  1. Vue uses asynchronous queues to control DOM updates and nextTick callbacks.
  2. Because of its high-priority feature, microtask can ensure that the microtasks in the queue are executed before an event loop
  3. Due to compatibility issues, vue had to do a downgrade scheme from microtask to macrotask

reference


九旬
1.1k 声望1.2k 粉丝