Непонимание использования async-await в vue

Vue.js
Непонимание использования async-await в vue

предисловие

Я видел использование async/await для функции ловушки, чтобы позволить асинхронному коду функции ловушки выполняться синхронно и блокировать основной поток до тех пор, пока вся логика ловушки не будет обработана, как в следующем коде:

// exp-01
export default {
  async created() {
    const timeKey = 'cost';
    console.time(timeKey);
    console.log('start created');

    this.list = await this.getList();

    console.log(this.list);
    console.log('end created');
    console.timeEnd(timeKey);
  },

  mounted() {
    const timeKey = 'cost';
    console.time(timeKey);
    console.log('start mounted');
    
    console.log(this.list.rows);

    console.log('end mounted');
    console.timeEnd(timeKey);
  },

  data() {
    return {
      list: []
    };
  },

  methods: {
    getList() {
      return new Promise((resolve) => {
        setTimeout(() => {
          return resolve({
            rows: [
              { name: 'isaac', position: 'coder' }
            ]
          });
        }, 3000);
      });
    }
  }
};

exp-01Код наконец выведет:

start created
start mounted
undefined
end mounted
mounted cost: 2.88623046875ms
{__ob__: Observer}
end created
created cost: 3171.545166015625ms

Явно желаемый эффект не достигается, почему?

согласно сexp-01Результат вывода, вы можете увидеть порядок выполнения кода, первый — это порядок выполнения хука:

created => mounted

Да, порядок выполнения хуков по-прежнему нормальный и не нарушен, свидетельство тому: код синхронизации в созданном хуке выполняется раньше, чем смонтированный:

start created
start mounted

Взгляните еще раз на асинхронный код внутри созданного хука:

this.list = await this.getList();

Вы можете увидеть результат печати this.list

end mounted
mounted cost: 2.88623046875ms
// 这是created钩子打印的this.list
{__ob__: Observer}
end created

Он печатается после выполнения смонтированного хука.Подразумевается, что асинхронный код внутри хука с использованием async/await не блокирует выполнение основного потока хука. Основной поток функции ловушки, упомянутой здесь, относится к:

beforeCreate => created => beforeMount => mounted => ...

Я предполагаю, что есть две причины для написания приведенного выше кода:

  1. Существует сильная зависимость от данных, возвращаемых асинхронным кодом, поэтому есть надежда, что созданный контент (включая функцию обратного вызова асинхронного кода) должен быть выполнен до завершения вызова контента смонтированного хука;
  2. Просто хочу, чтобы асинхронный код функции ловушки выполнялся в том порядке, в котором он был написан (exp-01действительно достигается), но не ожидал побочных эффектов;

текст

препарировать это

В предисловии я проанализировал процесс выполнения кода.Очевидно, что он выполняется не в ожидаемом порядке.Давайте сначала рассмотрим ожидаемый порядок.

// step 1
created() {
  // step 1.1
  let endTime;
  const startTime = Date.now();
  console.log(`start created: ${startTime}ms`);
  // step 1.2
  this.list = await this.getList();
  endTime = Date.now();
  console.log(this.list);
  console.log(`end created: ${endTime}ms, cost: ${endTime - startTime}ms`);
},
// step 2
mounted() {
  let endTime;
  const startTime = Date.now();
  console.log(`start mounted: ${startTime}ms`);
  console.log(this.list.rows);
  endTime = Date.now();
  console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`);
}

// step 1 => step 1.1 => step 1.2 => step 2 

Ожидаемый результат печати:

// step 1(created)
start created
// this.list
{__ob__: Observer}
end created
created cost: 3171.545166015625ms

// step 2(mounted)
start mounted
// this.list.rows
[{…}, __ob__: Observer]
end mounted
mounted cost: 2.88623046875ms

Сравнивая фактическую печать и ожидаемую печать, мы знаем, что проблема кроется в асинхронном коде, который использует await в созданном хуке, который не достигает ожидаемого нами эффекта «синхронного выполнения асинхронного кода», а лишь достигает этого до в определенной степени Эффект.

Давайте проанализируем, почему возникает этот неожиданный результат!

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

(function __main() {
  console.log('start');
  setTimeout(() => {
    console.log('console in setTimeout');
  }, 0);
  console.log('end');
})()

// output
start
end
console in setTimeout

Этот порядок печати вам ничего не напоминает? !

 

Очередь задач!

 

Все мы знаем, что код JavaScript можно разделить на две категории:

синхронный кода такжеасинхронный код

Синхронный код будет выполняться в основном потоке в том порядке, в котором он был написан;

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

Например, теперь мы инициируем асинхронный запрос:

// exp-02
console.log('start');
axios.get('http://xxx.com/getList')
  .then((resp) => {
    console.log('handle response');
  })
  .catch((error) => {
    console.error(error);
  });
console.log('end');

В основном потоке, вероятно, сначала произойдет следующий процесс:

// exp-03
// step 1
console.log('start');

// step 2
axios.get('http://xxx.com/getList');  // 此时回调函数(即then内部的逻辑)还没有被调用

// step 3
console.log('end');

Глядя на то, что браузер делает в это время!

В это время появился цикл событий (Event Loop), на самом деле он не появился в это время, но он был всегда!

Механизм «опроса по событию» определит, есть ли исполняемая задача в очереди задач в определенный период (т.н. задача на самом деле является обратным вызовом), и если есть, то она будет выполнена вне очереди.

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

// 假定没有出错的话
((resp) => {
  console.log('handle response');
})()

Пока что краткое введение в механизм очередей задач, Lenovoexp-01code, вы, вероятно, знаете причину неожиданного результата!

Хотя функция await в созданном хуке в определенной степени синхронизирована, она все равно приостановлена, собственно логика обработки (this.list=resp.xxx) добавляется в очередь задач после завершения ответа и выполняется после выполнения кода синхронизации основного потока. Вот печать после установки времени задержки на 0:

start created
start mounted
undefined
end mounted
mounted cost: 2.88623046875ms
{__ob__: Observer}
end created
created cost: 9.76611328125ms

Эта сторона показывает, что функция await действительно приостановлена, а обратный вызов добавлен в очередь задач, ожидая выполнения после выполнения кода основного потока.  

Тогда почемуexp-01Есть ли в коде какой-то уровень синхронизации? !

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

посмотриexp-01печать:

{__ob__: Observer}
end created
created cost: 3171.545166015625ms

end createdЭтот принт является кодом основного потока. Если это общий асинхронный запрос, этот принт должен быть в{__ob__: Observer}Печатается йо перед этим предложением.Что касается того, почему так происходит, то здесь особого разбора нет, просто погуглите сами!

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

Но перед очисткой этого механизма у вас наверняка есть две догадки:

  1. При срабатывании асинхронного кода логика обработки будет добавлена ​​в очередь задач;
  2. Как упоминалось выше, логика обработки не будет добавлена ​​в очередь задач, пока ответ асинхронного кода не будет завершен;

На самом деле можно предположить

Характеристики структуры данных очереди: «первым пришел — первым обслужен» (First in First out).

На данный момент, если в основном потоке есть два асинхронных запроса, выполните следующие действия:

// exp-04
syncRequest01(callback01);
syncRequest02(callback02);

Если предположить, что механизм обработки соответствует описанию в первом пункте, то сначала в очередь задач будет добавлен callback01, а затем callback02.

Затем предположим, что время отклика syncRequest01 составляет 10 с, время отклика syncRequest02 — 5 с.

В этот момент вы заметили какое-либо чувство неповиновения?

Какова фактическая производительность асинхронного запроса? Тот, кто быстрее, выполнит обратный вызов первым, верно? Тогда фактическая производительность заключается в том, что callback02 будет выполнен до callback01!

Затем, основываясь на этом факте, посмотрите на приведенные выше предположения (выполнится callback01)~

Ok! Интерлюдия закончилась!

решение

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

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

Далее нам нужно обсудить решения!

Встроенная защита маршрутизацииУзнать о! ?

beforeRouteEnter

beforeRouteUpdate (новое в версии 2.2)

beforeRouteLeave

Здесь необходимо использовать следующие средства защиты маршрутизации:beforeRouterEnter, сначала посмотрите на код:

// exp-05
const storage = {};
export default {
  beforeRouteEnter(to, from, next) {
    showLoading();
    getList()
      .then((resp) => {
        hideLoading();
        storage.list = resp.data;
        next();
      })
      .catch((error) => {
        hideLoading();
        // handle error
      });
  },

  mounted() {
    let endTime;
    const startTime = Date.now();
    console.log(`start mounted: ${startTime}ms`);
    console.log(storage.list.rows);
    endTime = Date.now();
    console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`);
  },
};

охранник маршрутаbeforeRouterEnter, после срабатывания этого хука основной поток будет заблокирован, а страница останется приостановленной до вызоваbeforeRouterEnterфункция обратного вызоваnext, перейдет к маршруту, чтобы отобразить новый компонент маршрута.

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

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

С этой целью вexp-05в я звонил до и после, пожалуйста, заполнитеshowLoading()а такжеhideLoading()чтобы страницаkeep-alive.

Эта загрузка для обработки приостановленной анимации заставляет вас думать о том, что написать? Правильно, страница перехода github ниже - это маленькая синяя полоса вверху

Подумайте об этом, это немного КРУТО, конечно, есть еще много реализаций для улучшения взаимодействия с пользователем, таких как полноэкранная загрузка, загрузка с помощью кнопок и т. д.

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

Следовательно, вместо того, чтобы иметь очень сильную зависимость от данных, данные должны быть захвачены в ловушку маршрута, чтобы пользователь мог перейти на целевую страницу «быстрее». Чтобы избежать исключений, создаваемых страницей для зависимостей данных (предположительноundefined of xxx), мы можем сделать некоторые пресеты для исходных данных, напримерexp-01средняя параthis.list.rowsзависимости, мы можем установитьthis.list:

list: {
  rows: []
}

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

Пересмотренное заявление

До модификации исходное решение было:

// exp-05
export default {
  beforeRouteEnter(to, from, next) {
    this.showLoading();
    this.getList()
      .then((resp) => {
        this.hideLoading();
        this.list = resp.data;
        next();
      })
      .catch((error) => {
        this.hideLoading();
        // handle error
      });
  },

  mounted() {
    let endTime;
    const startTime = Date.now();
    console.log(`start mounted: ${startTime}ms`);
    console.log(this.list.rows);
    endTime = Date.now();
    console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`);
  },
};

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

  1. beforeRouteEnterКонтекст текущего компонента недоступен в теле функцииnext(это ссылка на функцию), используйтеnextПараметры обратного вызова для этой функцииvm(next(vm => {}))серединаvmдля доступа к контексту текущего компонента;
  2. несмотря на то чтоvmВы можете получить доступ к контексту компонента, но есть проблема, которую вы передаетеvmсделатьget/set,этоget/setдействие находится вbeforeCreate,created,beforeMount,mountedПосле выполнения этих хуков ~ в этом случаеbeforeRouterEnterБлокирующий эффект в основном отменен!

резюме

дляexp-01Нельзя сказать, что он не прав или плохо. Все зависит от того, что есть наша цель. Если это просто для обеспечения порядка выполнения нескольких асинхронных функций,exp-01Нет ничего плохого в том, чтобы написать, так что нет такой вещи, как async / a ждать нельзя использовать на маршрутизационных крючках!

это просто инструмент!

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