十年代码生涯一瞬间,代码即人生!
<返回上一级
用 Promise + 递归实现灌酒动画
Promise前端算法CSS

国际惯例先来看一下最后的实现效果~

pour

项目背景(扯淡)

世界杯期间公司拉了一批啤酒的赞助,有一个邀请(pian)好友来领(mai)会员灌啤酒的分享活动,达到8瓶酒就可以得一箱。听起来很是诱人,毕竟世界上最惬意的事情莫过于看看球,喝喝酒,撩撩妹(并没有)。

准备工作

我的任务呢就是把灌酒的这个操作弄的炫酷一点,毕竟我也是个有艺术细菌的人,立马就构思好了摇晃和酒上升的动画。这点小事难不倒我,撸起袖子,反手就打开了 codepen,搜索关键字 "beer shaking", "beer pouring",经过一番苦苦搜寻,终于找到了一个满意的例子。emmmmm我可真是个小机灵鬼,这下改改代码就好了。

转折

正在我洋洋得意之时,拿到了对应的设计稿。WTF ???这酒瓶是个图?这个泡沫还要动?这图里面的酒还要能上升?来来来,你行你上。。。这下之前所有的美好幻想都泡汤了,由于酒瓶是定制的图片,无法自定义酒的颜色和高度,就算采用绝对定位也很难和谐,这还得做动画,我做!@#¥%……&*不行,我得冷静一下,我能想出办法的!

实现

终于迎来了正文(我真的不是个话痨),经过我的一番思索,发现难点主要在这三个方面:
1.啤酒泡沫动画
2.啤酒上升的动画
3.一个酒瓶的上升动画完成之后才能开始下一个啤酒的动画

前两个问题由于这次的酒瓶是定制的,所以不可能用 CSS 自己画出来,只能用替换图片(GIF)的方式,即动态替换 img 标签的 src,实践下来,这样实现的效果不错,看不出切换的痕迹,当然如果嫌网速慢的话,可以提前预加载要替换的所有图片进行缓存,这样切换起来更加流畅。
至于最后一个问题,仔细想想是不是很像一个东西?在一件事情完成之后再去做另一个,这tm不就是 Promise 吗?,废话不说直接上代码:

// 首先定义10张图片的地址
const pics = [
  '1.gif',
  '2.gif',
  '3.gif',
  '4.gif',
  '5.gif',
  '6.gif',
  '7.gif',
  '8.gif',
  '9.gif',
  '10.png'
];
const pouredQuantity = 2.1; // 已经灌了的酒瓶数量
const pourQuantity = 0.2; // 本次灌了的酒的数量

const max = Math.ceil(pouredQuantity);
const startIndex = max === pouredQuantity ? max : max - 1; // 开始灌酒的酒瓶序号

/**
 * 灌酒递归方法
 * @param {Number} index 当前啤酒序号
 * @param {Number} leftQuantity 本次还剩多少可以灌的酒
 * @param {Number} total 总共酒量
 */
function recursion(index, leftQuantity, total) {
  if (leftQuantity === 0) {
    // 可以执行动画完成后的回调
    return;
  }
  const decimal = total - index === 0 ? 0 : calc(total, -index);
  new Promise(resolve => {
    const start = decimal === 0 ? 1 : decimal * 10 + 1;
    const end =
      decimal + leftQuantity >= 1
        ? pics.length
        : calc(decimal, leftQuantity) * 10;
    pourAnimation($bottles[index], start, end, resolve);
  }).then(() => {
    index++;
    const left =
      decimal + leftQuantity > 1 ? calc(leftQuantity, -calc(1, -decimal)) : 0;
    recursion(index, left, calc(total, calc(leftQuantity, -left)));
  });
}

recursion(startIndex, pourQuantity, pouredQuantity);

/**
 * 灌酒动画
 * @param {Element} ele
 * @param {Number} start
 * @param {Number} end
 * @param {Function} resolve
 */
function pourAnimation(ele, start, end, resolve) {
  let index = start - 1;
  (function loop() {
    ele.src = pics[index];
    index++;
    if (index < end) {
      setTimeout(loop, 300);
    } else {
      resolve();
    }
  })();
}

/**
 * 计算两个数的和 / 差,保留一位小数
 * @param {Number} a
 * @param {Number} b
 */
function calc(a, b) {
  return parseFloat((a + b).toFixed(1));
}

除了几个辅助方法外,核心代码都在递归的方法里,主要思路是先对啤酒的容量分级(这里是10等分),在一个啤酒的动画完成之后,调用 Promise 的 resolve 方法然后再去执行第下一个啤酒的动画,动画的实现过程就是用 setTimeout 去替换啤酒的图片而已。

总结

其实呢,任何问题想通之后也就那么回事,大多数问题都是在特定场景下寻求解决方案,那么我们需要做的就是先尽量思考问题的本质,想想痛点难点在哪,再去抽象化问题的层次,建模,那么慢慢,你会发现大部分问题你都曾遇到并解决过,问题也就不再是问题啦~(BTW,文中算法没怎么精简过,如果有更好的方法也可提出探讨)