이 글에서는 Koa에서 비동기 콜백의 동기 작성 원리를 연구합니다. 마찬가지로 관리 기능도 구현합니다. 동기 작성을 통해 비동기 콜백 함수를 작성할 수 있습니다.
1. 콜백 피라미드와 이상적인 솔루션
우리 모두는 JavaScript가 단일 스레드 비동기 비차단 언어라는 것을 알고 있습니다. 비동기 비차단은 확실히 장점 중 하나이지만, 많은 비동기 작업에는 필연적으로 많은 수의 콜백 함수가 포함됩니다. 특히 비동기 작업이 중첩되면 콜백 피라미드 문제가 발생하여 코드 가독성이 매우 떨어집니다. 예를 들면 다음과 같습니다.
var fs = require('fs'); fs.readFile('./file1', function(err, data) { console.log(data.toString()); fs.readFile('./file2', function(err, data) { console.log(data.toString()); }) })
이 예제에서는 두 파일의 내용을 차례로 읽어서 인쇄합니다. file2의 읽기는 file1의 읽기가 완료된 후에 수행되어야 하므로 작업이 수행되어야 합니다. file1을 읽은 후 가져온 콜백 함수에서 실행됩니다. 이는 일반적인 콜백 중첩이며 실제 프로그래밍에서는 더 많은 중첩 수준이 있을 수 있습니다.
우리가 상상하는 더 우아한 작성 방식은 다음과 같이 동기적으로 보이지만 실제로는 비동기적인 작성 방식이어야 합니다.
var data; data = readFile('./file1'); //下面的代码是第一个readFile执行完毕之后的回调部分 console.log(data.toString()); //下面的代码是第二个readFile的回调 data = readFile('./file2'); console.log(data.toString());
이렇게 작성하면 콜백을 완전히 피할 수 있습니다. 지옥. 실제로 koa를 사용하면 다음과 같은 방식으로 비동기 콜백 함수를 작성할 수 있습니다.
var koa = require('koa'); var app = koa(); var request=require('some module'); app.use(function*() { var data = yield request('http://www.baidu.com'); //以下是异步回调部分 this.body = data.toString(); }) app.listen(3000);
그렇다면 koa를 그토록 마법처럼 만드는 것은 정확히 무엇일까요?
2. Generator는 비동기 콜백 동기 쓰기 구현을 위해 협력합니다
이전 기사에서 언급했듯이 Generator는 "중단점"과 유사한 효과를 갖는다는 것이 핵심입니다. 항복을 만나면 일시 중지하고 항복 후 함수에 제어권을 넘겨주고 다음에 반환될 때 실행을 계속합니다.
위의 koa 예시에서는 Yield 후에 어떤 객체든 사용할 수 있는 것이 아닙니다! 특정 유형이어야 합니다. co 함수에서는 promise, 썽크 함수 등을 지원할 수 있습니다.
오늘 글에서는 Promise를 예로 들어 Generator와 Promise를 사용하여 비동기 동기화를 구현하는 방법을 분석하고 살펴보겠습니다.
여전히 분석을 위해 파일을 읽는 첫 번째 예를 사용하고 있습니다. 먼저 파일 읽기 기능을 변환하고 이를 Promise 객체로 캡슐화해야 합니다.
var fs = require('fs'); var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //下面是readFile使用的示例 var tmp = readFile('./file1'); tmp.then(function(data) { console.log(data.toString()); })
Promise 사용과 관련하여 익숙하지 않은 경우 다음 구문을 살펴보세요. es6. (가까운 장래에 es5 구문을 사용하여 기본 기능으로 Promise 객체를 구현하는 방법을 가르치는 기사도 작성할 예정이니 계속 지켜봐 주시기 바랍니다^_^)
간단히 말하면 Promise는 콜백 함수를 구현할 수 있습니다. promise.then(콜백) 형식으로 작성됩니다. 하지만 우리의 목표는 생성기와 협력하여 실제로 매끄럽고 동기화된 쓰기를 달성하는 것입니다. 협력 방법은 다음 코드를 참조하세요.
var fs = require('fs'); var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //将读文件的过程放在generator中 var gen = function*() { var data = yield readFile('./file1'); console.log(data.toString()); data = yield readFile('./file2'); console.log(data.toString()); } //手动执行generator var g = gen(); var another = g.next(); //another.value就是返回的promise对象 another.value.then(function(data) { //再次调用g.next从断点处执行generator,并将data作为参数传回 var another2 = g.next(data); another2.value.then(function(data) { g.next(data); }) })
위 코드에서는 생성기에서 readFile을 생성합니다. 명령문 코드는 항복 후 코드에 작성됩니다. 완전히 동기식이며 기사 시작 부분에서 아이디어를 구현합니다.
yield 후에 우리가 얻는 것은 또 다른 것입니다. value는 promise 객체입니다. then 문을 사용하여 콜백 함수를 정의할 수 있습니다. 함수의 내용은 읽은 데이터를 생성기에 반환하고 계속하는 것입니다. 생성기는 중단점에서 실행됩니다.
기본적으로 이것이 비동기 콜백 동기화의 핵심 원리입니다. 사실 Python에 익숙하신 분이라면 기본적으로 생성기를 사용하여 구현하는 Python에 "코루틴"이라는 개념이 있다는 것을 아실 것입니다(I es6의 생성기가 파이썬에서 차용된 것이 아닌가 하는 의심이 듭니다~)
그러나 우리는 여전히 위의 코드를 수동으로 실행합니다. 따라서 이전 기사와 마찬가지로 생성기 프로세스가 자동으로 실행될 수 있도록 관리하는 실행 기능도 구현해야 합니다!
3. 동기화 콜백 함수 자동 실행: 실행 함수 작성
생성기를 수동으로 실행하는 이전 코드 부분을 주의 깊게 관찰하면 이 패턴도 찾을 수 있습니다. 대신 재귀 함수를 직접 작성할 수 있습니다.
var run=function(gen){ var g; if(typeof gen.next==='function'){ g=gen; }else{ g=gen(); } function next(data){ var tmp=g.next(data); if(tmp.done){ return ; }else{ tmp.value.then(next); } } next(); }
함수는 생성기를 수신하고 그 안의 비동기 실행이 자동으로 실행되도록 허용합니다. 이 실행 함수를 사용하여 이전 비동기 코드가 자동으로 실행되도록 합니다.
var fs = require('fs'); var run = function(gen) { var g; if (typeof gen.next === 'function') { g = gen; } else { g = gen(); } function next(data) { var tmp = g.next(data); if (tmp.done) { return; } else { tmp.value.then(next); } } next(); } var readFile = function(fileName) { return new Promise(function(resolve, reject) { fs.readFile(fileName, function(err, data) { if (err) { reject(err); } else { resolve(data); } }) }) } //将读文件的过程放在generator中 var gen = function*() { var data = yield readFile('./file1'); console.log(data.toString()); data = yield readFile('./file2'); console.log(data.toString()); } //下面只需要将gen放入run当中即可自动执行 run(gen);
执行上述代码,即可看到终端依次打印出了file1和file2的内容。
需要指出的是,这里的run函数为了简单起见只支持promise,而实际的co函数还支持thunk等。
这样一来,co函数的两大功能基本就完整介绍了,一个是洋葱模型的流程控制,另一个是异步同步化代码的自动执行。在下一篇文章中,我将带大家对这两个功能进行整合,写出我们自己的一个co函数!