JavaScript的执行机制在上篇文章中进行了深入的探讨,那么既然是一门单线程语言,如何进行良好体验的异步编程呢
回调函数Callbacks
当程序跑起来时,一般情况下,应用程序(application program)会时常通过API调用库里所预先备好的函数。但是有些库函数(library function)却要求应用先传给它一个函数,好在合适的时候调用,以完成目标任务。这个被传入的、后又被调用的函数就称为回调函数(callback function)。
什么是异步
"调用"在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在"调用"发出后,"被调用者"通过状态、通知来通知调用者,或通过回调函数处理这个调用。异步调用发出后,不影响后面代码的执行。
简单说就是一个任务分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
在异步执行的模式下,每一个异步的任务都有其自己一个或着多个回调函数,这样当前在执行的异步任务执行完之后,不会马上执行事件队列中的下一项任务,而是执行它的回调函数,而下一项任务也不会等当前这个回调函数执行完,因为它也不能确定当前的回调合适执行完毕,只要引它被触发就会执行,
地狱回调阶段
异步最早的解决方案是回调函数,如事件的回调,setInterval/setTimeout中的回调。但是回调函数有一个很常见的问题,就是回调地狱的问题
下面这几种都属于回调
- 事件回调
- Node API
- setTimeout/setInterval中的回调函数
- ajax 请求
异步回调嵌套会导致代码难以维护,并且不方便统一处理错误,不能 try catch会陷入回调地狱
fs.readFile(A, 'utf-8', function(err, data) {
fs.readFile(B, 'utf-8', function(err, data) {
fs.readFile(C, 'utf-8', function(err, data) {
fs.readFile(D, 'utf-8', function(err, data) {
//....
});
});
});
});
ajax(url, () => {
// 处理逻辑
ajax(url1, () => {
// 处理逻辑
ajax(url2, () => {
// 处理逻辑
})
})
})
Promise解决地狱回调阶段
Promise 一定程度上解决了回调地狱的问题,Promise 最早由社区提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。
- Promise存在三个状态(state)pending、fulfilled、rejected
- pending(等待态)为初始态,并可以转化为fulfilled(成功态)和rejected(失败态)
- 成功时,不可转为其他状态,且必须有一个不可改变的值(value)
- 失败时,不可转为其他状态,且必须有一个不可改变的原因(reason)
- new Promise((resolve, reject)=>{resolve(value)}) resolve为成功,接收参数value,状态改变为fulfilled,不可再次改变。
- new Promise((resolve, reject)=>{reject(reason)}) reject为失败,接收参数reason,状态改变为rejected,不可再次改变。
- 若是executor函数报错 直接执行reject();
Promise 是一个构造函数,new Promise 返回一个 promise对象
const promise = new Promise((resolve, reject) => {
// 异步处理
// 处理结束后、调用resolve 或 reject
});
then方法注册 当resolve(成功)/reject(失败)的回调函数
// onFulfilled 参数是用来接收promise成功的值,
// onRejected 参数是用来接收promise失败的原因
//两个回调返回的都是promise,这样就可以链式调用
promise.then(onFulfilled, onRejected);
const promise = new Promise((resolve, reject) => {
resolve('fulfilled'); // 状态由 pending => fulfilled
});
promise.then(result => { // onFulfilled
console.log(result); // 'fulfilled'
}, reason => { // onRejected 不会被调用
})
then方法的链式调用
Promise对象的then方法返回一个新的Promise对象,因此可以通过链式调用then方法。then方法接收两个函数作为参数,第一个参数是Promise执行成功时的回调,第二个参数是Promise执行失败时的回调。两个函数只会有一个被调用,函数的返回值将被用作创建then返回的Promise对象。这两个参数的返回值可以是以下三种情况中的一种:
-
return
了一个值,那么then
返回的 Promise 将会成为接受(resolved
)状态,并且将返回的值作为接受状态的回调函数的参数值。 - 没有返回任何值(,默认返回
undefined
),那么then
返回的 Promise 将会成为接受(resolved
)状态,并且该接受状态的回调函数的参数值为undefined
。 - 抛出(
throw
)一个错误,那么then
返回的 Promise 将会成为拒绝状态,并且将抛出的错误作为拒绝状态的回调函数的参数值。 -
return 另一个 Promise,then方法将根据这个Promise的状态和值创建一个新的Promise对象返回。
- 返回一个已经是接受状态的 Promise,那么
then
返回的 Promise 也会成为接受状态,并且将那个 Promise 的接受状态的回调函数的参数值作为该被返回的Promise的接受状态回调函数的参数值。 - 返回一个已经是拒绝状态的 Promise,那么
then
返回的 Promise 也会成为拒绝状态,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值。 - 返回一个未定状态(
pending
)的 Promise,那么then
返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的。
- 返回一个已经是接受状态的 Promise,那么
解决层层回调问题
//对应上面第一个node读取文件的例子
function read(url) {
return new Promise((resolve, reject) => {
fs.readFile(url, 'utf8', (err, data) => {
if(err) reject(err);
resolve(data);
});
});
}
read(A).then(data => {
return read(B);
}).then(data => {
return read(C);
}).then(data => {
return read(D);
}).catch(reason => {
console.log(reason);
});
//对应第二个ajax请求例子
ajax(url)
.then(res => {
console.log(res)
return ajax(url1)
}).then(res => {
console.log(res)
return ajax(url2)
}).then(res => console.log(res))
可以看到,Promise在一定程度上其实改善了回调函数的书写方式,最明显的一点就是去除了横向扩展,无论有再多的业务依赖,通过多个then(...)来获取数据,让代码只在纵向进行扩展;另外一点就是逻辑性更明显了,将异步业务提取成单个函数,整个流程可以看到是一步步向下执行的,依赖层级也很清晰,最后需要的数据是在整个代码的最后一步获得。
所以,Promise在一定程度上解决了回调函数的书写结构问题,但回调函数依然在主流程上存在,只不过都放到了then(...)里面,和我们大脑顺序线性的思维逻辑还是有出入的。
catch方法
catch() 方法返回一个Promise,并且处理拒绝的情况。它的行为与调用Promise.prototype.then(undefined, onRejected) 相同。 (事实上, calling obj.catch(onRejected) 内部calls obj.then(undefined, onRejected)).
p.catch(onRejected);
p.catch(function(reason) {
// 拒绝
});
onRejected
当Promise 被rejected时,被调用的一个Function。 该函数拥有一个参数:
reason rejection 的原因。
如果 onRejected 抛出一个错误或返回一个本身失败的 Promise , 通过 catch() 返回的Promise 被rejected;否则,它将显示为成功(resolved)。
Promise缺点
- 无法取消 Promise
- 当处于pending状态时,无法得知目前进展到哪一个阶段
- 错误不能被 try catch(try..catch 结构,它只能是同步的,无法用于异步代码模式)
执行f2()
,无法通过try/catch捕获promise.reject,控制台抛出Uncaught (in promise)
function f2() {
try {
Promise.reject('出错了');
} catch(e) {
console.log(e)
}
}
改成await/async后,执行f()
就能在catch中捕获到错误了,并不会抛出Uncaught (in promise)
async function f() {
try {
await Promise.reject('出错了')
} catch(e) {
console.log(e)
}
}
可以这么理解,promise中的错误发生在未来,所以无法现在捕获
生成器Generators/ yield
什么是Generator
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。形式上,Generator 函数是一个普通函数,但是有两个特征。
- 一是,function关键字与函数名之间有一个星号;
- 二是,函数体内部使用yield表达式,定义不同的内部状态
Generator调用方式
Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。
下一步,必须调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
function* foo () {
var index = 0;
while (index < 2) {
yield index++; //暂停函数执行,并执行yield后的操作
}
}
var bar = foo(); // 返回的其实是一个迭代器
console.log(bar.next()); // { value: 0, done: false }
console.log(bar.next()); // { value: 1, done: false }
console.log(bar.next()); // { value: undefined, done: true }
了解Co
可以看到上个例子当中我们需要一步一步去调用next这样也会很麻烦,这时我们可以引入co来帮我们控制
Co是一个为Node.js和浏览器打造的基于生成器的流程控制工具,借助于Promise,你可以使用更加优雅的方式编写非阻塞代码。
Co 函数库约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以跟 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时等同于同步操作)。
说白了就是帮你自动执行你的Generator不用手动调用next
可以简单实现下:
function co(it) {
return new Promise(function (resolve, reject) {
function step(d) {
let { value, done } = it.next(d);
if (!done) {
value.then(function (data) { // 2,txt
step(data)
}, reject)
} else {
resolve(value);
}
}
step();
});
}
比如我们有个生成器函数是r(),直接扔进co里自动执行
function* r() {
let content1 = yield read('1.txt', 'utf8');
let content2 = yield read(content1, 'utf8');
return content2;
}
co(r()).then(function (data) {
console.log(data)
})
解决异步问题
我们可以通过 Generator 函数解决回调地狱的问题,可以把之前的回调地狱例子改写为如下代码:
const co = require('co');
co(
function* read() {
yield readFile(A, 'utf-8');
yield readFile(B, 'utf-8');
yield readFile(C, 'utf-8');
//....
}
).then(data => {
//code
}).catch(err => {
//code
});
function *fetch() {
yield ajax(url, () => {})
yield ajax(url1, () => {})
yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()
终极解决方案Async/ await
async 函数是Generator 函数的语法糖,是对Generator做了进一步的封装。
async函数对 Generator 函数的改进
- 1.
内置执行器
。Generator 函数的执行必须依靠执行器,而 async 函数自带执行器,无需手动执行 next() 方法。 - 2.
更好的语义
。async和await,比起星号和yield,语义更清楚了。async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果。 - 3.
更广的适用性
。co模块约定,yield命令后面只能是 Thunk 函数或 Promise 对象,而async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)。 - 4.
返回值是 Promise
。async 函数返回值是 Promise 对象,比 Generator 函数返回的 Iterator 对象方便,可以直接使用 then() 方法进行调用。
Co+Promise+Generator实现Async
async重点是自带了执行器,相当于把我们要额外做的(写执行器/依赖co模块)都封装了在内部。比如:
async function fn(args) {
// ...
}
等同于:
function fn(args) {
return spawn(function* () {
// ...
});
}
function spawn(genF) { //spawn函数就是自动执行器,跟简单版的思路是一样的,多了Promise和容错处理
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
// 没有结束,递归调用step
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
这样就不难理解为什么await函数会等到后面跟promise在resolve后才会执行,因为可以看出整个代码是在Generator里执行,这样在resolve里调用了gen.next(v),才会往下走。
注意一个细节:await下一行的代码相当注册在await后面跟的promise的.then回调里,这里和事件循环有关后面举例说明
Async特点
- 当调用一个 async 函数时,会返回一个 Promise 对象。
async function async1() {
return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}
- 当这个 async 函数返回一个值时,Promise 的 resolve 方法会负责传递这个值;
- 没有返回值Promise 的 resolve 方法专递undefined
- 当 async 函数抛出异常时,Promise 的 reject 方法也会传递这个异常值。
- async 函数中可能会有 await 表达式,这会使 async 函数暂停执行,等待 Promise 的结果出来,然后恢复async函数的执行并返回解析(resolved)。
- 内置执行器。 Generator 函数的执行必须靠执行器,所以才有了 co 函数库,而 async 函数自带执行器。也就是说,async 函数的执行,与普通函数一模一样,只要一行。
- 更广的适用性。co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise对象。而 async 函数的 await 命令后面则可以是 Promise 或者 原始类型的值(Number,string,boolean,但这时等同于同步操作)
await特点
- await 操作符用于等待一个Promise 对象。它只能在异步函数 async function 中使用。
- [return_value] = await expression;
- await 表达式会暂停当前 async function 的执行,等待 Promise 处理完成。若 Promise 正常处理(fulfilled),其回调的resolve函数参数作为 await 表达式的值,继续执行 async function。
- 另外,如果 await 操作符后的表达式的值不是一个 Promise,则返回该值本身。
- 若 Promise 处理异常(rejected),await 表达式会把 Promise 的异常原因抛出。可以用catch在外部捕获
重点:遇到 await 表达式时,会让 async 函数 暂停执行,等到 await 后面的语句(Promise)状态发生改变(resolved或者rejected)之后,再恢复 async 函数的执行(再之后 await 下面的语句),并返回解析值(Promise的值)
async await 异常处理
let last;
async function throwError() {
await Promise.reject('error');//这里就是异常
last = await '没有执行';
}
throwError().then(success => console.log('成功', success,last))
.catch(error => console.log('失败',error,last))
上面函数,执行的到await
排除一个错误后,就停止往下执行,导致last
没有赋值报错。async
里如果有多个await函数的时候,如果其中任一一个抛出异常或者报错了,都会导致函数停止执行,直接reject
;
怎么处理呢,可以用try/catch
,遇到函数的时候,可以将错误抛出,并且继续往下执行。
let last;
async function throwError() {
try{
await Promise.reject('error');
last = await '没有执行';
}catch(error){
console.log('has Error stop');
}
}
throwError().then(success => console.log('成功', last))
.catch(error => console.log('失败',last))
Async执行方式
简单说 , async/awit 就是对上面gennerator自动化流程的封装 , 让每一个异步任务都是自动化的执行 , 当第一个异步任务readFile(A)执行完如上一点说明的, async内部自己执行next(),调用第二个任务readFile(B);
这里引入ES6阮一峰老师的例子
const fs = require('fs');
const readFile = function (fileName) {
return new Promise(function (resolve, reject) {
fs.readFile(fileName, function(error, data) {
if (error) return reject(error);
resolve(data);
});
});
};
async function read() {
await readFile(A);//执行到这里停止往下执行,等待readFile内部resolve(data)后,再往下执行
await readFile(B);
await readFile(C);
//code
}
//这里可用于捕获错误
read().then((data) => {
//code
}).catch(err => {
//code
});
注意await下面的代码执行的时机
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
async1();
new Promise((resolve) => {
console.log(1)
resolve()
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
执行顺序:
async1 start
async2
1
async1 end//注册在async2()的.then里,推迟了一个时序
2
3
4
新版V8中执行的时序等价于(激进优化后与老版不同):
function async1(){
console.log('async1 start');
const p = async2();
return Promise.resolve(p)
.then(() => {
console.log('async1 end')
});
}
function async2(){
console.log('async2');
return Promise.resolve();
}
async1();
new Promise((resolve) => {
console.log(1)
resolve()
}).then(() => {
console.log(2)
}).then(() => {
console.log(3)
}).then(() => {
console.log(4)
})
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。