본문으로 건너뛰기

Generator 에서 Async function 으로

무료2018-11-03#JS#JavaScript async#js async function#async function polyfill#JS异步函数#JS异步函数是语法糖

18 행의 코드로 Async function 특성을 구현

서문에

비동기 함수说到,不由得想起 [Wind.js](/articles/向 windjs 致敬-node 异步流程控制 4/)、以及老趙의 遠見:

Wind.js 는 JavaScript 비동기 프로그래밍 영역에서 절대로 이노베이션이며,可谓前無来者。친구가 평가하여「Wind.js 를 보기 전에,진짜로 이는 실현 불가능하다고 생각했다」고,Wind.js 는 실제로는 라이브러리 형식으로 JavaScript 언어를「보수」한 것이며,也正是 이 원인으로,JavaScript 비동기 프로그래밍 체험이 질적 비약을 획득할 수 있었다. ——2012 년 7 월

ES2017 의 async&await 는 promise, generator 에서一路转转而来,Wind 는 이미 6 년 전에 이 날을 보았고,미리 비전을 실현했다

一.yield 와 await

왜 Async function 은 [Promise](/articles/完全理解 promise/)、[Generator](/articles/generator(生成器)-es6 笔记 2/) 에서一路 왔다고 말하는가?

왜냐하면 비동기 함수는 Generator 특성과千絲万縷의 관계가 있기 때문이며,예를 들어,의미적으로 모두 일시 정지의 의미가 있음:

  • yield:양보,좀 쉬어서 숨을 토함

  • await:교도마대(잠깐 기다려)

먼저 가장 간단한 시나리오를 비교:

// generator
function* gen() {
  console.log('Do step 1');
  yield 'Until step1 completed';
  console.log('Do step 2');
}
let iter = gen();
iter.next();
iter.next();

// async function
async function f() {
  console.log('Do step 1');
  await 'Until step1 completed';
  console.log('Do step 2');
}
f();

양자의 코드 구조는 유사하며,출력도 유사합니다(2 개의 예로서 따로 실행):

// generator
Do step 1
Do step 2
{value: undefined, done: true}

// async function
Do step 1
Do step 2
Promise?{<resolved>: undefined}

二.일시 정지는?

생성기는 실행 흐름에「숨을 토함」할 수 있게 하고,멈추지 않는 것을 일시 정지할 수 있게 하고,루프를 리팩토링할 수 있게 하고,무한 시퀀스를驾驭할 수 있게 하고,이터레이터를 포장할 수 있게 하고。。。메리트多多

([generator(생성기)_ES6 노트 2](/articles/generator(生成器)-es6 笔记 2/#articleHeader9) 에서 발췌)

그러나 위의 예에서는 일시 정지의 효과가 보이지 않는 것 같습니다. log 를 추가하여,모든 것을 더욱 명확하게 합시다:

// generator
function* gen() {
  console.log('Do step 1');
  yield 'Until step1 completed';
  console.log('Do step 2');
}
let iter = gen();
iter.next();
console.log('generator 抽根儿烟');
iter.next();

// async function
async function f() {
  console.log('Do step 1');
  await 'Until step1 completed';
  console.log('Do step 2');
}
f();
console.log('async function 抽根儿烟');

이번에는 각자의 반환 값에 주목하지 않습니다(위에서 이미 봤습니다)、함께 실행하여,출력 결과는 다음과 같습니다:

Do step 1
generator 抽根儿烟
Do step 2
Do step 1
async function 抽根儿烟
Do step 2

출력에 차이는 없지만,log('xxx 抽根儿烟') 의 위치에 차이가 있습니다

실제 차이는,위의 예에서 Generator 의 실행 프로세스는 순수하게 동기적이며,async function 의 실행 프로세스는 비동기적인 부분을 포함하고 있습니다. Generator 로 기술하는 경우,다음과 같습니다:

// generator 假装 async function
function* gen() {
  console.log('Do step 1');
  yield Promise.resolve('Until step1 completed');
  console.log('Do step 2');
}
let iter = gen();
let step1 = iter.next();
step1.value.then(iter.next.bind(iter));
console.log('generator 假装 async function 抽根儿烟');

// 出力結果
Do step 1
generator 假装 async function 抽根儿烟
Do step 2

三.조금 더,조금 더

더욱 나아가,매우 쉽게 Generator 로 Async function 특성을 구현할 수 있습니다:

function asyncFunction(gen, ...args) {
  return new Promise((resolve, reject) => {
    resolve(safeNext(gen(...args)));
  });
}

function safeNext(iter, last) {
  let step;
  try {
    step = iter.next(last);
  } catch(ex) {
    step = iter.throw(ex);
  }

  return Promise.resolve(step.value)
    .catch(ex => iter.throw(ex).value)
    .then(result => step.done ? result : safeNext(iter, result))
}

P.S. Github repo 주소 ayqy/asyncFunction

试玩一下:

asyncFunction(function* (){
  console.log('Do step 1');
  // Wait 100ms
  let x = yield new Promise((resolve, reject) => {
    setTimeout(resolve.bind(null, 1), 100);
  });
  // 100ms later
  console.log(`Step1 completed, got ${x}`);
  try {
    throw ++x;
  } catch(ex) {
    x = -1;
  }
  console.log(`x = ${x}`);
  x = yield x * 2;
  console.log(`All steps passed, got ${x}`);
  return x;
}).then(result => {
  console.log(`Final result ${result}`);
});
let intervalId = setInterval(console.log.bind(console, 'tick'), 10);
setTimeout(() => {
  clearInterval(intervalId);
}, 100);

다음과 같은 출력이 얻어집니다:

Do step 1
3
⑨tick
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
tick

그 중에서 2 행째의 3setTimeout 의 반환 값(따라서 asyncFunction 중에서는 첫 번째 세그먼트만 동기적으로 실행됨)、3 행째의 출력은 9 회 'tick' 로 90 여 ms 경과한 것을 나타내며,此時 Wait 100ms 가 종료되고,이어서 나머지 부분을 실행하여 종료까지

게다가,눈치채기 어려운상세는,본례 중 나머지 부분의 실행은 interval 콜백에 의해 중단되지 않는 것입니다(간격이 극히 짧아도):

asyncFunction(function* (){
  setTimeout(console.log.bind(console, '#0'), 0)
  console.log('Do step 1');
  // Wait 100ms
  let x = yield new Promise((resolve, reject) => {
    setTimeout(resolve.bind(null, 1), 100);
  });
  setTimeout(console.log.bind(console, '#1'), 0)
  // 100ms later
  console.log(`Step1 completed, got ${x}`);
  setTimeout(console.log.bind(console, '#2'), 0)
  try {
    throw ++x;
  } catch(ex) {
    x = -1;
  }
  setTimeout(console.log.bind(console, '#3'), 0)
  console.log(`x = ${x}`);
  x = yield x * 2;
  setTimeout(console.log.bind(console, '#4'), 0)
  console.log(`All steps passed, got ${x}`);
  return x;
}).then(result => {
  console.log(`Final result ${result}`);
});

출력 결과는:

Do step 1
Promise?{<pending>}
#0
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
#1
#2
#3
#4

#1, 2, 3, 4 는 마지막에 출력되며,이는 태스크 타입과 관계가 있습니다. 상세는 macrotask 와 microtask 참조

正版 async function 과 비교:

(async function(){
  setTimeout(console.log.bind(console, '#0'), 0)
  console.log('Do step 1');
  // Wait 100ms
  let x = await new Promise((resolve, reject) => {
    setTimeout(resolve.bind(null, 1), 100);
  });
  setTimeout(console.log.bind(console, '#1'), 0)
  // 100ms later
  console.log(`Step1 completed, got ${x}`);
  setTimeout(console.log.bind(console, '#2'), 0)
  try {
    throw ++x;
  } catch(ex) {
    x = -1;
  }
  setTimeout(console.log.bind(console, '#3'), 0)
  console.log(`x = ${x}`);
  x = await x * 2;
  setTimeout(console.log.bind(console, '#4'), 0)
  console.log(`All steps passed, got ${x}`);
  return x;
})().then(result => {
  console.log(`Final result ${result}`);
});

출력은 완전히 일치:

Do step 1
Promise?{<pending>}
#0
Step1 completed, got 1
x = -1
All steps passed, got -2
Final result -2
#1
#2
#3
#4

四.문법 설탕?

기본 문법 형식은 다음과 같습니다:

async function name([param[, param[, ... param]]]) {
  statements
}

2 점을 알 필요가 있습니다:

  • await 키워드는 Async function 내에 나타날 수 있으며,否则 에러

  • Async function 의 반환 값은 Promise

실제,async function 은 합계 4 종류의 형식이 있습니다:

  • 함수 선언:async function foo() {}

  • 함수식:const foo = async function () {};

  • 메서드 정의:let obj = { async foo() {} }

  • 애로우 함수:const foo = async () => {};

예를 들어:

async function fetchJson(url) {
  try {
    console.log('Starting fetch');
    let request = await fetch(url);
    let text = await request.text();
    return JSON.parse(text);
  } catch(error) {
    console.error(error);
  }
}

// test
fetchJson('https://unpkg.com/emoutils/package.json')
  .then(json => console.log(json));
console.log('Fetching...');

출력:

Starting fetch
Fetching...
undefined
{name: "emoutils",?…}

어라,비동기 함수는貌似「비동기」가 아니다、Async function 함수 본체의 첫 번째 세그먼트(첫 번째 await 전의 부분)는 동기적으로 실행되며,다음과 같습니다:

new Promise(resolve => {
  console.log('Starting fetch');
  setTimeout(resolve.bind(null, 'data'), 100);
}).then(data => console.log(data));
console.log('Fetching...');

동様に,매우 쉽게 이 것을 우리의 盗版으로 바꿀 수 있습니다:

asyncFunction(function* fetchJson(url) {
  try {
    console.log('Starting fetch');
    let request = yield fetch(url);
    let text = yield request.text();
    return JSON.parse(text);
  } catch(error) {
    console.error(error);
  }
}, 'https://unpkg.com/emoutils/package.json')
  .then(json => console.log(json));

// test
console.log('Fetching...');

실제 우리는 3 가지의 일을 수행했습니다:

  • 함수 본체를 Generator 로 감싸고,await 를 모두 yield 로 바꿈

  • asyncfunction 사이의 스페이스를 삭제하고 카멜 케이스 명명

  • 파라미터를 Generator 의 뒤로 이동

만약 이 3 가지의 일을 컴파일 변환을 통해 차폐한다면(甚至 단순 매치 치환으로 실현 가능):

function afunction(templateData) {
  const source = templateData;
  // ...一顿操作把上面字符串内容转换成
  let params = ['url'];
  let transformed = `function* fetchJson(url) {
    try {
      console.log('Starting fetch');
      let request = yield fetch(url);
      let text = yield request.text();
      return JSON.parse(text);
    } catch(error) {
      console.error(error);
    }
  }`;

  return function(...args) {
    return asyncFunction(new Function(...params, `return ${transformed}`)(), ...args);
  };
}

async function 특성은 盗版方案에 완전히 取代되었다、문법 형식도 더욱 유사하게 할 수 있습니다:

afunction`(url) => {
  try {
    console.log('Starting fetch');
    let request = await fetch(url);
    let text = await request.text();
    return JSON.parse(text);
  } catch(error) {
    console.error(error);
  }
}`('https://unpkg.com/emoutils/package.json')
  .then(json => console.log(json));

P.S. 여기서 ES2015 태그 템플릿(tagged templates)특성을 응용했습니다. 상세는 [템플릿 문자열_ES6 노트 3](/articles/模板字符串-es6 笔记 3/#articleHeader6) 참조

그렇다면,Async function 은 문법 설탕인가?

认为是할 수 있습니다. Generator 특성이 있은 후,Async function 도呼之欲出입니다(yield 에서 await 로,본질적으로는 더욱 비동기 프로그래밍 체험을 향상시켰을 뿐이며,微改進이라고 할 수 있음):

Internally, async functions work much like generators, but they are not translated to generator functions.

그러나 언어 레벨의 특성 서포트는 유사 컴파일 변환의 대체方案보다 우세가 있으며,퍼포먼스,에러 추적(깨끗한 콜스택),다른 특성과의 심리스한 붙여맞추기(애로우 함수,메서드 정의 등)등의 면에서 구현됩니다

비동기 프로그래밍 체험

프로그래밍 체험에서 보면,Async function 특성이 가져오는 향상은:

  • 동기적 형식으로 비동기 코드를 기술하며,비동기,콜백 등의 개념이淡化되었다

  • try-catch 가 비동기 조작 중의 이상을 캡처할 수 있다

비동기 조작을 포함한 코드 블록이仍然 순서 실행할 수 있는 것은,无疑是 최고의 비동기 프로그래밍 체험입니다:

// callback reqXXX(参数,成功回调,失败回调)
reqLogin(password, reqOrderNo, notFound);
  reqOrderNo(uid, reqOrderDetail, notFound);
    reqOrderDetail(orderNo, render, boom);
      render(data);

// promise
promisifiedReqLogin(password)
  .then(({ uid }) => promisifiedReqOrderNo(uid), notFound)
  .then(({ orderNo }) => promisifiedReqOrderDetail(orderNo), notFound)
  .then(({ data }) => render(data))
  .catch(boom)

// async function
async function renderPage(password) {
  let uid, orderNo;
  try {
    uid = await promisifiedReqLogin(password);
    orderNo = await promisifiedReqOrderNo(uid);
  } catch(ex) {
    notFound(ex);
  }

  let data = await promisifiedReqOrderDetail(orderNo);
  return render(data);
}
renderPage().catch(boom);

data = await fetchData(),仅此而已。콜백의 개념은不复存在,大脑가 비동기 조작에 따라入栈出栈하는 부담을 경감,毕竟

코드는 사람에게 보여주기 위해 쓰는 것이며,부수적으로 머신 위에서 실행할 수 있다

([좋은 JavaScript 를 쓰는](/articles/写好 javascript/) 에서 발췌)

五.연원

至此,우리는 Generator 와 Promise 특성을 사용하여 盗版 Async function 을 실현했습니다.甚至 수고를 들이지 않았습니다(僅 18 행의 코드)

이제回想一下우리는 어떻게 이 2 개의 특성을 조합했는지?或者说,이 2 개의 특성의 어떤 메커니즘에 의존하여,盗版이 쉽게 실현되었는지?

먼저,Async function 을 실현하려면,가장 중요한 특성은 Generator 로,yield 를 통해 순서 실행 흐름을 멈추게 하여,비로소「대기」라고 말할 수 있습니다:

function* infSeq() {
  let i = 0;
  // 不会发生死循环哟,yield 让 while true“停”下来了
  while (true) {
    console.log(i);
    yield i++;
  }
}

// test
let iter = infSeq();
// 出力 0, 1, 2...
iter.next();
iter.next();
iter.next();

「대기」할 수 있게 되었습니다,그럼 누구를 기다리는가?직접 비동기 조작을 기다리는가?어떻게 비동기 조작을 구별하는가?

没错,Promise 가 출번입니다:

// generator 假装 async function
function* gen() {
  console.log('Do step 1');
  yield Promise.resolve('Until step1 completed');
  console.log('Do step 2');
}
let iter = gen();
let step1 = iter.next();
step1.value.then(iter.next.bind(iter));
console.log('generator 假装 async function 抽根儿烟');

next().value 가 Promise 라면,那就肯定是비동기 조작,그것이 완료된 후 next(),이로써 비동기 조작을 1 개やり終えてから 아래의 일을 계속하는 대기를 실현했습니다. 즉 Async function 특성

상위 개념에서 보면,삼자 관계는 다음과 같습니다:

Async function = 調度機(Generator) + 非同期タスク(Promise)

그 중에서,Generator 라는調度機의 작용은:

  • 分片(拆不开怎么等):함수 본체 순서 코드 블록을 수段으로拆分

  • 調度(拆开了怎么执行):외부에서 이러한 플래그먼트의 실행 흐름을 제어,예를 들어 next()throw()

Promise 는비동기 태스크 모델로서,주요 특징은 다음과 같습니다:

  • 状態丢弃:1 회성의 Promise 오브젝트,使い捨て(then() 등은 신 Promise 를 반환)

  • 태스크組合:resolve(promise) 와 같은 방식으로 태스크 체인을 형성할 수 있으며,all()race() 등과 조합하여 그 순서를 제어

  • 에러上抛:버블에 유사한 이상 처리 메커니즘,태스크 체인을 따라 위에 이상을抛出,비동기 태스크의 이상 캡처를 간소화

Generator 는 직접 Promise 를 調度하지 않습니다(調度의 대상은拆分된 플래그먼트),그러나 각段의 실행 결과에 주목하며,결과가 pending Promise 라면,pending 이 아닐 때까지 기다린 후,다음 段의 실행을 제어

따라서,Promise 는ただ脇役,임의의 비동기 태스크 모델로 치환 가능,주요 작용은 Generator 에 여기에 비동기 조작이 있으므로 좀 기다릴 필요가 있다고 고지하는 것:

調度機:(1 段의 코드를 종이 띠에戳て,컴퓨터에塞ぎ,실행 결과를 꺼냄)어라,이것은 뭐다?
비동기 태스크:Hey,나는 비동기 태스크다,아직 끝나지 않았다,끝나면 가르쳐줄게
調度機:좋아,담배를 피운다(아바타가 회색이 됨)
비동기 태스크:끝났다 끝났다,결과는 xxx
調度機:(바로 온라인,다음 段의 코드와 xxx 를 취하여,모두 종이 띠에戳て,컴퓨터에塞ぎ,실행 결과를 꺼냄)어라,이것……尼瑪,어째서 에러가 나온 거야?

댓글

아직 댓글이 없습니다

댓글 작성