es6的一些知识点整理 - 2
异步与同步
异步和同步是一种消息通知机制。
什么是异步?
异步非阻塞:
比如: A调用B,B处理同时,A继续执行。B处理结束,返回结果,A再用B的结果执行。A在此期间不用等B执行结束。
同步阻塞:
比如: A调用B,B处理完后返回结果给A,A继续执行。A在此期间一直等B执行结束。正常代码的执行,是逐行执行的,就是同步机制。
实现异步的最基本方法:使用计时器
function move(ele, dir, dist) {
let curPos = parseFloat(getComputedStyle(ele)[dir]);
let speed = (dist - curPos) /Math.abs(curPos - dist);
clearInterval(ele.timer);
console.log("start moving...");
ele.timer = setInterval(() => {
if (Math.abs(curPos - dist) <= 0) {
clearInterval(ele.timer);
console.log("finish moving...");
} else {
curPos += speed;
ele.style[dir] = curPos + "px";
}
}, 20);
console.log("printing....");
}
let box = document.querySelector("#box");
move(box, "left", 100);
上述代码会产生下图效果:

打印结果如下:

从运行结果可以看出,printing在方块移动结束前打印了,并没有像常规的程序那样顺序执行。这就是最基本的异步的实现。
异步结束完需要操作怎么办?
肯定有很多解决方法,但是我认为比较好的方法是回调函数的方法,修改上述代码:
function move(ele, dir, dist, callback) {
let curPos = parseFloat(getComputedStyle(ele)[dir]);
let speed = (dist - curPos) /Math.abs(curPos - dist);
clearInterval(ele.timer);
console.log("start moving...");
ele.timer = setInterval(() => {
if (Math.abs(curPos - dist) <= 0) {
clearInterval(ele.timer);
console.log("finish moving...");
callback && callback(); // ===>此处先判断是否存在callback,如果存在就执行
} else {
curPos += speed;
ele.style[dir] = curPos + "px";
}
}, 20);
console.log("printing....");
}
let box = document.querySelector("#box");
move(box, "left", 100, () => {
console.log("执行回调函数");
});
上述代码执行结果是:
start moving...
printing....
finish moving...
执行回调函数
这样就能很好的解决异步执行之后需要用到异步的结果或者需要执行某些代码的问题。
这种实现异步和回调的问题:回调地狱
所谓回调地狱,就是多次回调,致使层数过深,如下:
move(box, "left", 100,() => {
move(box, "top", 100,() => {
move(box, "left", -100,() => {
move(box, "top", -100,() => {
console.log("finish...");
});
});
});
});
上述代码层数过深,不利于代码的复用性和阅读性。
回调地狱解决办法
解决办法有三种,分别是使用Promise ,使用Async和Await以及使用Generator函数。这一篇文章先讲一下Promise的使用。
Promise的概念以及使用
Promise不是解决异步问题本身,而是解决异步的写法问题,使异步写法更清晰。
先说下Promise,在MDN,Promise被如下定义:
Promise 对象用于表示一个异步操作的最终完成 (或失败), 及其结果值.[1]
Promise有三个状态:Pending(在等待异步流程执行完毕);Fulfilled或者Resolved(成功);Rejected(失败)。
异步操作的结果决定了当前状态,其它内容无法干扰Promise状态。且,Promise如果状态改变,则不会再变,比如从pending改变到rejected,就不会再回到pending,或者再变为fulfilled。[2]
接下来展示一段Promise的简单使用:
let p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(p);
resolve();
}, 2000);
});
p.then(() => {
console.log(p);
console.log("then....");
});
then方法就相当于之前实现多次异步时候的回调函数,根据上述代码,可以发现Promise创建后传入一个函数,函数的参数是两个函数resolve和reject,分别对应着rejected和resolved(fulfilled)两个状态。现在就一目了然了,如果异步执行成功,则调用运行resolve(),反之运行reject()。上述运行结果为:
程序运行两秒后,打印如下内容:
Promise {<pending>}
Promise {<resolved>: undefined}
then....
由此可见,如果异步执行成功,调用resolve函数,则程序会向下执行到then,而且从打印的两个p我们能看出,Promise的状态从pending向resolved改变。
那如何使用异步结果呢?
很多时候,异步产生的结果,是我们后面要使用的,那么如果像上述一样用Promise来优化异步的写法,我们如何使用异步结果呢?
其实答案已经在上述代码出现了。上述代码的打印结果的第二行,Promise状态resolved后面有一个undefined。这个地方,就是用来存异步结果的。代码可以改动如下:
let p = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(p);
resolve("parameters..."); // =======》这里执行resolve时候可以传入参数。
}, 2000);
});
p.then((res) => { // ======》这里能传入一个参数,比如res
console.log(p);
console.log(res);
console.log("then....");
});
运行结果如下:
Promise {<pending>}
Promise {<resolved>: "parameters..."}
parameters...
then....
结合上述代码和结果,我们能看出来,通过向resolve方法传入异步结果,在调用then方法后,可以接收一个参数,这个参数就指向异步的结果。从而就能使用异步结果了。
如果想在then函数中处理reject的结果,那么也很简单,传入第二个回调函数作为参数,比如:
p.then((res) => {
console.log(p);
console.log(res);
console.log("then....");
}, (rej) => {
console.log(rej);
});
一目了然,在此就不做赘述了。
但是如果存在需要链式调用then函数,每个函数中都传入两个函数来处理成功和失败的结果,有时候会比较繁琐,因此,如果所有步骤处理出现错误或者失败,都只需要返回一种信息给用户(比如网络请求错误),可以用以下办法:
p.then((res) => {
console.log(p);
console.log(res);
console.log("then....");
}).then((res) => {
console.log(p);
console.log(res);
console.log("then....");
}).then((res) => {
console.log(p);
console.log(res);
console.log("then....");
}).catch((rej) => {
console.log("fail...");
})
如果想实现之前那种需要多次回调的场景也很简单,代码如下:
let po = new Promise((resolve, reject) => {
resolve(1);
});
po.then((res) => {
console.log(res);
return 2;
}).then((res) => {
console.log(res);
return 3;
}).then((res) => {
console.log(res);
return 4;
}).then((res) => {
console.log(res);
})
运行结果是:
1
2
3
4
很显然,then方法最后return的结果是可以被后面的then方法获取到的。但是为什么能获取呢?我们可以给调用then方法的结果赋值,打印看下。代码为:
let po = new Promise((resolve, reject) => {
resolve(1);
});
let pt = po.then((res) => {
console.log(res);
return "pt";
})
console.log(pt);
结果为:
Promise {<pending>}__proto__: Promise[[PromiseStatus]]: "resolved"[[PromiseValue]]: "pt"
1
上述代码运行后先打印了最后一行的pt。因为异步的原因,然后打印了resolve传入then函数的数字1。pt的展开结果我们可以看出,then方法会给我们返回一个Promise对象,而且从上述结果看出,返回的是一个resolved状态的Promise对象。
现在我们都了解到Promise的用法了,上文中的移动方块的例子能改写为:
function move(ele, dir, dist) {
let curPos = parseFloat(getComputedStyle(ele)[dir]);
let speed = (dist - curPos) /Math.abs(curPos - dist);
return new Promise((resolve, reject) => {
clearInterval(ele.timer);
ele.timer = setInterval(() => {
if (Math.abs(curPos - dist) <= 0) {
clearInterval(ele.timer);
resolve();
} else {
curPos += speed;
ele.style[dir] = curPos + "px";
}
}, 20);
})
}
let box = document.querySelector("#box");
move(box, "left", 100).then(() => {
return move(box, "top", 100);
}).then(() => {
return move(box, "left", 0);
}).then(() => {
return move(box, "top", 0);
})
运行结果和上述结果一样。但是可读性和代码可扩展性得到了显著的提高。
Promise all方法
说完Promise的使用和then还有catch常用方法,接着说下Promise的另一个常用方法 - all方法。
MDN对于all方法的说明是:
Promise.all(iterable) 方法返回一个 Promise 实例,此实例在 iterable 参数内所有的 promise 都“完成(resolved)”或参数 中不包含 promise 时回调完成(resolve);如果参数中 promise 有一个失败(rejected),此实例回调失败(reject),失 败原因的是第一个失败 promise 的结果。[3]
简单说就是比如创建了三个不同的Promise对象,调用all方法执行三个Promise对象,如果三个都是resolved状态,则可以向下执行。
例子:
let p1 = new Promise((resolve, reject) => {
console.log(1);
resolve();
});
let p2 = new Promise((resolve, reject) => {
console.log(2);
resolve();
});
let p3 = new Promise((resolve, reject) => {
console.log(3);
resolve();
});
Promise.all([p1, p2, p3]).then(() =>{
console.log(4);
})
结果:
1
2
3
4
如果任意一个Promise对象的执行状态是rejected。则需要在then中或者或者代码最后使用catch方法捕获错误,不然会报错。
Promise race方法
这个方法和all方法正好相反,all方法是全部执行结束,才执行下面的内容。race方法顾名思义,有其中一项执行结束,则开始执行下面的方法。代码如下:
let p1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(1);
resolve();
}, 2000);
});
let p2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(2);
resolve();
}, 1000);
});
let p3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log(3);
resolve();
}, 5000);
});
Promise.race([p1, p2, p3]).then(() =>{
console.log(4);
}).catch(() => {
console.log("err");
});
结果是:
2
4
1
3
上面代码我们把p1的执行时间设置为2s,p2是1s,p3是5s,从结果就能看出,p1执行结束后,直接执行下面的then方法,因此打印顺序是2,4接着两个分别按照设定时间执行完毕。
Async和Await写法
有时候用Promise的一系列方法,仍然感觉可读性一般,代码扩展性也有局限,那么这时候,可以用Async和Await结合Promise使用。示例代码如下:
async function fn () {
let p1 = await new Promise((resolve) => {
console.log(1);
resolve(2);
});
let p2 = await new Promise((resolve) => {
console.log(p1);
resolve(3);
});
let p3 = await new Promise((resolve) => {
console.log(p2);
resolve(4);
});
console.log(p3);
}
fn();
打印结果:
1
2
3
4
Async和Await进一步简化了代码量,优化了写法,也是现今为止最常用的异步写法。
注意:
- Await 后面接一个能返回Promise对象的方法或者创建一个新Promise对象。
- Await必须放在Async标示的函数当中。
- 如果需要捕获异常,可以用try{} catch (exception) {},把await写入try后面的代码块中。
- Async函数返回一个Promise,状态是resolved。
- Await可接受非Promise作为await表达式的结果。但是不管await后面是什么,都会阻塞async函数内部代码执行,如果这时有外部代码,则先执行外部代码,之后再回到内部执行。
- 如果async中代码是同步代码,那就同步执行。
小结
Promise差不多就说完了,也简单说了下Async和Await。感觉会用是最关键的。至于Generator,听说过没用过,感觉用的比较少,以后找机会再说吧。
参考链接
[1] https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise
[2] https://www.jianshu.com/p/1ab01ee4102a
[3] https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/all