Простая для понимания стратегия асинхронного обновления Vue и принцип nextTick

JavaScript Vue.js
Простая для понимания стратегия асинхронного обновления Vue и принцип nextTick

Недавно я изучил некоторые низкоуровневые знания. Поэтому я хочу сделать серию, чтобы попытаться рассказать об этих более сложных и важных моментах знания. Обучение похоже на большую гору: только взобравшись на гору самостоятельно, вы сможете увидеть разные пейзажи и получить более глубокие переживания. Сегодня мы поговорим о более важной стратегии асинхронного обновления и принципе nextTick в Vue.

Прежде чем говорить о теме, мы можем взглянуть на следующие вопросы интервью:

setTimeout(() => {
  console.log('真的在300ms后打印吗?')
}, 300)

Этот код очень прост, я думаю, многие скажут да. Конечно, есть и большие коровы, и с первого взгляда видно, что ответ не обязательно. Почему это? Как отвечать на такие вопросы в процессе интервью может удовлетворить интервьюера и как распространять наши знания.

Перед формальным объяснением мы можем сначала понять некоторые простые понятия:

Что такое процесс:Процесс — это наименьшая единица, которой ЦП выделяет ресурсы; (это наименьшая единица, которая может владеть ресурсами и работать независимо)

Что такое нить:Поток — это наименьшая единица планирования процессора (поток — это одноразовая единица выполнения программы, основанная на процессе, а процесс может иметь несколько потоков).

Концепция относительно скучна для понимания, поэтому давайте проведем аналогию:

обработатьЭто компания, у каждой компании есть свои ресурсы, которыми можно управлять; компании независимы друг от друга; инитьТо есть каждый сотрудник в компании, несколько сотрудников работают вместе для выполнения задачи, в компании может быть один сотрудник или более, и сотрудники делят пространство компании.

Браузер является многопроцессорным:В браузере каждый раз, когда открывается вкладка, фактически открывается новый процесс, в котором также есть потоки рендеринга пользовательского интерфейса, потоки движка js и потоки HTTP-запросов. Итак, браузер — это многопроцессорный процесс.

js является однопоточным:js — это язык сценариев браузера, в основном для реализации взаимодействия между пользователем и браузером и для работы с домом; это определяет, что это может быть только один поток, иначе возникнут очень сложные проблемы с синхронизацией. Например: если js разработан с несколькими потоками, если один поток хочет изменить элемент dom, а другой поток хочет удалить этот элемент dom, браузер будет в недоумении и будет в недоумении. Таким образом, чтобы избежать сложности, JavaScript с самого начала был однопоточным.

механизм выполнения js — цикл событий

Так как js является однопотоковым, js-дизайнеры делят задачи на синхронные задачи и асинхронные задачи.Синхронные задачи ставятся в очередь для выполнения в основном потоке.В очереди задач, дождавшись завершения всех задач основного потока, оповещают задачу очереди, чтобы исполняемые задачи могли быть помещены в основной поток для выполнения. После выполнения асинхронной задачи в основном потоке очередь задач уведомляется о необходимости поместить следующую асинхронную задачу в основной поток для выполнения. Этот процесс продолжается до тех пор, пока не завершится выполнение асинхронной задачи.Этот непрерывный и повторяющийся процесс называется циклом событий. А петля - это галочка.

Асинхронные задачи в очереди задач можно разделить на два типамикротаста такжемакрозадача

микротаст: Promise, process.nextTick, Object.observe, MutationObserver

макрозадача: общий код скрипта, setTimeout, setInterval и т.д.

С точки зрения приоритета выполнения, сначала выполняется макрозадача макрозадача, а затем выполняется микрозадача минкрозадача.

Несколько моментов, на которые стоит обратить внимание при реализации:

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

Потому что setTimeout 300 мс не означает, что он будет выполнен сразу после 300 мс, а будет помещен в список задач через 300 мс. Подождите, пока цикл событий выполнит код, ожидая его выполнения. Если в списке асинхронных задач есть только эта макрозадача, то она ровно 300 мс. Но если есть другие задачи типа микротаста, то больше 300мс.

Расширение один:

Этот тип вопросов иногда встречается в процессе собеседования. До этого я думал, что тест — это разница между let и var, но на самом деле он содержит и те знания, о которых мы сегодня говорили.

for(var i =0 ;i < 3; i++) {
  console.log("for中i的值:"+i)
  var time = setTimeout(() => {
    console.log("setTimeout中i的值:"+i)
  }, 300);
}

Как выглядит результат печати:

  • 1. При выполнении цикла for определяются три таймера.Поскольку setTimeout является асинхронной задачей, все три таймера будут добавлены в очередь задач через 300 мс.
  • 2. Выполнить код в это время, и вывести значения i in for по очереди: 1, 2, 3
  • Через 300мс каждый setTimeout добавляется в очередь задач.В это время цикл for уже выполнен.В это время i становится 3 после выполнения основного потока. Поскольку анонимная функция обратного вызова setTimeout в это время хранит ссылку на внешнюю переменную i, она, наконец, выводит значения i в 3 setTimeout: 3

Использование let для изменения результата не то же самое. Наконец, значения i в setTimeout выводятся по очереди: 0, 1, 2

for(let i =0 ;i < 3; i++) {
  console.log("for中i的值:"+i)
  var time = setTimeout(() => {
    console.log("setTimeout中i的值:"+i)
  }, 300);
}
  • 1. Переменные, объявленные var, действительны в глобальной области видимости, поэтому в мире есть только одна переменная i. Значение переменной i будет меняться каждый раз, когда цикл зацикливается, а внутри цикла i присваивается переменной Функция setTimeout указывает на глобальный i. В сочетании с механизмом выполнения событий, упомянутым ранее, i последнего раунда печати также равно 3.
  • 2. Для переменных, объявленных с помощью let, let допустим только в блоках и областях видимости, а продвижение переменной отсутствует. Таким образом, i в setTimeout — это новая переменная каждый раз в цикле.

🤔Поскольку i каждого цикла является новым значением, вывод результата должен быть значением инициализации 1? Это связано с тем, что движок JavaScript запомнит значение предыдущего цикла, и когда переменная i этого цикла будет инициализирована, она будет рассчитана на основе предыдущего цикла.

Расширение два:

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

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

Этот код часто проверяют на собеседованиях, раньше я очень путался, но когда вы освоите сегодняшние знания, вы обнаружите, что все так просто

  • 1. Так как скрипт тоже относится к макрозадаче, то содержимое всего скрипта помещается в основной поток (стек задач), а код выполняется по порядку. Затем найдите console.log(1) и напечатайте 1 напрямую.
  • Когда встречается setTimeout, это означает, что он будет добавлен в очередь задач через 0 секунд.Поскольку setTimeout является макрозадачей, она будет помещена в следующую макрозадачу, которая здесь выполняться не будет.
  • При обнаружении нового промиса выполнение кода нового промиса в процессе экземпляра выполняется синхронно, и только обратный вызов .then() является микрозадачей. Итак, сначала напечатайте 3. После выполнения цикла выведите 4. Затем встретите первый .then(), который принадлежит микрозадаче и добавляется в очередь микрозадач этого цикла. Затем выполните down и встретите еще один .then(), который добавляется в очередь микрозадач этого цикла. Затем продолжайте движение вниз.
  • Когда встречается console.log(7), выведите 7 напрямую. До этого времени выполнение макрозадачи событийного цикла завершается, а затем идем проверять, есть ли в этом цикле микрозадачи, и обнаруживаем, что там как раз .then() , сразу ставим в основной поток на выполнение, и печатаем из 5. Затем я обнаружил, что был второй .then(), который сразу же был помещен в основной поток для выполнения, и было напечатано 6. На этом этапе список задач микрозадач пуст. На этом заканчивается первый цикл.
  • Второй цикл обработки событий находит setTimeout, введенный в первый раз из списка задач макрозадач, помещает его в основной поток для выполнения и выводит 2.
  • Окончательные результаты печати: 1, 3, 4, 7, 5, 6, 2

Расширение в vue — стратегия асинхронного обновления и принцип nextTick

Обычно есть некоторые возможные интервью часто спрашивают: Что vue в nextTick, что? Его принцип и действие в чем? Что за nextTick в конце, так это то, что в официальном документе так определено:

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

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

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

В сочетании с исходным кодом давайте попробуем еще раз: когда запускается метод установки определенных данных, его функция установки уведомит Dep в замыкании, и Dep вызовет все объекты Watch, которыми он управляет. Запустите реализацию обновления объекта Watch. Давайте посмотрим, как реализовано обновление. (Dep и Watcher здесь являются основой отзывчивости Vue, которая будет рассмотрена в последующих главах. Здесь вам нужно только понимать, что при изменении состояния и обновлении для обновления вызывается функция update)

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

Как видно из кода, функция queueWatcher(this) вызывается при изменении состояния, что также позволяет vue асинхронно обновлять очередь. Итак, давайте посмотрим, что делает queueWatcher

 /*将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送*/
export function queueWatcher (watcher: Watcher) {
  /*获取watcher的id*/
  const id = watcher.id
  /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
  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.
      let i = queue.length - 1
      while (i >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

Из кода queueWatcher видно, что объект Watch не сразу обновляет представление, а помещается в очередь очередь, в это время состояние находится в состоянии ожидания, в это время будут продолжаться объекты Watch. помещается в очередь очереди и ждет до следующей.Когда тик работает, все очереди очереди вынимаются и запускаются один раз, и эти объекты Watch будут пройдены и вынуты для обновления представления. При этом Наблюдатели с повторяющимися идентификаторами не будут добавляться в очередь несколько раз. Это также объясняет, что один и тот же наблюдатель запускается несколько раз и помещается в очередь только один раз.

На этом моменте можно на время прекратить рассуждения, и научиться на такой картинке присматриваться повнимательнее:

Подводя итог рисунку и циклу событий, упомянутому ранее: Vue использует асинхронный способ обновления DOM, чтобы избежать частых манипуляций с DOM. Эти асинхронные операции будут помещать эти операции в очередь задач в виде cb через функцию nextTick (с приоритетом микрозадачи), и когда каждый тик закончится, эти cbs будут выполняться для обновления DOM.

Чтобы добиться эффекта асинхронного обновления, в vue установлена ​​функция nextTick.Далее давайте посмотрим, как реализуется nextTick.Адрес источникаПроверить.

/*存放异步执行的回调*/
const callbacks = [] 
/*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
let pending = false
/*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
let timerFunc

/*
  推送到队列中下一个tick时执行
  cb 回调函数
  ctx 上下文
*/
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
   // 第一步 传入的cb会被push进callbacks中存放起来
  callbacks.push(() => {
    if (cb) {      
        try {
            cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })  
  // 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了。pending此处相当于一个锁
  if (!pending) {
  // 若上一个异步任务队列已经执行完毕,则将pending设定为true(把锁锁上)
    pending = true
    // 调用判断Promise,MutationObserver,setTimeout的优先级
    timerFunc()
  }
  // 第三步执行返回的状态
  if (!cb && typeof Promise !== 'undefined') {   
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

Функция timerFunc здесь опущена, она предназначена для оценки приоритета использования Promise, MutationObserver и setTimeout. Promise будет использоваться в системе первым, а MutationObserver будет использоваться, когда Promise не существует.Эти два метода будут выполняться в микрозадаче и будут выполняться раньше, чем setTimeout, поэтому они используются первыми. Если среда не поддерживает два вышеуказанных метода, setTimeout будет использоваться для отправки этой функции в конце задачи и ожидания выполнения вызова.

Мысль 🤔: почему вы отдаете предпочтение Promise, MutationObserver 》setTimeout

В основном потоке, если вы снова столкнетесь с макрозадачей, поместите ее в конец очереди задач макрозадачи. Поскольку цикл событий может принимать только одну макрозадачу, обнаруженная макрозадача должна ждать других раундов цикла событий; микрозадача, поместите ее в очереди микрозадач этого цикла. Это позволит понять, почему микрозадача будет обработана раньше макрозадачи. Это также то, что nextTick всегда выполняется до setTimeout.

Подводя итог, внимательно изучите официальную документацию, чтобы лучше понять:официальная документация

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