一、JavaScript常见的异步操作
1. 定时器函数(setTimeout / setInterval)
// setTimeout定时器:待到duration毫秒后执行回调函数callback,
// 只执行一次。
setTimeout(callback,duration);
// setInterval定时器:待到duration毫秒后执行回调函数callback,
// 不清除定时器则一直执行回调函数。
setInterval(callback,duration);
// 清除定时器:
clearTimeout(定时器名称)
2. 事件函数(click点击事件等)
$("#btn").click(function(){......})
$("#btn").dblclick(function(){......})
$("#btn").mouseover(function(){......})
$("#btn").mouseout(function(){......})
document.getElementById("myBtn").addEventListener("click", function(){
document.getElementById("demo").innerHTML = "Hello World";
});
...
3. 网络请求(Ajax、Axios、wx、Request)
jQuery中发起Ajax请求:
// GET请求传参也可以直接在地址后传参 xxx.com?num=10
$.get(url,dataParams,function(result,status){})
// POST请求传参有请求头和请求体的限制,不可以直接在地址后传参
$.post(url,dataParams,function(result,status){})
// $.post请求默认传参类型为JSON对象
$.ajax({
url:"",
type:"POST",
async:true, // 是否异步
data:{...},
dataType:"text", // 预计返回的数据格式
success:function(res,status,xhr){
...
},
error:function(xhr,status,error){
...
},
complete:function(xhr,status){ //请求完成后的回调函数,不论请求成功还是失败。
...
}
// 更多属性请善用搜索引擎
})
4. Promise对象
ECMAscript 6 原生提供了 Promise 对象。
Promise 对象代表了未来将要发生的事件,用来传递异步操作的消息。
Promise 对象有以下两个特点:
1、对象的状态不受外界影响。Promise 对象代表一个异步操作,有三种状态:
pending: 初始状态,不是成功或失败状态。
fulfilled: 意味着操作成功完成。
rejected: 意味着操作失败。
只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。这也是 Promise 这个名字的由来,它的英语意思就是「承诺」,表示其他手段无法改变。
2、一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise 对象的状态改变,只有两种可能:从 Pending 变为 Resolved 和从 Pending 变为 Rejected 。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。
创建Promise对象:
<script>
let promise = new Promise(function (resolve, reject) {
// 异步处理
$.ajax({......})
// 处理结束后、调用resolve 或 reject
})
</script>
5. Generator函数
ES6 新引入了 Generator 函数,可以通过 yield 关键字,把函数的执行流挂起,为改变执行流程提供了可能,从而为异步编程提供了解决方案。
Generator 有两个区分于普通函数的部分:
一是在 function 后面,函数名之前有个'*'
;
函数内部有yield
表达式。
其中 * 用来表示函数为 Generator 函数,yield 用来定义函数内部的状态。
function* func(){
console.log("one");
yield '1';
console.log("two");
yield '2';
console.log("three");
return '3';
}
Generator函数的执行机制:
调用 Generator 函数和调用普通函数一样,在函数名后面加上()即可,但是 Generator 函数不会像普通函数一样立即执行,而是返回一个指向内部状态对象的指针,所以要调用遍历器对象Iterator 的 next 方法,指针就会从函数头部或者上一次停下来的地方开始执行。
上面代码的运行结果:
f.next();
// one
// {value: "1", done: false}
f.next();
// two
// {value: "2", done: false}
f.next();
// three
// {value: "3", done: true}
f.next();
// {value: undefined, done: true}
6. async / await关键字
async 是 ES7 才有的与异步操作有关的关键字,是和 Promise , Generator 有很大关联的。
async function fn1(params){
const response = await fetch("...")
...
return "abc"
}
async函数返回值为Promise对象,可以使用 then 方法添加回调函数。
async function helloAsync(){
return "hello";
}
console.log(helloAsync()) // Promise {<resolved>: "hello"}
helloAsync().then(v => {
console.log(v); // hello
})
async 函数中可能会有 await 表达式,async 函数执行时,如果遇到 await 就会先暂停执行 ,等到触发的异步操作完成后,恢复 async 函数的执行并返回解析值。
但是await关键字并没有表面这么简单,深入解析见另一篇文章:async/await面试题
await 关键字仅在 async函数中有效。 如果在 async 函数体外使用 await 则会报错。
await的返回值为 Promise 对象的处理结果。如果等待的不是 Promise 对象,则返回其对应的值。
如果一个 Promise 被传递给一个 await 操作符,await 将等待 Promise 正常处理完成并返回其处理结果。
function testAwait (x) {
return new Promise(resolve => {
setTimeout(() => {
resolve(x);
}, 2000);
});
}
async function helloAsync() {
var x = await testAwait ("hello world");
console.log(x);
}
helloAsync ();
// hello world
正常情况下,await 命令后面是一个 Promise 对象,它也可以跟其他值,如字符串,布尔值,数值以及普通函数。
function testAwait(){
console.log("testAwait");
}
async function helloAsync(){
await testAwait();
console.log("helloAsync");
}
helloAsync();
// testAwait
// helloAsync
7. 回调函数
即被传递给另一个函数作为参数的函数,如定时器函数中的callback,各种事件函数中的 function 等等等等。
setTimeout(callback,duration);
setInterval(callback,duration);
$("#btn").click(function(){......})
$("#btn").dblclick(function(){......})
$("#btn").mouseover(function(){......})
$("#btn").mouseout(function(){......})
...
只要是被传递给另一个函数作为参数的 函数 都可以被叫做回调函数,回调函数的执行通常伴随着某种条件,即当…时执行此函数;
二、异步操作时的问题(回调地狱)及解决方案
在通过Ajax进行网络请求时有这么一种特殊的情况:当前请求的返回值是下一次请求执行时所需要的参数;
这种情况在我们书写代码时会造成回调函数的层层嵌套,使代码看起来像这个效果↓ :
fs.readdir(source, function (err, files) {
if (err) {
console.log('Error finding files: ' + err)
} else {
files.forEach(function (filename, fileIndex) {
console.log(filename)
gm(source + filename).size(function (err, values) {
if (err) {
console.log('Error identifying file size: ' + err)
} else {
console.log(filename + ' : ' + values)
aspect = (values.width / values.height)
widths.forEach(function (width, widthIndex) {
height = Math.round(width / aspect)
console.log('resizing ' + filename + 'to ' + height + 'x' + height)
this.resize(width, height).write(dest + 'w' + width + '_' + filename, function(err) {
if (err) console.log('Error writing file: ' + err)
})
}.bind(this))
}
})
})
}
})
代码的左侧形成了一个空的三角形地带,代码的可读性变得非常差,极其不利于代码的维护,我们为这种情况起了一个霸气的名字:回调地狱。
为了解决这个问题,ES6中推出了Promise对象。
见上述,Promise对象有三个状态:
Pending(初始) 、Fulfilled(操作成功)、Rejected(操作失败)。
Promise的状态只能由Pending → Fulfilled,或者Pending → Rejected。
在创建Promise对象时又有两个参数:resolve函数和reject函数;
resolve函数为操作成功后的回调函数,reject函数为操作失败后的回调函数。
function getCourseList(type = "free", pageNum = '1', pageSize = '10') {
return new Promise(function (resolve, reject) {
$.post(baseUrl + "/weChat/applet/course/list/type", { type, pageNum, pageSize }, function (res, err) {
if (res.code == 0) {
resolve(res)
} else {
console.log(err);
reject(err)
}
})
})
}
此时 resolve(res) 为操作成功后把返回的数据res作为返回值返回,然后我们可以通过 then() 方法来获取数据 res,而 then() 方法的返回值又是一个Promise对象,所以我们可以进行链式调用:
getCourseList().then(res => { renderCourses("#freeClassContent", res.rows, "free") }).then(jumpToClassContent)...
但是如果需要连续调用N次,链式调用的写法也不是很好看,可读性也不高,属于把原来的纵向的地狱问题变成了横向的
于是ES7又推出了 async 关键字 与 await 关键字,其中 await 关键字必须在async函数中使用,否则会报错。
利用它们可以彻底解决回调地狱的问题,把代码从视觉上变回了正常代码,还在视觉效果上将异步操作变为了同步操作,但是从底层逻辑来说,异步操作还是异步操作、同步还是同步,这个是不变的。(即底层层面上异步操作并不能变成同步操作)
async function fn1(){
let a = await getCourseList()
let b = await renderCourses(a)
b.jumpToClassContent()
}