js是单线程的,为了避免io类操作阻塞主线程,所以必须采用回调函数callback的形式把耗时的io操作委托给其他io线程处理(所以js并不是纯的单线程,只是有一个主线程在做mainloop而已)比如这样:
fs.readFile("a.txt",{encoding:"utf-8"}, function (err, fr) {
//readFile回调函数
if (err) {
console.log(err);
}else {
let str = fr;
fs.readFile(str,{encoding:"utf-8"}, function (err, fr2){
if (err) {
console.log(err);
}else{
console.log(fr2);
}
})
}
});
callback的代码是很影响阅读体验的,上面的代码我要这么理解:先read一个file,然后获得的内容到哪里去了呢?它被赋值给回调函数的fr参数里去了,我要再去看回调函数是怎么处理fr的,所以我的注意力又转到回调函数里去了,然后回调函数里又嵌套了一层callback,我的思路又被打断了,又要进入第二个callback看看fr2是怎么处理的。
人不是机器,都习惯顺序查看代码,假如能这样写那代码就好理解的多了:
let str = fs.readFile("./a.txt",{encoding:"utf-8"})
if(str){
let fr2 = fs.readFile(str,{encoding:"utf-8"})
if(fr2){
console.log(fr2)
}
}
所以后来各种架构做了各种努力让js的代码更加human readable。
我们先说说Promise,因为它是后面要讲的async/await的基础。
Promise是一个代理对象,它代理的是一个值,这个值我们可以称为Promise对象的状态(status)。这个status对Promise非常重要,它有三种状态:pending,resolved,rejected。我们来看看怎么去new一个Promise:
new Promise( function(resolve, reject) {...} /* executor */ );
Promise的构造函数里直接传入了一个function,我们可以称呼它为executor,它有两个函数参数resolve,reject。当上面这行语句执行的时候,executor会立即异步执行。在executor方法的方法体内,你可以写你自己的代码逻辑,一般逻辑代码都包括正常执行逻辑和出错异常处理,你可以在代码的正常执行逻辑里调用resolve(retValue)来把Promise的status改为resolved,在出错异常处理的代码里调用reject(err)来把Promise的status改为rejected。也就是说,executor函数的执行成功还是失败,是可以从Promise的状态里判断出来的。这里要注意两点:
Promise的状态变化了有什么用呢,它的状态可以影响后续的then的行为:
promise.then(function onFulfilled(value)
{ console.log(value); })
.catch(function onRejected(error)
{ console.error(error);}
当promise的状态是resolved时候,会调用then方法里面的onFulfilled函数,value的值就是我们通过上面提到的resolve(retValue)传入的;如果是rejected状态,会调用catch方法里面的onRejected函数,error就是我们通过rejected(error)传入的。上面的then catch模式和下面的模式是等价的:
promise.then(function onFulfilled(value){ console.log(value); },
function onRejected(error){ console.error(error);})
then里面的onFulfilled和onRejected函数的执行,有个前提是promise必须要是resolved或者rejected状态,如果是pending状态,then是不会执行的。所以聪明的你一定想到了,promise就可以替代callback了,我们把读取文件的代码放到promise的executor里面,当读取完毕了再resolve这个promise,就可以接着执行then里面的onFulfilled了,所以本文开头的例子可以这样重写了:
//对readFile做一下封装,从这个封装的代码可以看出来promise完美地对应了callback的代码书写方式
const readFile = function (path) {
return new Promise((resolve, reject) => {
fs.readFile(path, (err, data) => {
if (err) reject(err);
resolve(data);//注释1
});
});
};
//readFile会把读取文件包装成一个promise,我们接下来开始使用它
readFile('a.text').then(function onFulfilled(str) {
console.log(str);
return readFile(str);
}).then(function onFulfilled(str2) {
console.log(str2);
}).catch(function onRejected(error){ console.error(error);});
有几点需要说明下:
var promise = new Promise(function (resolve, reject) {
setTimeout(function () {
throw new Error('error') //setTimeout里的代码是异步执行的,throw的Error不会被捕捉
}, 0)
//throw new Error('error') //把上面的setTimeout注释掉,用这一行的throw new Error是会被捕捉的
resolve('ok');
});
promise
.then(function (value) { console.log(value) })
.catch(() => console.log('catch err'))//如果把这里的catch注释掉,会打印出错误,但不会影响后面的console.log执行
console.log('finish')
Promise.resolve(43).then(function (value) {//then方法1
console.log(value);
setTimeout(()=>{
return value+1
},1000)
}).then(value=>console.log(value))//then方法2
上面的代码会输出:
43
undefined
因为setTimeout是异步执行的,所以then方法1不会等它1000ms后返回value+1传给then方法2,而是直接传了undefined。异步的需求完全可以通过新增一个then来解决。
上面的promise调用链看起来比callback方式清晰了不少,但是还是有它的不足:
async function()
{
let str = await readFile("./a.txt")
if(str){
let fr2 = await readFile(str)
if(fr2){
console.log(fr2)
}
}
}
看起来是不是很完美?跟同步代码比只多了await。相比于Promise写法,它的好处是:
敲到大半夜终于一鼓作气写完了,感觉挺爽,打完收工。