Асинхронное обновление Vue.js и nextTick

внешний интерфейс JavaScript Vue.js Promise

Статья впервые опубликована на:GitHub.com/US TB-Вуд, умри, о ты…

написать впереди

Некоторое время, чтобы написать пункты перед проектом, есть некоторые сомнения по поводу NEXTTICK. Изучив различную информацию, подведите итог стратегии асинхронного обновления Vue.js, а также использования и принципа NEXTTICK. Если есть место, чтобы обобщить неправильное место, добро пожаловать, чтобы указать!

Эта статья будет резюмирована из следующих 3 пунктов:

  1. Почему Vue.js обновляет представления асинхронно?
  2. Как JavaScript работает асинхронно?
  3. Когда использовать nextTick?

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

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

Напечатанный результат — «начало». Мы четко назначаем сообщение «конец» в событии клика, но innerHTML реального узла DOM не получает ожидаемого «начала». Почему?

см. другой пример

 <template>
  <div>
    <div>{{number}}</div>
    <div @click="handleClick">click</div>
  </div>
</template>
 export default {
    data () {
        return {
            number: 0
        };
    },
    methods: {
        handleClick () {
            for(let i = 0; i < 10000; i++) {
                this.number++;
            }
        }
    }
}

После щелчка по событию клика число будет пройдено и увеличено 10 000 раз. В реактивной системе Vue.js вы можете взглянуть на мою предыдущую статьюПринцип адаптивной системы Vue.js. Мы знаем, что Vue.js пройдет через процессы «setter->Dep->Watcher->patch->View». .

Согласно предыдущему пониманию, каждый раз, когда число равно +1, будет срабатывать установщик числа для изменения реального DOM в соответствии с описанным выше процессом, а затем DOM будет обновляться 10 000 раз.Об этом интересно думать. ! Посмотрите описание на официальном сайте:Vue выполняет обновления DOM асинхронно. Как только наблюдается изменение данных, Vue открывает очередь и буферизует все изменения данных, которые происходят в том же цикле событий. Если один и тот же наблюдатель запускается несколько раз, он будет помещен в очередь только один раз. Эта дедупликация во время буферизации важна, чтобы избежать ненужных вычислений и манипуляций с DOM..

Как работает JavaScript

Чтобы облегчить понимание стратегии асинхронного обновления Vue.js и nextTick, сначала представим механизм работы следующего JS, обратитесь к учителю Руан Ифэн.Подробное объяснение механизма работы JavaScript: снова поговорим о цикле событий. Ключевая часть извлечения заключается в следующем: JS является однопоточным, а это означает, что одновременно может выполняться только одно действие. Он основан на опросе событий и может быть разделен на следующие этапы:

(1) Все задачи синхронизации выполняются в основном потоке, образуя стек контекста выполнения.

(2) В дополнение к основному потоку существует еще «очередь задач» (task queue). Как только асинхронная задача имеет запущенный результат, событие помещается в «очередь задач».

(3) Как только все задачи синхронизации в «стеке выполнения» будут выполнены, система прочитает «очередь задач», чтобы увидеть, какие события в ней находятся. Затем соответствующие асинхронные задачи завершают состояние ожидания, входят в стек выполнения и начинают выполнение.

(4) Основной поток продолжает повторять третий шаг выше.

На приведенном выше рисунке схематично показан основной поток и очередь задач. Пока основной поток пуст, он будет читать «очередь задач», как работает JavaScript. Этот процесс будет повторяться. Процесс выполнения основного потока — это тик. Все асинхронные результаты планируются через «очередь задач». Существует два основных типа очередей задач: "макрозадача" и "микрозадача". Эти два типа задач попадают в очередь задач. Общие макрозадачи включают setTimeout, MessageChannel, postMessage и setImmediate, а общие микрозадачи включают MutationObsever и Promise.then.

опрос событий

Когда Vue.js изменяет данные, он не будет изменять данные немедленно, а будет ждать обновления данных, опрашиваемых тем же событием, а затем единообразно обновлять представление.Знай почтиПример выше:

 //改变数据
vm.message = 'changed'

//想要立即使用更新后的DOM。这样不行,因为设置message后DOM还没有更新
console.log(vm.$el.textContent) // 并不会得到'changed'

//这样可以,nextTick里面的代码会在DOM更新后执行
Vue.nextTick(function(){
    console.log(vm.$el.textContent) //可以得到'changed'
})

Диаграмма:

Имитация следующего тика

Определение nextTick на официальном сайте:

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

Следующее использует setTimeout для имитации nextTick. Во-первых, определите обратные вызовы для хранения nextTick. Прежде чем следующий тик обработает функцию обратного вызова, все cbs будут сохранены в этом массиве обратных вызовов. pending — это бит флага, который представляет состояние ожидания. Тогда setTimeout создаст в задаче событие flushCallbacks, а flushCallbacks будет выполнять все cbs в callbacks по очереди при выполнении.

// 存储nextTick
let callbacks = [];
let pending = false;

function nextTick (cb) {
    callbacks.push(cb);

    if (!pending) {
        // 代表等待状态的标志位
        pending = true;
        setTimeout(flushCallbacks, 0);
    }
}

function flushCallbacks () {
    pending = false;
    const copies = callbacks.slice(0);
    callbacks.length = 0;
    for (let i = 0; i < copies.length; i++) {
        copies[i]();
    }
}

Реальный код намного сложнее, чем здесь.В исходном коде Vue.js nextTick определен в отдельном файле для обслуживания, в src/core/util/next-tick.js:

/* @flow */
/* globals MessageChannel */

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIOS, isNative } from './env'

const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    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).
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 */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

// Determine microtask defer implementation.
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(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.
    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.
 */
export function withMacroTask (fn: Function): Function {
  return fn._withTask || (fn._withTask = function () {
    useMacroTask = true
    const res = fn.apply(null, arguments)
    useMacroTask = false
    return res
  })
}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  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
    })
  }
}

После добавления комментариев:

 /**
 * Defer a task to execute it asynchronously.
 */
 /*
    延迟一个任务使其异步执行,在下一个tick时执行,一个立即执行函数,返回一个function
    这个函数的作用是在task或者microtask中推入一个timerFunc,在当前调用栈执行完以后以此执行直到执行到timerFunc
    目的是延迟到当前调用栈执行完以后执行
*/
export const nextTick = (function () {
  /*存放异步执行的回调*/
  const callbacks = []
  /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
  let pending = false
  /*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
  let timerFunc

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

  // the nextTick behavior leverages the microtask queue, which can be accessed
  // via either native Promise.then or MutationObserver.
  // MutationObserver has wider support, however it is seriously bugged in
  // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
  // completely stops working after triggering a few times... so, if native
  // Promise is available, we will use it:
  /* istanbul ignore if */

  /*
    这里解释一下,一共有Promise、MutationObserver以及setTimeout三种尝试得到timerFunc的方法
    优先使用Promise,在Promise不存在的情况下使用MutationObserver,这两个方法都会在microtask中执行,会比setTimeout更早执行,所以优先使用。
    如果上述两种方法都不支持的环境则会使用setTimeout,在task尾部推入这个函数,等待调用执行。
    参考:https://www.zhihu.com/question/55364497
  */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    /*使用Promise*/
    var p = Promise.resolve()
    var logError = err => { console.error(err) }
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError)
      // 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.
      if (isIOS) setTimeout(noop)
    }
  } else if (typeof MutationObserver !== 'undefined' && (
    isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    MutationObserver.toString() === '[object MutationObserverConstructor]'
  )) {
    // use MutationObserver where native Promise is not available,
    // e.g. PhantomJS IE11, iOS7, Android 4.4
    /*新建一个textNode的DOM对象,用MutationObserver绑定该DOM并指定回调函数,在DOM变化的时候则会触发回调,该回调会进入主线程(比任务队列优先执行),即textNode.data = String(counter)时便会触发回调*/
    var counter = 1
    var observer = new MutationObserver(nextTickHandler)
    var textNode = document.createTextNode(String(counter))
    observer.observe(textNode, {
      characterData: true
    })
    timerFunc = () => {
      counter = (counter + 1) % 2
      textNode.data = String(counter)
    }
  } else {
    // fallback to setTimeout
    /* istanbul ignore next */
    /*使用setTimeout将回调推入任务队列尾部*/
    timerFunc = () => {
      setTimeout(nextTickHandler, 0)
    }
  }

  /*
    推送到队列中下一个tick时执行
    cb 回调函数
    ctx 上下文
  */
  return function queueNextTick (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
      timerFunc()
    }
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve
      })
    }
  }
})()

Ключ — timeFunc(), который действует как отложенное выполнение.

Из приведенного выше введения можно узнать, что существует три реализации timeFunc().

  • Promise
  • MutationObserver
  • setTimeout

использовать

Цель nextTick

Сценарий приложения: необходимо работать на основе нового представления после обновления представления.

См. пример: Нажмите кнопку show, чтобы отобразить исходное поле ввода v-show: false и получить фокус:

 <div id="app">
  <input ref="input" v-show="inputShow">
  <button @click="show">show</button>  
 </div>
new Vue({
  el: "#app",
  data() {
   return {
     inputShow: false
   }
  },
  methods: {
    show() {
      this.inputShow = true
      this.$nextTick(() => {
        this.$refs.input.focus()
      })
    }
  }
})

Можете обратить внимание на мой паблик-аккаунт «Muchen Classmate», фермера на гусиной фабрике, который обычно записывает какие-то банальные мелочи, технологии, жизнь, инсайты и срастается.