JS异步编程与promise
Promise 是异步编程的解决方案
什么是异步编程?
首先我们需要了解什么是同步编程。同步就是一行一行地执行代码,下一段代码要等到上面的代码执行完毕。当发生一些耗时的任务(例如网络请求)时,很容易阻塞,并且必须等待数据请求到达才能执行后续步骤。因此,异步执行是在请求数据的同时执行以下操作。
以现实生活为例,比如你要做饭、炒菜。如果是同步执行的话,那么你就得等饭煮好了才煮饭,或者煮饭之前,这可能要花很多时间。如果是异步执行,当出现煮饭任务时,可以把任务交给电饭锅,然后去煮饭,煮好后把米拿出来。我们可以称这个过程为异步操作。
带米就相当于执行回调函数。这里我已经解释了什么是同步执行、异步执行和回调函数。如果不懂的话再看一遍或者看这里异步执行的分析。
异步操作的影响?
那么从上面我们可以看出,异步执行依赖于回调函数,那么回调函数对于执行异步操作有什么作用呢?这就是回调地狱。
回调地狱是指:回调函数嵌套回调函数,形成多层嵌套。
让我们看下面的例子。这里,如果需要在1秒后打印3条hello,则在1秒后打印3条vue.js消息,在1秒后打印3条node.js消息
首先你需要知道setTimeout是一个异步操作,传入的第一个参数是一个回调函数,当到达指定时间时会被回调并运行。
setTimeout(() => {
console.log("hello");
console.log("hello");
console.log("hello");
setTimeout(() => {
console.log("vue.js");
console.log("vue.js");
console.log("vue.js");
setTimeout(() => {
console.log("node.js");
console.log("node.js");
console.log("node.js");
}, 1000);
}, 1000);
}, 1000);
使用Promise解决回调地狱
上面的代码是回调地狱,代码太多的话会很复杂。下面是解决上述问题的优雅方式(承诺)
此时我不打算更详细地解释Promise的使用,大家可以根据下面的注释来分析代码。
//这里注意Promise()不用调用会自动执行,Promise括号中接收一个函数为参数,而函数中有两个参数resolve,reject
//当然resolve与reject参数名不是固定的,但是为了语义化我们通常这样写更加的通俗易懂
new Promise((resolve, reject) => {//resolve表示异步操作成功时的回调。reject表示失败时的回调(暂时不谈)。
setTimeout(() => {
resolve(); //因为resolve与reject是函数,所以加上括号调用
}, 1000);
}).then(() => {
//then表示然后,一旦执行了resolve()就会执行then()这个方法
//之后的所有的回调代码就能then()函数中处理,这样会使得代码变得更加清晰
console.log("hello");
console.log("hello");
console.log("hello");
setTimeout(() => {
console.log("vue.js");
console.log("vue.js");
console.log("vue.js");
setTimeout(() => {
console.log("node.js");
console.log("node.js");
console.log("node.js");
}, 1000);
}, 1000);
})
看来回调函数的嵌套还是then,那么我们可以在then()方法中返回Promise实例,这样当promise返回后,我们就可以继续使用then()交接来解析嵌套并形成循环。那么该怎么做呢?下一篇:
new Promise((resolve, reject) => {
//第一次回调执行的函数
setTimeout(() => {
resolve();
}, 1000);
}).then(() => {
//第一次回调函数处理的代码
console.log("hello");
console.log("hello");
console.log("hello");
return new Promise((resolve, reject) => {
//第二次回调执行的函数
setTimeout(() => {
resolve();
}, 1000);
})
}).then(() => {
//第二次回调函数处理的代码
console.log("vue.js");
console.log("vue.js");
console.log("vue.js");
setTimeout(() => {
console.log("node.js");
console.log("node.js");
console.log("node.js");
}, 1000);
})
//最后还有一层嵌套那么交给你自己来决绝。
虽然代码变得复杂了,但是逻辑还是清晰的。即使嵌套多层,代码仍然很清晰。只要像上面这样的嵌套回调函数(例如网络请求)必须包装在 Promise 函数中。
对于第二个示例,每1秒对值加1,并打印加1后的结果并将其传递给下一个回调,然后加1并打印两次。
new Promise(resolve => {//因为我并不打算调用reject,这个参数可以省略
setTimeout(() => {
resolve(0);//resolve中传入的参数在then中接收
}, 1000)
}).then(data => {
data++;
console.log(data);//1
return new Promise(resolve => {
setTimeout(() => {
resolve(data);
}, 1000)
})
}).then(data => {
data++;
console.log(data);//2
})
相信你仔细观察过,then()中进行的是加1的操作。这就是Promise的运营思路。将所有要运行的代码放在回调函数中执行then(),然后由resolve()调用。这里可能还是比较麻烦,但是实际编码中回调函数的代码量是非常大的,这样结构的代码就比较清晰了。
说了这么多,我们来谈谈reject()函数。上面的注释提到了reject()是在回调失败的时候调用的,所以如果回调失败的话,如果我们也在then()里面写代码来求值,那么Promise()就显得不是很优雅了,因为then里面有这么成功的回调代码以及失败的回调代码。
当然,Promise考虑到了这一点,所以我们失败时的回调代码写在catch()中,catch的意思是“捕获”,当你调用reject()时,就到了catch()方法。
举个简单的例子:
new Promise((resolve, reject) => {
setTimeout(() => {
let num = Math.floor(Math.random() * 11);//0-10的随机数
if (num >= 5) {
resolve(num);
} else {
reject(num);
}
},1000)
}).then(data => {
console.log("执行了成功时的回调,数值为:"+data);
}).catch(reason => {
console.log("执行了失败时的回调,数值为:"+reason);
})
异步获取一个随机数(0-10)并在1秒后运行。如果大于等于5,我们认为成功,所以调用solve()改变promise的状态,否则失败,调用reject()改变promise的状态,我们可以考虑参数在reject()中是失败的原因,所以一般使用reason作为参数名。
那么现在你又困惑了,promise 的状态是什么?
promise 有三种状态:待处理/已履行/已拒绝;
- pending:挂起状态,当异步任务回调函数没有履行承诺时,就处于挂起状态,比如网络请求、定时器时间
- completed:已完成状态,当我们调用resolve()方法时,promise的状态从pending变为fulfilled,then()方法的名称为
- rejected:拒绝状态,当我们调用reject()方法时,promise的状态从pending变为rejected,并调用catch()方法
基础的朋友又困惑了,不是可以在then()里写失败的回调代码吗?没错,我们看一下Promise的另一种写法。
不过,以上面的代码为例。
new Promise((resolve, reject) => {
setTimeout(() => {
let num = Math.floor(Math.random() * 11);//0-10的随机数
if (num >= 5) {
resolve(num);
} else {
reject(num);
}
},1000)
}).then(data => {
console.log("执行了成功时的回调,数值为:"+data);
},reason => {
console.log("执行了失败时的回调,数值为:"+reason);
})
实际上,then()有两个参数。第一个参数 function 是处理成功的回调代码,第二个参数 function 是处理失败的回调代码。
启用链式调用和快捷方法
参见以下代码:
new Promise(resolve => {
//这里1s后回调,执行then()中的代码
setTimeout(() => {
resolve('aaa');
}, 1000)
}).then(data => {
console.log(data);//aaa
return new Promise(resolve => {
resolve(data + 'bbb');
})
}).then(data => {
console.log(data);//aaabbb
return new Promise(resolve => {
resolve(data + 'ccc');
})
}).then(data => {
console.log(data);//aaabbbccc
})
你会发现上面的代码下面并没有进行异步操作,所以我们不需要使用promise来包装没有异步操作的代码,可以简写为如下代码:
new Promise(resolve => {
setTimeout(() => {
resolve('aaa');
}, 1000)
}).then(data => {
console.log(data);
return Promise.resolve(data + 'bbb');
}).then(data => {
console.log(data);
return Promise.resolve(data + 'ccc');
}).then(data => {
console.log(data);
})
我没想到会这样。除了上述之外,还有一个更简单的方法:
new Promise(resolve => {
setTimeout(() => {
resolve('aaa');
}, 1000)
}).then(data => {
console.log(data);
return data + 'bbb';
}).then(data => {
console.log(data);
return data + 'ccc';
}).then(data => {
console.log(data);
})
直接返回结果,实际上内部封装了返回结果。
在上面的操作中,我们肯定不仅会得到成功的回调,当然失败也会有回调。
new Promise(resolve => {
setTimeout(() => {
resolve('aaa');
}, 1000)
}).then(data => {
console.log(data);
//想必也猜到了失败的简写
return Promise.reject('error info');
//当然我们如果抛出一个异常也会执行catch中的代码
//throw 'error info';
}).then(data => {
console.log(data);
return Promise.resolve(data + 'ccc');
}).then(data => {
console.log(data);
}).catch(reason => {
//如果执行失败时的回调catch()那么中间的then()都不会执行
console.log('执行了失败时的回调',reason);
})
Promise.all()方法
Promise.all()主要用于处理多个异步请求。当我们需要完成业务时,即两次网络请求都成功时,就会执行回调函数中的代码。现在,您需要使用 Promise.all()。
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('结果1');
}, 1000);
})
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('结果2');
}, 2000);
})
//Promise.all([])接收一个数组
Promise.all([p1, p2]).then(results => {//其中results也是一个数组
console.log(results);//["结果1", "结果2"]
})
Promise.race()方法
Promise.all() 在等待每个异步任务完成后执行。而 Promise.race() 是先跑谁就跑,所以你可以看到,然后我总是写结果而不是复数结果。
发表评论:
◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。