Эта статья является четвертой статьей о принципе и реализации асинхронного JS.Первые три статьи:
Прочитайте и поймите исходный код EventEmitter Node.js из модели публикации-подписки.
Написал от руки Promise/A+ и отлично прошел официальные 872 тестовых примера.
В этой статье в основном речь пойдет о применении и принципе реализации Generator, затем мы прочитаем исходный код модуля co и, наконец, упомянем async/await.
Все примеры в этой статье есть на GitHub:GitHub.com/Денис — см....
Generator
Асинхронное программирование всегда было одним из основных направлений JS, и индустрия изучает различные решения, от «ада обратных вызовов» до режима публикации-подписки и Promise, и все они оптимизируют асинхронное программирование. Хотя Promise и так очень хорош, он не попадет в «ад обратных вызовов», но будет серия вложенных слоев, когда слоев станет больше.then, вы не можете записывать напрямую, как синхронный код. Генератор — это решение, представленное ES6 для дальнейшего улучшения асинхронного программирования.Давайте сначала рассмотрим основное использование.
Основное использование
Генератор переводится с китайского как "генератор". На самом деле то, что он хочет сделать, тоже генератор. Если добавить функцию*, она станет функцией-генератором, а результат ее работы вернет объект-итератор, например следующий код:
// gen是一个生成器函数
function* gen() {
let a = yield 1;
let b = yield a + 2;
yield b + 3;
}
let itor = gen(); // 生成器函数运行后会返回一个迭代器对象,即itor。
next
Как указано в спецификации ES6Итераторы должны иметьnextметод, который возвращает объект, имеющийdoneа такжеvalueдва свойства,doneУказывает, было ли выполнено текущее содержимое итератора и выполнениеtrue, иначеfalse,valueПредставляет значение, возвращаемое текущим шагом. существуетgeneratorВ конкретном использовании, каждый раз, когда вы сталкиваетесь сyieldключевые слова приостановят выполнение, когда итераторnext, будуyieldЗначение следующего выражения используется в качестве значения возвращаемого объекта.value, например, результат выполнения приведенного выше генератора выглядит следующим образом:
Мы можем увидеть первую мелодиюnextвозвращает первыйyeildЗначение следующего выражения равно 1.Следует отметить, что весь итератор в настоящее время приостановлен в первую очередьyieldЗдесь дайте переменнуюaПрисваивание не выполняется, и вызывается следующееnextпеременная будет задана, когдаaзадание, затем выполнять до второгоyield. это должно датьaКакое значение вы присваиваете? Из кода,aЗначение должно бытьyieldвозвращаемое значение инструкции, ноyieldСамо по себе возвращаемое значение отсутствует, или возвращаемое значениеundefined, если вы хотите датьaДля задания требуется следующий звонокnextПри передаче вручную мы передаем здесь 4, и 4 будет использоваться как последний разyieldВозвращаемое значение присваиваетсяa:
можно увидеть второйyieldследующее выражениеa + 2Значение равно 6, потому что 4, которое мы передали, используется как предыдущее.yieldвозвращаемое значение , а затем вычислитьa + 2Конечно это 6.
мы продолжимnext, завершите этот итератор:
За картинкой выше следует предыдущая операция, первая на картинкеnextвернутьvalueдаNaNпотому что мы настраиваемnextКогда никакие параметры не передаются, то естьbдляundefined,undefined + 3Просто дляNaNА. последнийnextПо сути тело выполнения функции закончилось, на этот разvalueдолжна быть эта функцияreturnзначение, а потому что мы не написалиreturn, по умолчаниюreturn undefinedДа, после казниdoneОн будет установлен наtrue.
throw
Другой метод итераторовthrow, этот метод может выдать ошибку за пределами тела функции, а затем перехватить ее внутри функции, или приведенный выше пример:
function* gen() {
let a = yield 1;
let b = yield a + 2;
yield b + 3;
}
let itor = gen();
нам не нужно это времяnextвыполнено, непосредственноthrowВыходит ошибка:
Эта ошибка выбрасывается сразу на самый внешний слой, потому что мы ее не поймали, мы можем поймать ее в теле функции и немного изменить:
function* gen() {
try {
let a = yield 1;
let b = yield a + 2;
yield b + 3;
} catch (e) {
console.log(e);
}
}
let itor = gen();
тогда сноваthrowВниз:
Как видно из этого рисунка, ошибка фиксируется в функции, аcatchВнутри есть только одинconsoleСинхронный код, вся функция выполняется прямо до конца, поэтомуdoneсталиtrueНу конечно; естественноcatchВы можете продолжать писать в немyieldзатем используйтеnextвыполнить.
return
Итератор также имеетreturnметод, этот метод очень прост, он напрямую завершит текущий итератор иdoneустановлен вtrue, параметром этого метода является итераторvalue, или приведенный выше пример:
function* gen() {
let a = yield 1;
let b = yield a + 2;
yield b + 3;
}
let itor = gen();
На этот раз мы напрямую вызываемreturn:
yield*
просто понять,yield*Это вызов другого генератора в генераторе, но он не занимаетnext, а непосредственно в вызываемый генератор для запуска.
function* gen() {
let a = yield 1;
let b = yield a + 2;
}
function* gen2() {
yield 10 + 5;
yield* gen();
}
let itor = gen2();
Приведенный выше код мы вызываем в первый разnext, значение естественно10 + 515, затем позвоните во второй разnext, на самом деле пошелyield*, это фактически эквивалентно вызовуgen, затем выполняет свой первыйyield, значение равно 1.
сопрограмма
На самом деле, Generator реализует сопрограммы, а сопрограммы — понятие меньшее, чем потоки. У процесса может быть несколько потоков, а у потока может быть несколько сопрограмм, но в потоке может одновременно выполняться только одна сопрограмма. Это означает, что если текущая сопрограмма может быть выполнена, например синхронный код, то выполнить ее, если текущая сопрограмма не может продолжать выполняться временно, например, это асинхронная операция чтения файла, то приостановить ее, а затем выполнить другую сопрограмму. , когда результат этой сопрограммы возвращается, вы можете продолжать выполнять ее снова.yieldПо сути, это равносильно приостановке текущей задачи, и отсюда начнется следующий вызов. Концепция сопрограмм на самом деле была предложена много лет назад, и многие другие языки имеют свои реализации. Генератор эквивалентен сопрограмме, реализованной в JS.
Асинхронное приложение
Я рассказал об основном использовании Generator, давайте воспользуемся им для обработки асинхронного события. Я по-прежнему использую пример из предыдущей статьи, три сетевых запроса, запрос 3 зависит от результата запроса 2, а запрос 2 зависит от результата запроса 1, если callback используется так:
const request = require("request");
request('https://www.baidu.com', function (error, response) {
if (!error && response.statusCode == 200) {
console.log('get times 1');
request('https://www.baidu.com', function(error, response) {
if (!error && response.statusCode == 200) {
console.log('get times 2');
request('https://www.baidu.com', function(error, response) {
if (!error && response.statusCode == 200) {
console.log('get times 3');
}
})
}
})
}
});
На этот раз мы используем Generator, чтобы решить «ад обратных вызовов»:
const request = require("request");
function* requestGen() {
function sendRequest(url) {
request(url, function (error, response) {
if (!error && response.statusCode == 200) {
console.log(response.body);
// 注意这里,引用了外部的迭代器itor
itor.next(response.body);
}
})
}
const url = 'https://www.baidu.com';
// 使用yield发起三个请求,每个请求成功后再继续调next
const r1 = yield sendRequest(url);
console.log('r1', r1);
const r2 = yield sendRequest(url);
console.log('r2', r2);
const r3 = yield sendRequest(url);
console.log('r3', r3);
}
const itor = requestGen();
// 手动调第一个next
itor.next();
В этом примере мы написали метод запроса в генераторе, этот метод будет инициировать сетевой запрос, и каждый раз, когда сетевой запрос будет успешным, он будет продолжать вызывать следующий для выполнения следующегоyield, и, наконец, вручную настроить один во внешнем слоеnextзапустить этот процесс. На самом деле это похоже на хвостовой вызов, поэтому запись может достичь эффекта, но вrequestGenвнутри ссылки на внешний итераторitor, соединение очень высокое, и его нелегко использовать повторно.
преобразователь функция
Чтобы решить проблему высокой связанности и сложного повторного использования, упомянутую выше, существует функция thunk. Функция thunk немного запутана для понимания, я сначала пишу код, а затем шаг за шагом анализирую порядок его выполнения:
function Thunk(fn) {
return function(...args) {
return function(callback) {
return fn.call(this, ...args, callback)
}
}
}
function run(fn) {
let gen = fn();
function next(err, data) {
let result = gen.next(data);
if(result.done) return;
result.value(next);
}
next();
}
// 使用thunk方法
const request = require("request");
const requestThunk = Thunk(request);
function* requestGen() {
const url = 'https://www.baidu.com';
let r1 = yield requestThunk(url);
console.log(r1.body);
let r2 = yield requestThunk(url);
console.log(r2.body);
let r3 = yield requestThunk(url);
console.log(r3.body);
}
// 启动运行
run(requestGen);
Функция Thunk в этом коде возвращает несколько слоев функций, давайте начнем с ее использования и будем отделять слой за слоем:
-
requestThunkЭто возвращаемое значение операции Thunk, то есть возвращаемое значение первого слоя.request, то есть:function(...args) { return function(callback) { return request.call(this, ...args, callback); // 注意这里调用的是request } } -
runПараметры генератора функций в том, что мы смотрим на то, что он сделал в конце:-
В бегах сначала позвоните генератору и получите итератор
gen, а затем настроитьnextметод и вызовите егоnextметод, чтобы облегчить различие, я называю этот обычай здесьnextдля местныхnext -
местный
nextвызовет генераторnext, генераторnextНа самом деле этоyield requestThunk(url), параметры, которые мы передаемurl, который переходит к методу, который мы использовали ранее, этоyieldвернутьvalueФактически:function(callback) { return request.call(this, url, callback); } -
Проверяем, прошел ли итератор итерацию. Если нет, то продолжаем вызывать функцию на втором шаге.
request, параметры, передаваемые в это время, являются локальнымиnext, местныйnextтакже какrequestфункция обратного вызова. -
Когда эта функция обратного вызова будет выполнена, она снова вызовет
gen.next, так что генератор может продолжать выполняться, и в то же времяgen.nextПараметр является функцией обратного вызоваdata, так что в генератореr1Фактически, он получает возвращаемое значение запроса.
-
Функция Thunk — это такая функция, которая может автоматически выполнять генератор, из-за упаковки функции Thunk мы можем получить ее непосредственно в генераторе, как синхронный код.yieldВозвращаемое значение асинхронного кода.
сомодуль
Модуль co – очень популярный модуль. Он также может автоматически запускать генератор. Его доходность поддерживает thunk и Promise. Давайте сначала рассмотрим его основное использование, а затем проанализируем его исходный код. Официальный гитхаб:github.com/tj/co
основное использование
поддержка
Мы говорили о функции thunk ранее, мы по-прежнему начинаем с функции thunk. В коде по-прежнему используется функция thunk, которую мы написали ранее, но поскольку thunk, поддерживаемый co, представляет собой форму функции, которая получает только функции обратного вызова, нам нужно настроить ее при ее использовании:
// 还是之前的thunk函数
function Thunk(fn) {
return function(...args) {
return function(callback) {
return fn.call(this, ...args, callback)
}
}
}
// 将我们需要的request转换成thunk
const request = require('request');
const requestThunk = Thunk(request);
// 转换后的requestThunk其实可以直接用了
// 用法就是 requestThunk(url)(callback)
// 但是我们co接收的thunk是 fn(callback)形式
// 我们转换一下
// 这时候的baiduRequest也是一个函数,url已经传好了,他只需要一个回调函数做参数就行
// 使用就是这样:baiduRequest(callback)
const baiduRequest = requestThunk('https://www.baidu.com');
// 引入co执行, co的参数是一个Generator
// co的返回值是一个Promise,我们可以用then拿到他的结果
const co = require('co');
co(function* () {
const r1 = yield baiduRequest;
const r2 = yield baiduRequest;
const r3 = yield baiduRequest;
return {
r1,
r2,
r3,
}
}).then((res) => {
// then里面就可以直接拿到前面返回的{r1, r2, r3}
console.log(res);
});
Обещает поддержку
На самом деле, официальный представитель рекомендует, чтобы за yield шел Promise, хотя thunk поддерживается, в будущем он может быть удален. Используя Promise, наш код на самом деле проще писать, просто используйте fetch напрямую, без обёртывания Thunk.
const fetch = require('node-fetch');
const co = require('co');
co(function* () {
// 直接用fetch,简单多了,fetch返回的就是Promise
const r1 = yield fetch('https://www.baidu.com');
const r2 = yield fetch('https://www.baidu.com');
const r3 = yield fetch('https://www.baidu.com');
return {
r1,
r2,
r3,
}
}).then((res) => {
// 这里同样可以拿到{r1, r2, r3}
console.log(res);
});
Анализ исходного кода
Анализ исходного кода в этой статье основан на модуле co версии 4.6.0, исходный код:GitHub.com/recommend/co/blob/…
При внимательном рассмотрении исходного кода не много он найдет в общей сложности более двух строчек, половину всех выходных параметров при тестировании и последующей обработке, он не обнаруживает промис, если его не преобразовать в промис, так что даже если вы yield back pass thunk, он все равно будет конвертирован в Promise-обработку. Преобразование кода промисов относительно независимо и просто, я начал здесь, не подробно описывая здесь, в основном, чтобы рассказать об основных методах.co(gen). Вот упрощенный код, который я оставил выключенным:
function co(gen) {
var ctx = this;
var args = slice.call(arguments, 1);
return new Promise(function(resolve, reject) {
if (typeof gen === 'function') gen = gen.apply(ctx, args);
if (!gen || typeof gen.next !== 'function') return resolve(gen);
onFulfilled();
function onFulfilled(res) {
var ret;
try {
ret = gen.next(res);
} catch (e) {
return reject(e);
}
next(ret);
return null;
}
function onRejected(err) {
var ret;
try {
ret = gen.throw(err);
} catch (e) {
return reject(e);
}
next(ret);
}
function next(ret) {
if (ret.done) return resolve(ret.value);
var value = toPromise.call(ctx, ret.value);
if (value && isPromise(value)) return value.then(onFulfilled, onRejected);
return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, '
+ 'but the following object was passed: "' + String(ret.value) + '"'));
}
});
}
-
Из общей структуры параметр co является генератором, возвращаемое значение — промисом, и почти весь логический код находится в этом промисе, поэтому мы используем then для получения результата при его использовании.
-
В Promise сначала вытащите генератор и выполните его, а затем получите итератор
gen -
вызвать его вручную
onFulfilled, начать итерацию-
onFulfilledполучить параметрres, этот параметр не передается при первом вызове, этот параметр в основном используется для получения возвращаемого к тому времени результата. - тогда позвони
gen.next, обратите внимание, что возвращаемое значение ret имеет вид {value, done}, а затем передайте это ret локальному методу next.
-
-
Затем выполните локальный следующий, параметр, который он получает, представляет собой возвращаемое значение yield {value, done}
- Здесь мы сначала проверяем, завершена ли итерация, и если да, то разрешаем все промис напрямую.
- Значение здесь — это значение выражения после yield, которое может быть преобразователь или обещание
- Преобразовать ценность в обещание
- Возьмите преобразованное обещание и выполните, а успешный обратный вызов будет предыдущим.
onFulfilled
-
давайте посмотрим еще раз
onFulfilled, это второе исполнениеonFulfilled. Параметр res, переданный в это время, является результатом выполнения последнего асинхронного промиса, соответствующий нашей выборке — это данные, которые мы возвращаем, эти данные передаются второмуgen.next, в результате присваивание в нашем коде назначается первомуyieldпредыдущая переменнаяr1. Затем продолжите с локальным next, который на самом деле является выполнением второго асинхронного промиса. Обратный вызов успеха этого обещания продолжает вызыватьgen.next, и так далее, и так далее, покаdoneсталиtrueдо. -
последний взгляд
onRejectedметод, этот метод фактически используется как ветвь ошибки асинхронного промиса, который напрямую вызывается в этой функцииgen.throw, чтобы мы могли использовать его непосредственно в генератореtry...catch...получить ошибку. должен быть в курсеgen.throwЗатем продолжайте звонитьnext(ret), потому что в генератореcatchФилиал может по-прежнему иметьyield, таких как сетевой запрос, сообщенный об ошибке, итератор в это время не обязательно заканчивается.
async/await
Последнее упоминаниеasync/await, сначала посмотрите на использование:
const fetch = require('node-fetch');
async function sendRequest () {
const r1 = await fetch('https://www.baidu.com');
const r2 = await fetch('https://www.baidu.com');
const r3 = await fetch('https://www.baidu.com');
return {
r1,
r2,
r3,
}
}
// 注意async返回的也是一个promise
sendRequest().then((res) => {
console.log('res', res);
});
На первый взгляд, очень ли это похоже на предыдущую версию обещания co?Возвращаемое значение является обещанием, но Генератор заменен наasyncфункция внутри функцииyieldзаменяетсяawait, и внешний слой не нуждается в обертывании, он также может выполняться автоматически. По сути, асинхронная функция — это синтаксический сахар Генератора плюс автоматический исполнитель, который можно понимать как поддержку автоматического выполнения Генератора с языкового уровня. Приведенный выше код фактически эквивалентен обещанию совместной версии.
Суммировать
- Генератор — более современное асинхронное решение, поддерживающее сопрограммы на уровне языка JS.
- Возвращаемое значение Generator является итератором
- Этот итератор необходимо настроить вручную
nextвыполнять один за другимyield -
nextВозвращаемое значение {value, done},valueзначение выражения, следующего за yield -
yieldСам оператор не возвращает значение, следующий вызовnextпараметр будет использоваться как предыдущийyieldвозвращаемое значение инструкции - Генератор сам по себе не может запускаться автоматически, для автоматического запуска нужно внедрять другие решения Как упоминалось ранее
thunkдает решение, когдаcoМодули также являются популярным решением для автоматизации. - Эта идея чем-то похожа на две программы, которые первыми написали локальный метод, этот метод для вызова
gen.next, при этом сам метод будет передан callback-функции или ветке успеха промиса.После асинхронного завершения локальный метод будет продолжать вызываться, а локальный метод будет вызываться снова.gen.next, который повторяется до тех пор, пока итератор не завершит выполнение. -
async/awaitПо сути, это синтаксический сахар Генератора и автоматического исполнителя.Принципы написания и реализации аналогичны режиму обещания модуля co.
В конце статьи спасибо, что потратили свое драгоценное время на чтение этой статьи. Если эта статья немного поможет вам или вдохновит, пожалуйста, не скупитесь на лайки и звезды GitHub. Ваша поддержка является движущей силой для автор продолжать творить.
Добро пожаловать, чтобы обратить внимание на мой общедоступный номербольшой фронт атакиПолучите высококачественные оригиналы впервые~
Цикл статей "Передовые передовые знания":nuggets.capable/post/684490…
Адрес GitHub с исходным кодом из серии статей «Advanced Front-end Knowledge»:GitHub.com/Денис — см....