JavaScript 异步之回掉地狱写法

7,954 阅读2分钟

「这是我参与2022首次更文挑战的第27天,活动详情查看:2022首次更文挑战」。

异步与同步

  • 说起 js,大家都知道它是单线程执行的
  • 如果它是多线程的,那么代表它在同一时刻就能执行多个任务
    • 例如两个任务,一个任务修改 dom, 同时另一个任务删除此 dom,那么浏览器就不知道该怎么操作 dom 了
    • 所以为了保证 web 应用的交互一致性,而被设计成单线程执行
  • 如果程序中的任务都是普通的同步任务,那么按照单线程的执行逻辑,依次执行每个任务即可
  • 但是往往我们的应用程序,不会只包含同步任务,往往也会涉及到异步的网络请求等任务,那 js 引擎该如何处理呢,答案就是使用 callback 回掉函数
    • 将下一个任务作为回掉传给上一个任务执行,当上一个任务执行完后,立即执行回掉

用回掉函数解决异步问题

  • 下面就是一个典型案例用回掉来解决 setTimeout 带来的异步问题
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      * {
        margin: 0;
        padding: 0;
      }

      .box {
        position: absolute;
        width: 200px;
        height: 200px;
        background-color: teal;
        /* left: 0; */
      }
    </style>
  </head>
  <body>
    <div class="box"></div>

    <script>
      let move = (ele, direction, target, cb) => {
        let start = parseInt(getComputedStyle(ele, null)[direction]);
        let speed = ((target - start) / Math.abs(target - start)) * 2;
        start += speed;
        ele.style[direction] = `${start}px`;

        setTimeout(() => {
          if (Math.abs(target - start) <= 2) {
            ele.style[direction] = `${target}px`;
            cb & cb();
          } else {
            move(ele, direction, target, cb);
          }
        }, 10);
      };

      let ele = document.querySelector(".box");
      move(ele, "left", 301, () => {
        console.log("右移 ok");
        move(ele, "top", 301, () => {
          console.log("下移 ok");
          move(ele, "left", 0, () => {
            console.log("左移 ok");
            move(ele, "top", 0, () => {
              console.log("上移 ok");
            });
          });
        });
      });
    </script>
  </body>
</html>
  • 上面的实现逻辑很简单,通过 setTimeout 每隔 10ms 去执行一次 move 函数,move 函数接收要移动的终点值,当移动到终点后,执行回掉函数,即传入的下个动画函数
  • move 函数的实现也很简单
    • 接收四个参数,依次是:需要操作的 dom 元素,dom 元素移动的方向,dom 元素移动的终点值,回掉函数
    • 通过 getComputedStyle API 获取 dom 元素最新的样式
    • 我们的 dom 元素初始状态的坐标是 (0,0), 那么当 dom 元素向右和向下移动时,speed 为正值,当 dom 元素向左和向上移动时,speed 为负值
    • move 函数每次执行,都需要将关于移动的属性值更新一下
    • 在 setTimeout 中,若 dom 元素已经到达终点,则调用下个移动的回掉函数,否则按照当前方向继续移动dom 元素

小结

  • 尽管上面已经实现了,我们想要的元素移动的效果
  • 但是实现的代码,未免太不优雅,形成了传说中的回掉地狱,极其不利于代码阅读与维护
  • 为了解决这个问题,我的下篇文章将会介绍一种相对优雅的实现方式:基于自定义事件的异步操作

最后

  • 今天的分享就到这里了,欢迎大家在评论区里面进行讨论 👏。
  • 如果觉得文章写的不错的话,希望大家不要吝惜点赞,大家的鼓励是我分享的最大动力 🥰