이 기사의 내용은 js의 프로세스 제어: Callbacks&Promises&Async/Awai 분석에 관한 것입니다. 필요한 친구들이 참고할 수 있기를 바랍니다.
JavaScript는 종종 _asynchronous_이라고 주장합니다. 그게 무슨 뜻이에요? 발달에 어떤 영향을 미치나요? 최근 몇 년 동안 이러한 접근 방식이 어떻게 바뀌었나요?
다음 코드를 고려하세요:
result1 = doSomething1(); result2 = doSomething2(result1);
대부분의 언어는 동기화된 모든 줄을 처리합니다. 첫 번째 줄이 실행되어 결과를 반환합니다. 두 번째 줄은 첫 번째 줄이 완료된 후 시간이 얼마나 걸리더라도 실행됩니다.
JavaScript는 단일 처리 스레드에서 실행됩니다. 브라우저 탭에서 실행하는 동안 페이지의 DOM에 대한 변경 사항이 병렬 스레드에서 발생하지 않기 때문에 다른 모든 스레드가 중지됩니다. 다른 스레드가 하위 노드를 추가하려고 시도하는 동안 한 스레드를 다른 URL로 리디렉션하는 것은 위험합니다.
이것은 사용자에게 분명합니다. 예를 들어 JavaScript는 버튼 클릭을 감지하고 계산을 실행하며 DOM을 업데이트합니다. 완료되면 브라우저는 대기열의 다음 항목을 자유롭게 처리할 수 있습니다.
(참고: PHP와 같은 다른 언어도 단일 스레드를 사용하지만 Apache와 같은 다중 스레드 서버에서 관리할 수 있습니다. 동일한 PHP 런타임 페이지에 대한 두 개의 동시 요청은 격리된 인스턴스를 실행하는 두 개의 스레드를 시작할 수 있습니다. )
단일 스레딩에 문제가 발생합니다. JavaScript가 "느린" 프로세스(예: 브라우저의 Ajax 요청 또는 서버의 데이터베이스 작업)를 호출하면 어떻게 되나요? 이 작업은 몇 초 또는 몇 분이 걸릴 수 있습니다. 응답을 기다리는 동안 브라우저가 잠겨 있습니다. 서버에서 Node.js 애플리케이션은 사용자 요청을 더 이상 처리할 수 없습니다.
해결책은 비동기 처리입니다. 완료를 기다리는 대신 결과가 준비되면 프로세스에 다른 함수를 호출하라는 지시가 전달됩니다. 이를 콜백이라고 하며 비동기 함수에 매개변수로 전달됩니다. 예:
doSomethingAsync(callback1); console.log('finished'); // call when doSomethingAsync completes function callback1(error) { if (!error) console.log('doSomethingAsync complete'); }
doSomethingAsync()는 콜백 함수를 인수로 받아들입니다(함수에 대한 참조만 전달되므로 오버헤드가 거의 없습니다). doSomethingAsync()가 얼마나 오래 걸리는지는 중요하지 않습니다. 우리가 아는 것은 callback1()이 미래의 어느 시점에 실행된다는 것뿐입니다. 콘솔에 다음이 표시됩니다.
finished doSomethingAsync complete
일반적으로 콜백은 비동기 함수에 의해서만 호출될 수 있습니다. 따라서 간결한 익명 인라인 함수를 사용할 수 있습니다.
doSomethingAsync(error => { if (!error) console.log('doSomethingAsync complete'); });
콜백 함수를 중첩하면 일련의 두 개 이상의 비동기 호출을 연속적으로 완료할 수 있습니다. 예:
async1((err, res) => { if (!err) async2(res, (err, res) => { if (!err) async3(res, (err, res) => { console.log('async1, async2, async3 complete.'); }); }); });
불행하게도 이것은 악명 높은 개념인 Callback Hell을 소개합니다(http://callbackhell.com/)! 코드는 읽기 어렵고 오류 처리 논리가 추가되면 코드가 더 나빠집니다.
클라이언트 측 코딩에서는 콜백 지옥이 비교적 드뭅니다. Ajax 호출을 하고 DOM을 업데이트하고 애니메이션이 완료될 때까지 기다리는 경우 2~3단계까지 진행될 수 있지만 일반적으로 여전히 관리가 가능합니다.
운영 체제나 서버 프로세스에 따라 상황이 다릅니다. Node.js API 호출은 파일 업로드를 수신하고, 여러 데이터베이스 테이블을 업데이트하고, 로그에 쓰고, 응답을 보내기 전에 추가 API 호출을 수행할 수 있습니다.
ES2015(ES6)에서는 Promise를 도입했습니다. 콜백을 계속 사용할 수 있지만 Promise는 비동기 명령을 연속적으로 실행할 수 있도록 더 깔끔한 구문을 제공합니다(자세한 내용).
Promise 기반 실행을 활성화하려면 비동기 콜백 기반 함수가 Promise 객체를 즉시 반환하도록 변경해야 합니다. 이 promise 개체는 미래의 특정 시점에 두 가지 함수(인수로 전달됨) 중 하나를 실행합니다.
resolve: 핸들러가 성공적으로 완료될 때 실행되는 콜백 함수
reject: 다음과 같은 경우 실행되는 선택적 콜백 함수 실패가 발생합니다.
아래 예에서 데이터베이스 API는 콜백 함수를 허용하는 connect() 메서드를 제공합니다. 외부 asyncDBconnect() 함수는 새 Promise를 즉시 반환하고 연결이 설정되거나 실패한 후 해결() 또는 거부()를 실행합니다.
const db = require('database'); // connect to database function asyncDBconnect(param) { return new Promise((resolve, reject) => { db.connect(param, (err, connection) => { if (err) reject(err); else resolve(connection); }); }); }
Node.js 8.0+는 콜백 기반 함수를 수행하는 util.promisify() 유틸리티를 제공합니다. 약속 기반 대안으로 변환되었습니다. 몇 가지 조건이 있습니다.
콜백을 비동기 함수의 마지막 인수로 전달합니다.
콜백 함수는 오류를 가리킨 다음 값 인수가 와야 합니다.
예:
// Node.js: promisify fs.readFile const util = require('util'), fs = require('fs'), readFileAsync = util.promisify(fs.readFile); readFileAsync('file.txt');
다양한 클라이언트 라이브러리도 promisify 옵션을 제공하지만 몇 가지 직접 만들 수도 있습니다.
// promisify a callback function passed as the last parameter // the callback function must accept (err, data) parameters function promisify(fn) { return function() { return new Promise( (resolve, reject) => fn( ...Array.from(arguments), (err, data) => err ? reject(err) : resolve(data) ) ); } } // example function wait(time, callback) { setTimeout(() => { callback(null, 'done'); }, time); } const asyncWait = promisify(wait); ayscWait(1000);
Promise를 반환하는 모든 것은 .then()에 정의된 대로 시작할 수 있습니다. 메소드 일련의 비동기 함수 호출입니다. 각각에는 이전 솔루션의 결과가 전달됩니다.
asyncDBconnect('http://localhost:1234') .then(asyncGetSession) // passed result of asyncDBconnect .then(asyncGetUser) // passed result of asyncGetSession .then(asyncLogAccess) // passed result of asyncGetUser .then(result => { // non-asynchronous function console.log('complete'); // (passed result of asyncLogAccess) return result; // (result passed to next .then()) }) .catch(err => { // called on any reject console.log('error', err); });
동기 함수는 .then() 블록에서도 실행될 수 있습니다. 반환된 값은 다음 .then()에 전달됩니다(있는 경우).
.catch() 메서드는 이전 거부가 트리거될 때 호출되는 함수를 정의합니다. 이 시점에서 .then() 메서드는 더 이상 실행되지 않습니다. 체인 전체에서 여러 .catch() 메서드를 사용하여 다양한 오류를 잡을 수 있습니다.
ES2018에서는 결과에 관계없이 최종 논리(예: 정리, 데이터베이스 연결 닫기 등)를 실행하는 .finally() 메서드를 도입했습니다. 현재 Chrome 및 Firefox에서만 지원되지만 기술 위원회 39에서는 .finally() 폴리필을 출시했습니다.
function doSomething() { doSomething1() .then(doSomething2) .then(doSomething3) .catch(err => { console.log(err); }) .finally(() => { // tidy-up here! }); }
Promise .then()方法一个接一个地运行异步函数。如果顺序无关紧要 - 例如,初始化不相关的组件 - 同时启动所有异步函数并在最后(最慢)函数运行解析时结束更快。
这可以通过Promise.all()来实现。它接受一组函数并返回另一个Promise。例如:
Promise.all([ async1, async2, async3 ]) .then(values => { // array of resolved values console.log(values); // (in same order as function array) return values; }) .catch(err => { // called on any reject console.log('error', err); });
如果任何一个异步函数调用失败,则Promise.all()立即终止。
Promise.race()与Promise.all()类似,只是它会在first Promise解析或拒绝后立即解析或拒绝。只有最快的基于Promise的异步函数才能完成:
Promise.race([ async1, async2, async3 ]) .then(value => { // single value console.log(value); return value; }) .catch(err => { // called on any reject console.log('error', err); });
Promises 减少了回调地狱但引入了别的问题。
教程经常没有提到_整个Promise链是异步的。使用一系列promise的任何函数都应返回自己的Promise或在最终的.then(),. catch()或.finally()方法中运行回调函数。
学习基础知识至关重要。
Promises 可能令人生畏,因此ES2017引入了async and await。 虽然它可能只是语法糖,它使Promise更完善,你可以完全避免.then()链。 考虑下面的基于Promise的示例:
function connect() { return new Promise((resolve, reject) => { asyncDBconnect('http://localhost:1234') .then(asyncGetSession) .then(asyncGetUser) .then(asyncLogAccess) .then(result => resolve(result)) .catch(err => reject(err)) }); } // run connect (self-executing function) (() => { connect(); .then(result => console.log(result)) .catch(err => console.log(err)) })();
用这个重写一下async/await:
外部函数必须以async语句开头
对异步的基于Promise的函数的调用必须在await之前,以确保在下一个命令执行之前完成处理。
async function connect() { try { const connection = await asyncDBconnect('http://localhost:1234'), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return log; } catch (e) { console.log('error', err); return null; } } // run connect (self-executing async function) (async () => { await connect(); })();
await有效地使每个调用看起来好像是同步的,而不是阻止JavaScript的单个处理线程。 此外,异步函数总是返回一个Promise,因此它们可以被其他异步函数调用。
async/await 代码可能不会更短,但有相当大的好处:
1、语法更清晰。括号更少,错误更少。
2、调试更容易。可以在任何await语句上设置断点。
3、错误处理更好。try / catch块可以与同步代码一样使用。
4、支持很好。它在所有浏览器(IE和Opera Mini除外)和Node 7.6+中都得到了支持。
但是并非所有都是完美的......
async / await仍然依赖于Promises,它最终依赖于回调。你需要了解Promises是如何工作的,并且没有Promise.all()和Promise.race()的直接等价物。并且不要忘记Promise.all(),它比使用一系列不相关的await命令更有效。
在某些时候,您将尝试调用异步函数中的同步循环。例如:
async function process(array) { for (let i of array) { await doSomething(i); } }
它不会起作用。这也不会:
async function process(array) { array.forEach(async i => { await doSomething(i); }); }
循环本身保持同步,并且总是在它们的内部异步操作之前完成。
ES2018引入了异步迭代器,它与常规迭代器一样,但next()方法返回Promise。因此,await关键字可以与for循环一起用于串行运行异步操作。例如:
async function process(array) { for await (let i of array) { doSomething(i); } }
但是,在实现异步迭代器之前,最好将数组项映射到异步函数并使用Promise.all()运行它们。例如:
const todo = ['a', 'b', 'c'], alltodo = todo.map(async (v, i) => { console.log('iteration', i); await processSomething(v); }); await Promise.all(alltodo);
这具有并行运行任务的好处,但是不可能将一次迭代的结果传递给另一次迭代,并且映射大型数组可能在性能消耗上是很昂贵。
如果省略任何await失败的try / catch,async函数将以静默方式退出。如果您有一组很长的异步await命令,则可能需要多个try / catch块。
一种替代方案是高阶函数,它捕获错误,因此try / catch块变得不必要(thanks to @wesbos for the suggestion):
async function connect() { const connection = await asyncDBconnect('http://localhost:1234'), session = await asyncGetSession(connection), user = await asyncGetUser(session), log = await asyncLogAccess(user); return true; } // higher-order function to catch errors function catchErrors(fn) { return function (...args) { return fn(...args).catch(err => { console.log('ERROR', err); }); } } (async () => { await catchErrors(connect)(); })();
但是,在应用程序必须以与其他错误不同的方式对某些错误做出反应的情况下,此选项可能不实用。
尽管有一些陷阱,async / await是JavaScript的一个优雅补充。
异步编程是一项在JavaScript中无法避免的挑战。回调在大多数应用程序中都是必不可少的,但它很容易陷入深层嵌套的函数中。
Promises 抽象回调,但有许多语法陷阱。 转换现有函数可能是一件苦差事,而.then()链仍然看起来很混乱。
幸运的是,async / await提供了清晰度。代码看起来是同步的,但它不能独占单个处理线程。它将改变你编写JavaScript的方式!
相关推荐:
整理Javascript流程控制语句学习笔记_javascript技巧
JavaScript中使用Callback控制流程介绍_javascript技巧
위 내용은 js의 프로세스 제어: 콜백 및 약속 및 비동기/Awai 분석의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!