Дополнительные возможности Vue -- Анализ vue.nextTick()

внешний интерфейс исходный код JavaScript Vue.js Promise опрос

Когда мы отправляем ajax на задний план в циклах жизни beforeCreate и created vue, и когда данные возвращаются, мы обнаруживаем, что узел DOM еще не сгенерировал нерабочий узел, так что же нам делать?

На этот раз мы будем использовать метод this.$nextTick (думаю, вы его использовали).

nextTick — это функция глобального vue в системе vue, используемая для обработки операции обновления dom. В vue есть наблюдатель, который используется для наблюдения за изменениями данных, а затем обновления dom.В vue он не запускает обновление dom каждый раз при изменении данных, а кэширует эти операции в очереди, и обновляет очередь после завершения цикла обработки событий. , единообразно выполняет операцию обновления dom.

Обычно нам не нужно об этом заботиться, и если мы хотим что-то сделать после обновления состояния DOM, нам нужно использовать nextTick. Операция DOM, выполняемая функцией-ловушкой created() жизненного цикла vue, должна быть помещена в функцию обратного вызова Vue.nextTick(), потому что DOM не выполняет никакого рендеринга, когда функция-ловушка created() выполняется, и Операция DOM в это время бесполезна, поэтому здесь мы должны поместить JS-код манипулирования DOM в функцию обратного вызова Vue.nextTick(). Для соответствующей установленной функции-ловушки все монтирование и рендеринг DOM были завершены, когда функция-ловушка выполняется.В настоящее время не будет проблем с функцией-ловушкой, выполняющей любые операции с DOM.

Vue.nextTick(callback), когда данные изменяются, обратный вызов выполняется после обновления.

Vue.$nextTick(callback), когда домен изменяется, обратный вызов выполняется после обновления.

См. следующий фрагмент кода:

<template>
  <div>
    <div ref="text">{{text}}</div>
    <button @click="handleClick">text</button>
  </div>
</template>
export default {
    data () {
        return {
            text: 'start'
        };
    },
    methods () {
        handleClick () {
            this.text = 'end';
            console.log(this.$refs.text.innerText);//打印“start”
        }
    }
}

Результат печати — начало, почему тексту задано значение «конец», а innerText реального DOM-узла получает не ожидаемый нами «конец», а предыдущее значение «начало»?

Интерпретация исходного кода

Имея в виду этот вопрос, мы нашли Watch-реализацию исходного кода Vue.js. Когда реагирующие данные изменяются, его функция установки уведомляет Dep в замыкании, и Dep вызывает все объекты Watch, которыми он управляет. Запустите реализацию обновления объекта Watch. Давайте посмотрим на реализацию update.

watcher

/*
      调度者接口,当依赖发生改变的时候进行回调。
   */
  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
    /*同步则执行run直接渲染视图*/
      this.run()
    } else {
    /*异步推送到观察者队列中,由调度者调用。*/
      queueWatcher(this)
    }
  }

Мы обнаружили, что Vue.js по умолчанию выполняет обновления DOM асинхронно. Когда обновление выполняется асинхронно, вызывается функция queueWatcher.

/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 **/
 /*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
    /*获取watcher的id*/
  const id = watcher.id
   /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
    /*如果没有flush掉,直接push到队列中即可*/
      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.
      let 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 (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

Глядя на исходный код queueWatcher, мы обнаруживаем, что объект Watch не обновляет представление сразу, а помещается в очередь очереди.В это время состояние находится в состоянии ожидания.В это время будут продолжаться Объекты наблюдения помещаются в очередь очереди, ожидая следующего.При галочке эти объекты наблюдения будут пройдены и извлечены для обновления представления. При этом Watchers с повторяющимися id не будут добавляться в очередь несколько раз, потому что при финальном рендеринге нам нужно заботиться только о конечном результате данных.

flushSchedulerQueue

vue/src/core/observer/scheduler.js
/**
 * Flush both queues and run the watchers.
 */
  /*nextTick的回调函数,在下一个tick时flush掉两个队列同时运行watchers*/
function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  /*
    刷新前给queue排序,这样做可以保证:
    1.组件更新的顺序是从父组件到子组件的顺序,因为父组件总是比子组件先创建。
    2.一个组件的user watchers比render watcher先运行,因为user watchers往往比render watcher更早创建
    3.如果一个组件在父组件watcher运行期间被销毁,它的watcher执行将被跳过。
  */
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  /*这里不用index = queue.length;index > 0; index--的方式写是因为不要将length进行缓存,
  因为在执行处理现有watcher对象期间,更多的watcher对象可能会被push进queue*/
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
     /*将has的标记删除*/
    has[id] = null
     /*执行watcher*/
    watcher.run()
    // in dev build, check and stop circular updates.
    /*
      在测试环境中,检测watch是否在死循环中
      比如这样一种情况
      watch: {
        test () {
          this.test++;
        }
      }
      持续执行了一百次watch代表可能存在死循环
    */
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }
  // keep copies of post queues before resetting state
  /*得到队列的拷贝*/
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  /*重置调度者的状态*/
  resetSchedulerState()

  // call component updated and activated hooks
  /*使子组件状态都改编成active同时调用activated钩子*/
  callActivatedHooks(activatedQueue)
  /*调用updated钩子*/
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

flushSchedulerQueue — это функция обратного вызова для следующего тика, основная цель — выполнить функцию запуска наблюдателя для обновления представления.

nextTick

Vue.js предоставляет функцию nextTick, которая на самом деле является nextTick, вызванной выше.

Реализация nextTick относительно проста: цель выполнения — поместить функцию в микрозадачу или задачу и выполнить функцию, переданную nextTick, после выполнения текущего стека (также будут некоторые задачи, которые необходимо выполнить в микрозадаче или задаче). передний).

Реализация nextTick, обсуждаемая во многих статьях в Интернете, — это реализация ниже версии 2.4. Версии выше 2.5 внесли множество изменений во внутреннюю реализацию nextTick. Взгляните на исходный код:

Первый — начать с Vue 2.5+ и вытащить отдельный файл next-tick.js для его выполнения.

vue/src/core/util/next-tick.js
 /*
    延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function
    这个函数的作用是在task或者microtask中推入一个timerFunc,
    在当前调用栈执行完以后以此执行直到执行到timerFunc
    目的是延迟到当前调用栈执行完以后执行
*/
/*存放异步执行的回调*/
const callbacks = []
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false

/*下一个tick时的回调*/
function flushCallbacks () {
/*一个标记位,标记等待状态(即函数已经被推入任务队列或者主线程,已经在等待当前栈执行完毕去执行),这样就不需要在push多个回调到callbacks时将timerFunc多次推入任务队列或者主线程*/
  pending = false
  //复制callback
  const copies = callbacks.slice(0)
  //清除callbacks
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
  //触发callback的回调函数
    copies[i]()
  }
}

// Here we have async deferring wrappers using both microtasks and (macro) tasks.
// In < 2.4 we used microtasks everywhere, but there are some scenarios where
// microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690) or even between bubbling of the same
// event (#6566). However, using (macro) tasks everywhere also has subtle problems
// when state is changed right before repaint (e.g. #6813, out-in transitions).
// Here we use microtask by default, but expose a way to force (macro) task when
// needed (e.g. in event handlers attached by v-on).
/**
其大概的意思就是:在Vue2.4之前的版本中,nextTick几乎都是基于microTask实现的,
但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,
就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场
景也会有性能的影响。所以最终nextTick采取的策略是默认走microTask,对于一些DOM
的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask。
**/

let microTimerFunc
let macroTimerFunc
let useMacroTask = false

// Determine (macro) task defer implementation.
// Technically setImmediate should be the ideal choice, but it's only available
// in IE. The only polyfill that consistently queues the callback after all DOM
// events triggered in the same loop is by using MessageChannel.
/* istanbul ignore if */
/**
而对于macroTask的执行,Vue优先检测是否支持原生setImmediate(高版本IE和Edge支持),
不支持的话再去检测是否支持原生MessageChannel,如果还不支持的话为setTimeout(fn, 0)。
**/

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && ( 
// MessageChannel与原先的MutationObserver异曲同工
/**
在Vue 2.4版本以前使用的MutationObserver来模拟异步任务。
而Vue 2.5版本以后,由于兼容性弃用了MutationObserver。
Vue 2.5+版本使用了MessageChannel来模拟macroTask。
除了IE以外,messageChannel的兼容性还是比较可观的。
**/
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  /**
  可见,新建一个MessageChannel对象,该对象通过port1来检测信息,port2发送信息。
  通过port2的主动postMessage来触发port1的onmessage事件,
  进而把回调函数flushCallbacks作为macroTask参与事件循环。
  **/
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
   //上面两种都不支持,用setTimeout
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */

if (typeof Promise !== 'undefined' && isNative(Promise)) {
/*使用Promise*/
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    // in problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    //iOS的webview下,需要强制刷新队列,执行上面的回调函数
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}

/**
 * Wrap a function so that if any code inside triggers state change,
 * the changes are queued using a (macro) task instead of a microtask.
 */
 /**
 在Vue执行绑定的DOM事件时,默认会给回调的handler函数调用withMacroTask方法做一层包装,
 它保证整个回调函数的执行过程中,遇到数据状态的改变,这些改变而导致的视图更新(DOM更新)
 的任务都会被推到macroTask而不是microtask。
 **/
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}
 /*
    推送到队列中下一个tick时执行
    cb 回调函数
    ctx 上下文
  */
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
   /*cb存到callbacks中*/
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  
  if (!pending) {
    pending = true
    if (useMacroTask) {
      macroTimerFunc()
    } else {
      microTimerFunc()
    }
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

MessageChannel VS setTimeout

Почему вы предпочитаете MessageChannel для создания macroTask вместо setTimeout?

HTML5 предусматривает, что минимальная задержка setTimeout составляет 4 мс, что означает, что асинхронный обратный вызов может быть запущен за 4 мс в идеальной среде.

Vue использует так много функций для имитации асинхронных задач, и его единственная цель — сделать обратные вызовы асинхронными и вызываться как можно раньше. Задержка MessageChannel явно меньше, чем setTimeout.

Сказав так много, что такое макрозадачи и что такое микрозадачи?

Конкретная реализация обоих

макрозадачи:

setTimeout, setInterval, setImmediate, requestAnimationFrame, ввод-вывод, рендеринг пользовательского интерфейса

microtasks:

Обещание, process.nextTick, Object.observe, MutationObserver

Это можно резюмировать просто так:

1. Выполните самую раннюю задачу в очереди макрозадач, а затем удалите ее.

2. Затем выполнить все доступные задачи в очереди микрозадач, а затем удалить их

3. В следующем цикле выполнить задачу в следующей макрозадаче (перейти к шагу 2)

Так что же такое очередь задач, о которой мы упоминали выше? Какая связь между макрозадачами и микрозадачами?

• An event loop has one or more task queues.(task queue is macrotask queue)
• Each event loop has a microtask queue.
• task queue = macrotask queue != microtask queue
• a task may be pushed into macrotask queue,or microtask queue
• when a task is pushed into a queue(micro/macro),we mean preparing work is finished,
so the task can be executed now.

Переведите это на:

• Цикл событий имеет одну или несколько очередей задач;

• Каждый цикл событий имеет очередь микрозадач;

• Очередь макрозадач — это то, что мы часто называем очередью задач, а очередь микрозадач — это не очередь задач;

• Задачу можно поставить в очередь макрозадач или в очередь микрозадач;

• Когда задача помещается в очередь микрозадач или макрозадач, подготовительная работа завершена, и в это время задача может быть выполнена.

Видно, что setTimeout и Promises — это не однотипные задачи, и методы обработки должны быть разными. отэта статьяНашел следующую фразу:

Microtasks are usually scheduled for things that should happen straight after the currently
executing script, such as reacting to a batch of actions, or to make something async
without taking the penalty of a whole new task. The microtask queue is processed after
callbacks as long as no other JavaScript is mid-execution, and at the end of each task. Any
additional microtasks queued during microtasks are added to the end of the queue and also
processed. Microtasks include mutation observer callbacks, and as in the above example,
promise callbacks.

Говоря простым языком, роль микрозадач заключается в планировании задач, которые должны выполняться сразу после выполнения текущего исполняемого скрипта. Например, реагирование на события или асинхронные операции, чтобы не платить за дополнительную задачу.

Микрозадача будет выполняться в двух случаях:

Очередь задач (macrotask = очередь задач) выполняется после обратного вызова при условии, что нет другого исполняемого кода. Выполняется в конце каждой задачи. Кроме того, при обработке микрозадач, если есть вновь добавленные микрозадачи, они также будут добавлены в конец очереди и выполнены.

То есть порядок выполнения такой:

Старт -> взять первую задачу из очереди задач на выполнение -> взять все задачи микрозадачи на выполнение последовательно -> взять следующую задачу очереди задач на выполнение -> снова взять все задачи микрозадачи на выполнение выполнить -> ... Этот цикл повторяется

Once a promise settles, or if it has already settled, it queues a microtask for its
reactionary callbacks. This ensures promise callbacks are async even if the promise has
already settled. So calling .then(yey, nay) against a settled promise immediately queues a
microtask. This is why promise1 and promise2 are logged after script end, as the currently
running script must finish before microtasks are handled. promise1 and promise2 are logged
before setTimeout, as microtasks always happen before the next task.

Как только обещание установлено как завершенное, оно планирует микрозадачу для своего обратного вызова (функция внутри .then).

Затем мы вернемся к нашему коду выше:

setTimeout(function(){
    console.log(1)
},0);
new Promise(function(resolve){
    console.log(2)
    for( var i=100000 ; i>0 ; i-- ){
        i==1 && resolve()
    }
    console.log(3)
}).then(function(){
    console.log(4)
});
console.log(5);

Проведите повторный анализ в соответствии с приведенными выше правилами:

При запуске в setTimeout функция обратного вызова console.log(1) для setTimeout будет помещена в очередь задач, а затем продолжит выполнение вниз.

Далее идет Обещание. Сначала выполните и распечатайте console.log(2), а затем выполните цикл for. Даже если цикл for накопит до 100 000, он также находится в стеке выполнения. Дождавшись завершения цикла for, переключите состояние Обещание от выполненного разрешить, а затем поставить требуемое. Выполненная функция обратного вызова, то есть console.log(4), затем помещается в микрозадачу. Далее, немедленно запускается console.log(3).

Затем из Promise остается синхронный console.log(5) и распечатайте его напрямую. Таким образом, в первом раунде последовательно были напечатаны 2, 3 и 5.

Теперь первый раунд очереди задач выполнен, и код не выполняется. В соответствии с условиями выполнения микрозадачи, упомянутыми выше, задачи в микрозадаче будут выполняться первыми, поэтому выполняется console.log(4).

Наконец, функция console.log(1), помещенная в setTimeout в макрозадаче, остается для выполнения.

Выходная последовательность этого анализа:

2
3
5
4
1

Давайте посмотрим еще на один:

Когда в программе есть: setTimeout, setInterval, setImmediate, I/O, UI rendering, Promise, process.nextTick, Object.observe, MutationObserver:

1. Сначала выполните макрозадачи: ввод-вывод -> рендеринг пользовательского интерфейса.

2. Снова выполнить микрозадачи: process.nextTick -> Promise -> MutationObserver -> Object.observe

3. Затем поместите setTimeout setInterval setImmediate в новую макрозадачу по порядку:

setImmediate --> setTimeout, setInterval

Подводя итог, цель nextTick — сгенерировать функцию обратного вызова для добавления в задачу или микрозадачу.После выполнения текущего стека (в середине могут быть другие функции) вызывается функция обратного вызова, которая воспроизводит асинхронный триггер (то есть, когда происходит следующий тик) цель триггера.

setImmediate(function(){
    console.log(1);
},0);
setTimeout(function(){
    console.log(2);
},0);
new Promise(function(resolve){
    console.log(3);
    resolve();
    console.log(4);
}).then(function(){
    console.log(5);
});
console.log(6);
process.nextTick(function(){
    console.log(7);
});
console.log(8);
结果是:3 4 6 8 7 5 1 2

Каковы преимущества использования nextTick для асинхронного обновления представлений?

Далее давайте посмотрим на демо:

<template>
  <div>
    <div>{{test}}</div>
  </div>
</template>
export default {
    data () {
        return {
            test: 0
        };
    },
    created () {
      for(let i = 0; i < 1000; i++) {
        this.test++;
      }
    }
}

Сейчас такая ситуация, когда значение test будет создано, оно будет выполнено 1000 раз по циклу ++. Каждый раз ++ срабатывает setter->Dep->Watcher->update->patch в соответствии с отзывчивостью. Если в это время представление не обновляется асинхронно, то ++ будет напрямую манипулировать DOM для обновления представления каждый раз, что очень сильно влияет на производительность. Таким образом, Vue.js реализует очередь очереди, которая будет равномерно выполнять запуск наблюдателя в очереди на следующем такте. При этом Watcher с одним и тем же id не будут добавляться в очередь повторно, поэтому 1000 запусков Watcher выполняться не будут. Окончательное представление обновления только напрямую изменит 0 DOM, соответствующий тесту, на 1000. Гарантируется, что действие по обновлению DOM операции просмотра вызывается на следующем тике после выполнения текущего стека, что значительно оптимизирует производительность.

Если вам это нравится, дайте ему звезду и поддержите его.github

благодарныйНачалось ва такжеzhlevenпредоставленные идеи.