引子
异步在js中我们经常会遇到,也是很多刚接触js的同学经常遇到的问题。会灰常的头大。so,今天咋们就来看看异步。希望通过今天的分享,对异步还有疑问的同学能够搞懂。
啥是异步?
同步和异步是一种消息通知机制 。
- 同步阻塞: A调用B,B处理获得结果,才返回给A。A在这个过程中,一直等待B的处理结果,没有拿到结果之前,需要A(调用者)一直等待和确认调用结果是否返回,拿到结果,然后继续往下执行。
- 异步非阻塞: A调用B,无需等待B的结果,B通过状态,通知等来通知A或回调函数来处理。 恩,说到这里需要强调一下 。js里我们可以认为同步会造成堵塞,异步会是非阻塞。但是在其他情景下,同样也会存在同步非阻塞及异步阻塞的概念。所以不要把同步异步、阻塞非阻塞概念混淆了。
为什么会有异步?
为什么会有异步呢?原因是因为js的单线程导致的。你可以理解单线程就是一次只能处理一个任务,如果多个任务就需要排队执行。举一个很俗气的比如就是一个厕所只有一个坑,来了多个人上厕所怎么办?那么就需要在厕所外面排队。任务亦如此,需要在队列里依次执行。
但是话说回来如果有些人上厕所非常慢怎么办 ?那么后面的人是不是需要一直等待?so,js里任务处理也是如此,也会有非常慢的任务。所以js处理办法是先把这些慢的任务放在一个异步队列里,恩就是把这些上厕所慢的单独放在一个位置管理起来。然后上厕所快的就是同步任务,慢的就是异步任务。把所有同步执行完成之后才会执行异步。这样做的好处在于,不会堵塞厕所,高效的使用一个坑(单线程)的厕所。如图:
js中异步
为了不堵,就是不阻塞。js引入了异步概念。代码如下:
function zhangsan() {
console.log("张三上完厕所了");
}
function lisi() {
setTimeout(() => {
console.log("李四上完厕所了");
})
}
function wangwu() {
console.log("王五上完厕所了");
}
zhangsan();
lisi();
wangwu();
上面代码由于 李四比较墨迹(异步任务)so,你会发现执行结果是:
会发现李四是最后上完厕所的,因为他被放到了异步队列延后执行了。这样会导致对于异步不熟悉的同学,会按照惯性认为李四第二个做完的,其实不然。在很多编码需求中我们希望即使是异步任务,我们也希望按照顺序执行,这样更符合我们的编码习惯。那么如何做呢?也就是如何处理异步?
回调函数
最简单的方式就是通过回调函数,上面的代码,如果我想让张三、李四、王五 即使有异步也按照正常顺序上厕所咋办呢?用回调(callback)就好了。代码如下:
function zhangsan() {
console.log("张三上完厕所了");
}
function lisi(cb) { //这里cb就是回调函数
setTimeout(() => {
console.log("李四上完厕所了");
cb && cb();
})
}
function wangwu() {
console.log("王五上完厕所了");
}
zhangsan();
lisi(()=>{
wangwu();
});
如此,就可以解决问题。的确在实际开发中类似异步处理问题非常常见,最最常见的就是网络请求,网络请求都是需要时间的。我们多数情况下希望请求完成之后渲染视图,操作节点等等。所以你可以用回调。回调可以解决问题,但是也存在问题。回调地狱就是大量使用回调造成的问题。
回调地狱
回调地狱简单理解就是函数作为参数层层嵌套。会造成写法臃肿,嵌套关系复杂,可维护性差的问题。比如现在有一个需求让方块按照顺序移动。如下:
🤔这里只能贴这样一个静态图了。还没有大帅同学一夜的动画功底,有机会请教下。
这里先实现方块的移动函数封装,传入回调进行调用。代码如下:
function move(ele, target, direction, cb) {
// ele: 运动的元素
// target:运动结束的目标点
// direction:运动方向 这里只去改变 left 及 top值
// cb : 运动完成后的回调事件钩子。
let left = parseInt(ele.style[direction]) || 0;
let plusOrminus = (target - left) / Math.abs(target - left);//判断下运动应该加或者减
let speed = 1 * plusOrminus;
setTimeout(() => {
left += speed;
if (left === target) {
// console.log("已经移动到目标位置");
cb && cb();
} else {
ele.style[direction] = left + "px";
move(ele, target, direction, cb);
}
}, 10)
}
let ele = document.querySelector(".box");
// 回调地狱
move(ele, 300, "left", () => {
move(ele, 300, "top", () => {
move(ele, 0, "left", () => {
move(ele, 0, "top", () => {
console.log("运动完成");
})
})
})
});
如上,通过回调实现需求后,发现写了很多嵌套函数。这样就出现了回调地狱,你会发现后期再维护的时候比较困难。那么有没有更好的方式呢?答案是肯定的。
自定义事件
通过自定义也可以解决回调出现的回调地狱问题。代码如下:
let num = 1;
let targetObj = new EventTarget();
function move(ele, target, direction) {
// ele: 运动的元素
// target:运动结束的目标点
// direction:运动方向 这里只去改变 left 及 top值
// cb : 运动完成后的回调事件钩子。
let left = parseInt(ele.style[direction]) || 0;
let plusOrminus = (target - left) / Math.abs(target - left);//判断下运动应该加或者减
let speed = 1 * plusOrminus;
setTimeout(() => {
left += speed;
if (left === target) {
targetObj.dispatchEvent(new CustomEvent("myevent"+num));
num++;
} else {
ele.style[direction] = left + "px";
move(ele, target, direction);
}
}, 1)
}
let ele = document.querySelector(".box");
move(ele,300,"left");
targetObj.addEventListener("myevent1",()=>{
console.log("运动完成11");
move(ele,300,"top");
})
targetObj.addEventListener("myevent2",()=>{
console.log("运动完成22");
move(ele,0,"left");
})
targetObj.addEventListener("myevent3",()=>{
console.log("运动完成33");
move(ele,0,"top");
})
如上写法需要事件名称作为联系,且不能捕捉错误。有没有更好的方式呢?ES6给我们提供了好用的promise。
promise解决异步
把上述需求通过promise来进行改造轻松达到想要的效果,这里对于promise基本用法就不在细说了。可以搜索下。代码如下改造:
function move(ele, target, direction) {
return new Promise(resolve => {
function fn() {
let left = parseInt(ele.style[direction]) || 0;
let plusOrminus = (target - left) / Math.abs(target - left);//判断下运动应该加或者减
let speed = 1 * plusOrminus;
setTimeout(() => {
left += speed;
if (left === target) {
resolve();
} else {
ele.style[direction] = left + "px";
fn();
}
}, 1)
}
fn();
})
}
let ele = document.querySelector(".box");
move(ele, 300, "left").then(() => {
return move(ele, 300, "top")
}).then(() => {
return move(ele, 0, "left")
}).then(() => {
return move(ele, 0, "top")
}).then(res=>{
console.log("运动完成");
})
恩 一个链式调用,解决调用问题。看起来更加舒爽。也不需要关联事件名称。非常nice。但是目前我们解决异步最优雅的方式还是 async 及 await。
优雅的async及await
async及await在本质上还是异步,只是把异步变成了同步的写法。让代码可读性更强。如上案例只需要稍加改动。如下:
async function asyncFn() {
try {
await move(ele, 300, "left");
await move(ele, 300, "top")
await move(ele, 0, "left");
await move(ele, 0, "top");
console.log("运动完成");
} catch (e) {
return console.log(e);
}
}
asyncFn();
如此,代码调用看起来是不是会比链式调用更加舒爽 。完全符合了我们同步代码的执行逻辑。如此亲切友好。恩,也是目前异步的终极解决方案。
后记:望通过此文,让大家能了解js异步。从此上厕所不堵!!😀
最后:老铁你可以默默的看过,也可以轻轻的点赞,还可以偷偷的关注。😜!!
- 这是我们团队的开源项目 element3
- 一个支持 vue3 的前端组件库