>  기사  >  웹 프론트엔드  >  Node.js 비동기 프로그래밍_node.js에서 콜백 및 코드 디자인 패턴 분석

Node.js 비동기 프로그래밍_node.js에서 콜백 및 코드 디자인 패턴 분석

WBOY
WBOY원래의
2016-05-16 15:15:131226검색

NodeJS의 가장 큰 판매 포인트인 이벤트 메커니즘과 비동기식 IO는 개발자에게 투명하지 않습니다. 개발자는 일부 NodeJS 반대자들로부터 비판을 받아온 이 장점을 활용하기 위해 코드를 비동기적으로 작성해야 합니다. 그러나 어쨌든 비동기 프로그래밍은 실제로 NodeJS의 가장 큰 특징입니다. 비동기 프로그래밍을 마스터하지 않고서는 NodeJS를 진정으로 배웠다고 말할 수 없습니다. 이 장에서는 비동기 프로그래밍과 관련된 다양한 지식을 소개합니다.

코드에서 비동기 프로그래밍의 직접적인 표현은 콜백입니다. 비동기 프로그래밍은 콜백에 의존하지만 콜백을 사용한 후에 프로그램이 비동기화된다고 말할 수는 없습니다. 먼저 다음 코드를 살펴보겠습니다.

function heavyCompute(n, callback) {
 var count = 0,
  i, j;

 for (i = n; i > 0; --i) {
  for (j = n; j > 0; --j) {
   count += 1;
  }
 }

 callback(count);
}

heavyCompute(10000, function (count) {
 console.log(count);
});

console.log('hello');

100000000
hello

보시다시피 위 코드의 콜백 함수는 여전히 후속 코드보다 먼저 실행됩니다. JS 자체는 단일 스레드에서 실행되며 코드 조각의 실행이 완료되기 전에 다른 코드를 실행할 수 없으므로 비동기 실행 개념이 없습니다.

그러나 함수가 하는 일이 다른 스레드나 프로세스를 생성하고, JS 메인 스레드와 병렬로 작업을 수행하고, 작업이 완료되면 JS 메인 스레드에 알리는 것이라면 상황은 다릅니다. 다음 코드를 살펴보겠습니다.

setTimeout(function () {
 console.log('world');
}, 1000);

console.log('hello');

hello
world


이번에는 후속 코드 이후에 콜백 함수가 실행되는 것을 볼 수 있습니다. 위에서 언급했듯이 JS 자체는 단일 스레드이므로 비동기적으로 실행할 수 없습니다. 따라서 setTimeout과 같은 JS 사양 외부의 실행 환경에서 제공하는 특수 기능은 병렬 스레드를 생성하고 즉시 반환하는 것이라고 생각할 수 있습니다. JS 마스터에서 프로세스는 병렬 프로세스로부터 알림을 받은 후 후속 코드를 실행하고 콜백 함수를 실행할 수 있습니다. setTimeout 및 setInterval과 같은 일반적인 함수 외에도 이러한 함수에는 fs.readFile과 같은 NodeJS에서 제공하는 비동기 API도 포함됩니다.

또한 JS가 단일 스레드에서 실행된다는 사실로 돌아가서 JS는 코드 조각을 실행하기 전에 콜백 함수를 포함한 다른 코드를 실행할 수 없다고 판단합니다. 즉, 병렬 스레드가 작업을 완료하고 JS 메인 스레드에 콜백 함수를 실행하라고 통보하더라도 JS 메인 스레드가 유휴 상태가 될 때까지 콜백 함수는 실행을 시작하지 않습니다. 다음은 그러한 예입니다.

function heavyCompute(n) {
 var count = 0,
  i, j;

 for (i = n; i > 0; --i) {
  for (j = n; j > 0; --j) {
   count += 1;
  }
 }
}

var t = new Date();

setTimeout(function () {
 console.log(new Date() - t);
}, 1000);

heavyCompute(50000);

8520


보시다시피 JS 메인 스레드가 다른 코드를 실행하느라 1초 후에 호출되기로 되어 있던 콜백 함수의 실제 실행 시간이 크게 지연되었습니다.

코드 디자인 패턴
비동기 프로그래밍에는 동일한 기능을 달성하기 위해 여러 가지 고유한 코드 디자인 패턴이 있으며 동기 모드와 비동기 모드로 작성된 코드는 매우 다릅니다. 몇 가지 일반적인 패턴이 아래에 소개되어 있습니다.

함수 반환값
한 함수의 출력을 다른 함수의 입력으로 사용하는 것은 매우 일반적인 요구 사항입니다. 동기 모드에서 코드는 일반적으로 다음과 같이 작성됩니다.

var output = fn1(fn2('input'));
// Do something.

비동기 모드에서는 함수 실행 결과를 반환값으로 전달하지 않고 콜백 함수를 통해서 전달하기 때문에 일반적으로 코드는 다음과 같은 방식으로 작성됩니다.

fn2('input', function (output2) {
 fn1(output2, function (output1) {
  // Do something.
 });
});

보시다시피 이 메서드는 하나의 콜백 함수 내에 중첩된 하나의 콜백 함수입니다. 너무 많으면 > 모양의 코드를 작성하기 쉽습니다.

배열 탐색
배열을 순회할 때 데이터 멤버에 대한 일부 처리를 순서대로 수행하는 함수를 사용하는 것도 일반적인 요구 사항입니다. 함수가 동기적으로 실행되면 일반적으로 다음 코드가 작성됩니다.

var len = arr.length,
 i = 0;

for (; i < len; ++i) {
 arr[i] = sync(arr[i]);
}

// All array items have processed.

함수가 비동기적으로 실행되는 경우 위 코드는 루프가 끝난 후 모든 배열 구성원이 처리되었다고 보장할 수 없습니다. 배열 멤버를 순차적으로 처리해야 하는 경우 비동기 코드는 일반적으로 다음과 같이 작성됩니다.

(function next(i, len, callback) {
 if (i < len) {
  async(arr[i], function (value) {
   arr[i] = value;
   next(i + 1, len, callback);
  });
 } else {
  callback();
 }
}(0, arr.length, function () {
 // All array items have processed.
}));

보시다시피 위의 코드는 다음 배열 멤버만 전달하고 비동기 함수가 한 번 실행되고 실행 결과를 반환한 후 다음 실행 라운드를 시작합니다. 모든 배열 멤버가 처리될 때까지 후속 코드가 실행됩니다. 콜백을 통해 트리거됩니다.

배열 멤버를 병렬로 처리할 수 있지만 후속 코드에서 실행하기 전에 여전히 모든 배열 멤버를 처리해야 하는 경우 비동기 코드는 다음 형식으로 조정됩니다.

(function (i, len, count, callback) {
 for (; i < len; ++i) {
  (function (i) {
   async(arr[i], function (value) {
    arr[i] = value;
    if (++count === len) {
     callback();
    }
   });
  }(i));
 }
}(0, arr.length, 0, function () {
 // All array items have processed.
}));

보시다시피 비동기 직렬 순회 버전과 비교해 위 코드는 모든 배열 구성원을 병렬로 처리하고 카운터 변수를 사용하여 모든 배열 구성원이 처리된 시점을 확인합니다.

예외 처리
JS 자체에서 제공하는 예외 포착 및 처리 메커니즘인 try..catch..는 동기적으로 실행되는 코드에만 사용할 수 있습니다. 아래는 예시입니다.

function sync(fn) {
 return fn();
}

try {
 sync(null);
 // Do something.
} catch (err) {
 console.log('Error: %s', err.message);
}

Error: object is not a function

보시다시피 예외는 첫 번째 try 문을 만날 때 포착될 때까지 코드 실행 경로를 따라 버블링됩니다. 그러나 비동기 함수는 코드 실행 경로를 방해하므로 비동기 함수 실행 중 및 실행 후에 생성된 예외가 실행 경로가 중단된 위치까지 버블링될 때 try 문이 발견되지 않으면 전역 예외로 발생합니다. . 아래는 예시입니다.

function async(fn, callback) {
 // Code execution path breaks here.
 setTimeout(function () {
  callback(fn());
 }, 0);
}

try {
 async(null, function (data) {
  // Do something.
 });
} catch (err) {
 console.log('Error: %s', err.message);
}

/home/user/test.js:4
  callback(fn());
     ^
TypeError: object is not a function
 at null._onTimeout (/home/user/test.js:4:13)
 at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

因为代码执行路径被打断了,我们就需要在异常冒泡到断点之前用 try 语句把异常捕获住,并通过回调函数传递被捕获的异常。于是我们可以像下边这样改造上边的例子。

function async(fn, callback) {
 // Code execution path breaks here.
 setTimeout(function () {
  try {
   callback(null, fn());
  } catch (err) {
   callback(err);
  }
 }, 0);
}

async(null, function (err, data) {
 if (err) {
  console.log('Error: %s', err.message);
 } else {
  // Do something.
 }
});

Error: object is not a function

可以看到,异常再次被捕获住了。在 NodeJS 中,几乎所有异步 API 都按照以上方式设计,回调函数中第一个参数都是 err。因此我们在编写自己的异步函数时,也可以按照这种方式来处理异常,与 NodeJS 的设计风格保持一致。

有了异常处理方式后,我们接着可以想一想一般我们是怎么写代码的。基本上,我们的代码都是做一些事情,然后调用一个函数,然后再做一些事情,然后再调用一个函数,如此循环。如果我们写的是同步代码,只需要在代码入口点写一个 try 语句就能捕获所有冒泡上来的异常,示例如下。

function main() {
 // Do something.
 syncA();
 // Do something.
 syncB();
 // Do something.
 syncC();
}

try {
 main();
} catch (err) {
 // Deal with exception.
}

但是,如果我们写的是异步代码,就只有呵呵了。由于每次异步函数调用都会打断代码执行路径,只能通过回调函数来传递异常,于是我们就需要在每个回调函数里判断是否有异常发生,于是只用三次异步函数调用,就会产生下边这种代码。

function main(callback) {
 // Do something.
 asyncA(function (err, data) {
  if (err) {
   callback(err);
  } else {
   // Do something
   asyncB(function (err, data) {
    if (err) {
     callback(err);
    } else {
     // Do something
     asyncC(function (err, data) {
      if (err) {
       callback(err);
      } else {
       // Do something
       callback(null);
      }
     });
    }
   });
  }
 });
}

main(function (err) {
 if (err) {
  // Deal with exception.
 }
});

可以看到,回调函数已经让代码变得复杂了,而异步方式下对异常的处理更加剧了代码的复杂度。

성명:
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.