从promise说说异步编程

最近ajax请求都是使用的axios,将其包装成了一个promise对象,因此可以直接使用thencatch方法来代替之前的回调函数,多个并发请求也可以利用all来使用,非常的方便。

promise就是针对异步编程的一种解决方案,我们之前经常需要在一个异步操作结束时进行下一步的操作,而传统的方式就是进行一层一层的回调嵌套,这种代码的可读性和维护性都是很差的,promise就把之前基于回调函数和事件的的传统方案以同步操作的流程表达出来,这样就可以避免多重的嵌套引起的回调地狱。基本的promise用法可以参考阮一峰的ECMAScript 6 入门

举个关于动画的例子,我们控制一个div点击按钮1s后开始动画,分为四步,先右移100px,到达后再下移200px,然后再左移100px,上移200px回到原始位置:

/* css */
div#d {
  background-color: red;
  transition: 1s;
  position: absolute;
  width: 100px;
  height: 100px;
  left: 0;
  top: 0;
}
//javascript
var d = document.getElementById('d');
var btn = document.getElementById('btn');

function moveDivTo(direction, range, callback) {
  setTimeout(function(){
    d.style[direction] = range + 'px';
    callback && callback();
  }, 1000)  ;
}

btn.addEventListener('click', function(){
  moveDivTo('left', 100, function(){
    moveDivTo('top', 200, function(){
      moveDivTo('left', 0, function(){
        moveDivTo('top', 0);
      });
    });
  });
});

可以看到如果执行的步数有很多的话,代码就需要一层一层进行嵌套,数量多了以后就非常痛苦和丑陋了。

所以为了适合人类进行阅读,我们会希望以同步的流程表达出来,比如这样:

moveDivTo(...).then(...).then(...).then(...)

使用promise的话,我们就可以把代码转换成这样:

var moveDivTo = function(direction, range) {
  var promise = new Promise((resolve, reject) => {
    setTimeout(() => {
      d.style[direction] = range + 'px';
      resolve();
    }, 1000)
  });
  return promise;
}

btn.onclick = function() {
  moveDivTo('left', 100)
    .then(function(){
      return moveDivTo('top', 200)
    })
    .then(function(){
      return moveDivTo('left', 0)
    })
    .then(function(){
      return moveDivTo('top', 0)
    })
}

为了深入理解,我们可以自己手动尝试实现一个最简单promise,这里为了简化,暂时不考虑出错的情况,我们把这个过程分为三个部分:

  1. 定义构造函数
  2. 包装我们的move函数
  3. 编写then方法

promise的本质是维护一个状态队列,使用内部的resolve(或reject)函数来改变状态并进行队列中的下一步:

var MyPromise = function(executor) {
  var self = this;
  // 使用一个数组来存放队列
  self.steps = [];
  
  // 通过resolve函数来执行传入的函数并设置状态
  executor(resolve);
  function resolve(value) {
    // 这里因为我们不考虑状态,所以省略了self.status = 'resolved'这一步。
    var fn = self.steps.shift();
    fn && fn(value);
  }
}

我们的move函数在包装时应该在位置的设定后执行resolve,这里其实和ES6的是一样的,为了链式的调用,需要在最后返回这个promise对象:

var moveDivTo = function(direction, range) {
  var promise =  new MyPromise(function(resolve){
    setTimeout(function(){
      d.style[direction] = range + 'px';
      resolve();
    }, 1000)
  });
  return promise;
}

then方法我们直接定义在原型对象上,因为不考虑其他情况,直接把then方法传入的回调函数放入队列之中,同样then方法返回的是一个新的promise,

MyPromise.prototype.then = function(func) {
  var self = this;
  return new Promise(function(resolve) {
    self.steps.push(function(value) {
      var x = func();
      // 如果返回的是一个promise对象,执行then方法传入resolve
      if (x instanceof MyPromise) {
        x.then(resolve);
      }
    });
  });
}

最后使用我们自己定义的MyPromise:

btn.onclick = function() {
  moveDivTo('left', 100)
    .then(function(){
      return moveDivTo('top', 200)
    })
    .then(function(){
      return moveDivTo('left', 0)
    })
    .then(function(){
      return moveDivTo('top', 0)
    })
}