Принцип vue nextTick

Vue.js
Принцип vue nextTick

Я говорил о vue2.x ранееОтзывчивые принципы, vue.js использует стратегию асинхронного обновления для обновления представления, давайте посмотрим, как она это делает.

/** ? */

for(let i = 0; i < 100; i++) {
    this.count++;
}

/** ? */
在dom更新后执行一些操作
this.$nextTick(fn)

Сначала два вопроса:

  1. Цикл for обновляет значение счетчика, будет ли дом обновлен 100 раз?
  2. Как nextTick отслеживает завершение обновления dom?

Асинхронное обновление задействует механизм работы js, подробности можно посмотреть здесь 【механизм цикла событий】 В этой статье мы в основном анализируем принципиальную реализацию nextTick с точки зрения исходного кода.

Это класс наблюдателя в нашем реактивном

<!--观察者Watcher类-->
class Watcher {
    constructor  () {
        Dep.target = this  // new Watcher的时候把观察者存放到Dep.target里面
    }
    update () {
        queueWatcher(this) // 异步更新策略
    }
    run () {
        //  dom在这里执行真正的更新
    }
}

Объект наблюдателя обновляется и выполняет обновление, а функция queueWatcher в основном выполняется внутри, и объект наблюдателя передается как this, поэтому мы начинаем с queueWatcher.

queueWatcher

Функция queueWatcher находится в файле планировщика.

/** queueWatcher函数*/
let has = {};
let queue = [];
let waiting = false;

function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  // 防止queue队列wachter对象重复
  if (has[id] == null) {
    has[id] = true
    queue.push(watcher)
    
    // 传递本次的更新任务
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

/** flushSchedulerQueue函数 */
function flushSchedulerQueue () {
    let watcher, id;
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        id = watcher.id;
        has[id] = null;
        // 执行更新
        watcher.run();
    }
    // 更新完毕恢复标志位
    waiting = false;
}
  1. В очереди хранится объект-наблюдатель, который мы хотим обновить на этот раз. Функция queueWatcher выполняет сложную операцию оценки. Один и тот же объект-наблюдатель будет добавлен в очередь только один раз.
  2. Функция flushSchedulerQueue, в свою очередь, вызывает метод run объекта wacther для выполнения обновления. и передается функции nextTick в качестве обратного вызова.
  3. Флаг ожидания показывает, передали ли мы задачу обновления функции nextTick. NextTick обработает входящий обратный вызов после завершения текущей задачи. Его нужно передать только один раз, а затем сбросить флаг после завершения обновления.

next-tick


let callbacks = [];
let pending = false;
let timerFunc;

/**----- nextTick -----*/
function nextTick (cb) {
    // 把传进来的回调函数放到callbacks队列里
    callbacks.push(cb);

    // pending代表一个等待状态 等这个tick执行
    if (!pending) {
        pending = true
        timerFunc()
    }
    
    // 如果没传递回调 提供一个Promise化的调用
    if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
    }
}

/**----- timerFunc ----*/

// 1、优先考虑Promise实现
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]')) {
// 2、降级到MutationObserver实现
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
// 3、降级到setImmediate实现
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
// 4、如果以上都不支持就用setTimeout来兜底了
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

function flushCallbacks () {
  // 将callbacks中的cb依次执行
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
  1. Переданная функция обратного вызова будет сохранена в очереди обратных вызовов.Здесь используются обратные вызовы вместо выполнения функции обратного вызова непосредственно в nextTick, потому что это может гарантировать, что nextTick будет выполнен несколько раз в одном и том же тике, и рендеринг будет завершен за один тик. , Запустите несколько асинхронных задач.

    // 举个栗子🌰
    // 假如我们直接在nexttick里面直接执行回调
    
    function nextTick (cb) {
        setTimeout(cb)
    }
    nextTick(cb1)
    nextTick(cb2)
    
    这种情况下就会开启两个异步任务,也就是两次事件循环,造成了页面不必要的渲染
    
  2. timerFunc — это ядро ​​реализации, которое будет предпочтительно использовать микрозадачи, такие как Promise, чтобы обеспечить выполнение в одном и том же цикле событий, так что страница должна быть отображена только один раз. Если это действительно не работает, используйте setTimeout, чтобы получить практический результат.Хотя это вызовет вторичный рендеринг, это также худший случай. Здесь Vue использует стратегию обработки понижения версии.

$nextTick

Наконец, повесьте функцию nexttick на прототип Vue, и все в порядке.

Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this)
}

резюме

Асинхронное обновление Vue по сути представляет собой применение механизма событий js, отдающее приоритет микрозадачам с высоким приоритетом, а для совместимости реализована стратегия понижения версии.

Теперь вернитесь к двум вопросам в начале

  1. Цикл for обновляет значение счетчика, будет ли дом обновлен 100 раз?

    Нет, так как функция queueWatcher выполняет фильтрацию, один и тот же объект-наблюдатель не будет добавляться повторно.

  2. Как nextTick отслеживает завершение обновления dom?

    Vue использует асинхронную очередь для управления обновлением DOM и обратным вызовом nextTick для последовательного выполнения, гарантируя, что обратный вызов может быть выполнен после обновления DOM.