一个朴实无华的名字---js异步

785 阅读7分钟

引子

异步在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 的前端组件库