Анализ исходного кода $nextTick в Vue

исходный код Vue.js

  Когда мы работаем над проектами, мы часто используем nextTick.Простое понимание состоит в том, что это функция setTimeout, которая обрабатывается асинхронно, замена ее на setTimeout, кажется, может работать, но так ли это просто? Так почему бы нам просто не использовать setTimeout напрямую? Давайте разберем его поглубже.

проблема найдена

   Помните, что раньше было требование, заключающееся в отображении кнопки для расширения в зависимости от количества строк текста, поэтому нам нужно получить высоту текста после назначения данных в Vue.

<div id="app">
    <div class="msg">
        {{msg}}
    </div>
</div>
new Vue({
    el: '#app',
    data: function(){
        return {
            msg: ''
        }
    },
    mounted(){
        this.msg = '我是测试文字'
        console.log(document.querySelector('.msg').offsetHeight) //0
    }
})

   В настоящее время, независимо от того, как вы его получите, высота текста в Div равна 0; но она имеет значение, когда вы получаете ее напрямую:

problem.png

   Такая же ситуация возникает при передаче параметров в подкомпоненты, после того как мы передаем параметры в подкомпоненты, мы вызываем функции в подкомпонентах для просмотра параметров.

<div id="app">
    <div class="msg">
        <form-report ref="child" :name="childName"></form-report>
    </div>
</div>
Vue.component('form-report', {
    props: ['name'],
    methods: {
        showName(){
            console.log('子组件name:'+this.name)
        }
    },
    template: '<div>{{name}}</div>'
})
new Vue({
    el: '#app',
    data: function(){
        return {
            childName: '',
        }
    },
    mounted(){
        this.childName = '我是子组件名字'
        this.$refs.child.showName()
    }
})

  Хотя название подкомпонента отображается на странице, оно печатается с пустым значением:

problem1.png

Асинхронное обновление

   Мы обнаружили, что возникновение двух вышеупомянутых проблем, будь то дочерний компонент или родительский компонент, даетdataЭто вызвано проверкой данных сразу после присвоения значения. Поскольку действие «просмотр данных» является синхронной операцией, и все это происходит после присваивания, поэтому давайте предположим, что присваивание данных данным является асинхронной операцией и не выполняется немедленно. Официальный сайт Vue описывает операции с данными следующим образом:

Если вы не заметили, Vue выполняется асинхронно при обновлении DOM. Как только он узнает об изменении данных, Vue откроет очередь и буферизует все изменения данных, которые происходят в том же цикле событий. Если один и тот же наблюдатель запускается несколько раз, он будет помещен в очередь только один раз. Эта дедупликация во время буферизации важна, чтобы избежать ненужных вычислений и манипуляций с DOM. Затем, в следующем цикле событий «тик», Vue очищает очередь и выполняет фактическую (дедублированную) работу. Vue внутренне пытается использовать собственные Promise.then, MutationObserver и setImmediate для асинхронных очередей.Если среда выполнения не поддерживает это, вместо этого будет использоваться setTimeout(fn, 0).

   означает, что мы устанавливаемthis.msg = 'some thing'При , Vue не сразу обновляет данные DOM, а ставит эту операцию в очередь, если мы выполним ее повторно, то очередь также выполнит операцию дедупликации, подождитев том же цикле событийПосле того, как все изменения данных в очереди будут завершены, события в очереди будут взяты для обработки.

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

   Чтобы манипулировать DOM после операции обновления данных, мы можем использовать сразу после изменения данныхVue.nextTick(callback); Таким образом, функция обратного вызова будет вызываться после завершения обновления DOM, и можно будет получить последний элемент DOM.

//第一个demo
this.msg = '我是测试文字'
this.$nextTick(()=>{
    //20
    console.log(document.querySelector('.msg').offsetHeight)
})
//第二个demo
this.childName = '我是子组件名字'
this.$nextTick(()=>{
    //子组件name:我是子组件名字
    this.$refs.child.showName()
})

Анализ исходного кода nextTick

   Разобравшись с использованием и принципом nextTick, давайте посмотрим, как Vue реализует эту волну «операций».

opt.jpg

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

const callbacks = []
let pending = false
let timerFunc

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
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

   Сначала находимnextTickГде эта функция определена, посмотрите, что она делает; обратите внимание, что она определяет три переменные во внешнем слое, и одна переменная очень знакома по имени: callbacks, то есть очередь, о которой мы упоминали выше; определена во внешнем слое nextTick. формирует замыкание, поэтому каждый раз, когда мы вызываем $nextTick, мы фактически добавляем функцию обратного вызова к обратным вызовам.

  callbacks добавили функцию обратного вызова и выполнили функцию timerFunc,pendingИспользуется для идентификации того же времени может быть выполнено только один раз. Так что же делает эта функция timerFunc?Давайте продолжим смотреть на код:

export let isUsingMicroTask = false
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  //判断1:是否原生支持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)
  }
}

Есть несколькоisNativeфункция, которая используется для определения того, поддерживаются ли переданные параметры изначально в текущей среде; например, некоторые браузеры не поддерживают Promise, хотя мы используем прокладку (polify), но isNative(Promise) все равно вернет false.

   Можно видеть, что код здесь фактически делает четыре суждения, постоянно понижает текущую среду и пытается использовать нативнуюPromise.then,MutationObserverиsetImmediate, вышеперечисленные три не поддерживают конечное использование setTimeout; цель обработки перехода на более раннюю версию состоит в том, чтобыflushCallbacksФункция помещается в микрозадачу (оценка 1 и оценка 2) или макрозадачу (оценка 3 и оценка 4) и ожидает выполнения следующего цикла обработки событий.MutationObserverЭто новая функция Html5, которая используется для отслеживания того, изменяется ли целевая структура DOM, то есть вновь созданный textNode в коде; если он изменяется, выполняется функция обратного вызова в конструкторе MutationObserver, но она выполняется в микрозадача.

   Затем мы, наконец, нашли последнего большого босса: flushCallbacks; nextTick отчаянно вкладывал его в микрозадачи или макрозадачи для выполнения, где это святое? Давайте посмотрим, как это выглядит на самом деле:

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

   Обратные вызовы flushCallback, которые я считал сложными, состояли всего из 8 коротких строк. То, что он делает, также очень просто: копирует массив обратных вызовов, затем устанавливает обратные вызовы пустыми и, наконец, выполняет каждую функцию в скопированном массиве по очереди; поэтому его функция используется только для выполнения обратных вызовов в функции обратного вызова.

Суммировать

   К этому моменту был проанализирован весь код nextTick.Подводя итог, его процесс выглядит следующим образом:

  1. Поместите функцию обратного вызова в обратные вызовы, чтобы дождаться выполнения
  2. Поместите функцию выполнения в микрозадачу или макрозадачу
  3. Событие зацикливается на микрозадаче или макрозадаче, а функция выполнения по очереди выполняет обратные вызовы в обратных вызовах.

Возвращаясь к setTimeout, о котором мы упоминали в начале, можно увидеть, что nextTick выполнил ряд обработок совместимости для setTimeout, что в широком смысле можно понимать как помещение функции обратного вызова в setTimeout для выполнения; тем не менее, nextTick помещается в микро -task выполняется первым, а setTimeout — макрозадача, поэтому nextTick обычно выполняется до setTimeout, можно попробовать в браузере:

setTimeout(()=>{
    console.log(1)
}, 0)
this.$nextTick(()=>{
    console.log(2)
})
this.$nextTick(()=>{
    console.log(3)
})
//运行结果 2 3 1

   Гипотеза окончательно проверяется.После завершения выполнения текущей макрозадачи сначала выполняются две микрозадачи, а затем макрозадача выполняется последней.

Для получения дополнительной информации о внешнем интерфейсе, пожалуйста, обратите внимание на общедоступный номер【前端壹读】.

Если вы думаете, что это хорошо написано, пожалуйста, следуйте за мнойДомашняя страница Наггетс. Для получения дополнительных статей, пожалуйста, посетитеБлог Се Сяофэй