предисловие
Я видел использование 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 => ...
Я предполагаю, что есть две причины для написания приведенного выше кода:
- Существует сильная зависимость от данных, возвращаемых асинхронным кодом, поэтому есть надежда, что созданный контент (включая функцию обратного вызова асинхронного кода) должен быть выполнен до завершения вызова контента смонтированного хука;
- Просто хочу, чтобы асинхронный код функции ловушки выполнялся в том порядке, в котором он был написан (
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}Печатается йо перед этим предложением.Что касается того, почему так происходит, то здесь особого разбора нет, просто погуглите сами!
Кроме того, вот небольшая интерлюдия, вы должны заметить, что я подчеркивал, что время добавления функции обратного вызова в очередь задач наступает после завершения ответа, и это правда!
Но перед очисткой этого механизма у вас наверняка есть две догадки:
- При срабатывании асинхронного кода логика обработки будет добавлена в очередь задач;
- Как упоминалось выше, логика обработки не будет добавлена в очередь задач, пока ответ асинхронного кода не будет завершен;
На самом деле можно предположить
Характеристики структуры данных очереди: «первым пришел — первым обслужен» (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`); }, };
После того, как большие ребята в комментариях научили меня быть человеком, я внес изменения в следующие два пункта:
-
beforeRouteEnterКонтекст текущего компонента недоступен в теле функцииnext(это ссылка на функцию), используйтеnextПараметры обратного вызова для этой функцииvm(next(vm => {}))серединаvmдля доступа к контексту текущего компонента; - несмотря на то что
vmВы можете получить доступ к контексту компонента, но есть проблема, которую вы передаетеvmсделатьget/set,этоget/setдействие находится вbeforeCreate,created,beforeMount,mountedПосле выполнения этих хуков ~ в этом случаеbeforeRouterEnterБлокирующий эффект в основном отменен!
резюме
дляexp-01Нельзя сказать, что он не прав или плохо. Все зависит от того, что есть наша цель. Если это просто для обеспечения порядка выполнения нескольких асинхронных функций,exp-01Нет ничего плохого в том, чтобы написать, так что нет такой вещи, как async / a ждать нельзя использовать на маршрутизационных крючках!
это просто инструмент!