Promise?
Promise.

@meathill

关于作者

Meathill

全栈工程师,编程爱好者

博客

['prɒmɪs]

n. 许诺,允诺;希望

The Promise object is used for asynchronous computations.

A Promise represents a value which may be available now, or in the future, or never.

--MDN

Promise 对象用于异步计算。

一个 Promise 表示一个现在、将来或永不可能可用的值。

--MDN 中文

扶额

Promise 对象

  • 用于异步计算。
  • 代表一个值,
  • 这个值可能现在就可以用;
  • 也可能将来才可以用;
  • 甚至永远不会存在。

扶额

Promise 对象

  • 主要用于异步计算。
  • 可以帮将异步操作队列化,
    按照期望的顺序执行,
    返回符合预期的结果。
  • 可以在对象之间传递和操作 Promise。

为什么会有 Promise?

JavaScript 包含了大量异步操作

  1. JavaScript 为检查表单而生。
  2. 创造它的首要目标是操作 DOM。
  3. 所以,JavaScript 会有异步操作。

为什么异步操作可以避免界面冻结呢?

假设你去到一家饭店,自己找座坐下了,然后招呼服务员拿菜单来。

服务员说:"对不起,我是'同步'服务员,我要服务完这张桌子才能招呼你。"

你是不是很想抽ta?

那一桌人明明已经吃上了,你只是想要菜单,这么小的一个动作,服务员却要你等到别人的一个大动作完成。

这就是"同步"的问题:

顺序交付的工作1234,必须按照1234的顺序完成。

异步,则是将耗时很长的 A 交付的工作交给系统之后,就去继续做 B 交付的工作。等到系统完成之后,再通过回调或者事件,继续做 A 剩下的工作。

从观察者的角度,看起来 AB 工作的完成顺序,和交付他们的时间顺序无关,所以叫"异步"。

浏览器中的 JavaScript

  1. 异步操作以事件为主
  2. 回调主要出现在 Ajax 和 File API
  3. 这个时候问题尚不算严重

有了 Node.js 之后

对异步的依赖进一步加剧了……

  1. 无阻塞高并发,是 Node.js 的招牌。
  2. 异步操作是其保障。
  3. 大量操作依赖回调函数。

稍有不慎,就会踏入“回调地狱”。

callback hell

假设需求:

遍历目录,找出最大的一个文件。

function findLargest(dir, callback) {
  fs.readdir(dir, function (err, files) {
    if (err) return callback(err);
    let count = files.length; // [1]
    let errored = false;
    let stats = [];
    files.forEach( file => {
      fs.stat(path.join(dir, file), (err, stat) => {
        if (errored) return; // [2]
        if (err) {
          errored = true;
          return callback(err);
        }
        stats.push(stat); // [3]

        if (--count === 0) { // [4]
          let largest = stats
            .filter(function (stat) { return stat.isFile(); })
            .reduce(function (prev, next) {
              if (prev.size > next.size) return prev;
              return next;
            });
          callback(null, files[stats.indexOf(largest)]);
        }
      });      
    });    
  });
}

findLargest('./path/to/dir', function (err, filename) {
  if (err) return console.error(err);
  console.log('largest file was:', filename);
});

回调有四个问题:

  1. 嵌套层次很深,难以维护
  2. 无法正常使用 returnthrow
  3. 无法正常检索堆栈信息
  4. 多个回调之间难以建立联系

有没有一种方案,既能保留异步在无阻塞上的优势,又能让我们写代码写的更舒服呢?

社区经过长时间探索,最终总结出:

Promise

  1. 可以很好的解决回调嵌套问题
  2. 代码阅读体验很好
  3. 不需要新的语言元素

Promise 详解

new Promise(
  /* 执行器 executor */
  function (resolve, reject) {
    // 一段耗时很长的异步操作

    resolve(); // 数据处理完成

    reject(); // 数据处理出错
  }
)
  .then(function A() { 
    // 成功,下一步
  }, function B() {
    // 失败,做相应处理
  });
  1. Promise 是一个 代理对象,它和原先的操作并无关系。
  2. Promise 有3个状态:
    1. pending [待定] 初始状态
    2. fulfilled [实现] 操作成功
    3. rejected [被否决] 操作失败
  3. Promise 实例一经创建,执行器立即执行。
  4. Promise 状态发生改变,就会触发 .then() 执行后续步骤。
  5. Promise 状态一经改变,不会再变。

接下来,看一个简单的范例

定时执行

./sample/timeout.js

再看一个

分两次,顺序依次执行

./sample/timeout2.js

假如一个 Promise 已经完成了,再 .then() 会怎样?

./sample/fulfilled-then.js

假如在 .then() 的函数里面不返回新的 Promise,会怎样?

./sample/timeout3.js

.then()

  1. .then() 接受两个函数作为参数,分别代表 fulfilledrejected
  2. .then() 返回一个新的 Promise 实例,所以它可以链式调用
  3. 当前面的 Promise 状态改变时,.then() 根据其最终状态,选择特定的状态响应函数执行
  4. 状态响应函数可以返回新的 Promise,或其它值
  5. 如果返回新的 Promise,那么下一级 .then() 会在新 Promise 状态改变之后执行
  6. 如果返回其它任何值,则会立刻执行下一级 .then()

.then() 里有 .then() 的情况

因为 .then() 返回的还是 Promise 实例。

会等里面的 .then() 执行完,在执行外面的。

对于我们来说,此时最好将其展开,会更好读。

./sample/nested-then.js

问题:下面的四种 promises 的区别是什么

// #1
doSomething().then(function () {
  return doSomethingElse();
});

// #2
doSomething().then(function () {
  doSomethingElse();
});

// #3
doSomething().then(doSomethingElse());

// #4
doSomething().then(doSomethingElse);
doSomething()
  .then(function () {
    return doSomethingElse();
  })
  .then(finalHandler);

答案:

doSomething
|-----------|
            doSomethingElse(undefined)
            |------------|
                         finalHandler(resultOfDoSomethingElse)
                         |------------|
doSomething()
  .then(function () {
    doSomethingElse();
  })
  .then(finalHandler);

答案:

doSomething
|-----------------|
                  doSomethingElse(undefined)
                  |------------------|
                  finalHandler(undefined)
                  |------------------|
doSomething()
  .then(doSomethingElse())
  .then(finalHandler);

答案:

doSomething
|-----------------|
doSomethingElse(undefined)
|---------------------------------|
                  finalHandler(resultOfDoSomething)
                  |------------------|
doSomething()
  .then(doSomethingElse)
  .then(finalHandler);

答案:

doSomething
|-----------|
            doSomethingElse(resultOfDoSomething)
            |------------|
                         finalHandler(resultOfDoSomethingElse)
                         |------------------|

注:以上4道题及答案均来自
We have a problem with promises

错误处理

Promise 会自动捕获内部异常,并交给 rejected 响应函数处理。

./sample/error.js
./sample/error-reject.js

通常有两种做法:

  1. reject('错误信息')
    .then(null, message => {})
  2. throw new Error('错误信息')
    .catch( message => {})

我推荐使用第二种,更加清晰,更加好读,并且可以捕获前面的错误。

来看一个稍微复杂一些的,捕获错误的范例:

.catch() + .then()

./sample/catch-then.js

注意:强烈建议在所有队列最后都加上 .catch(),以避免漏掉错误处理造成意想不到的问题。

doSomething()
  .doAnotherThing()
  .doMoreThing()
  .catch( err => {
    console.log(err);
  });

批量执行

Promise.all()

Promise.all([p1, p2, p3, ....]) 用于将多个 Promise 实例,包装成一个新的 Promise 实例。

  1. 它接受一个数组作为参数
  2. 数组里可以是 Promise 对象,也可以是别的值,只有 Promise 会等待状态改变
  3. 当所有子 Promise 都完成,该 Promise 完成,返回值是全部值的数组
  4. 有任何一个失败,该 Promise 失败,返回值是第一个失败的子 Promise 的结果
./sample/all.js

Promise.all() 最常见就是和 .map() 连用。

我们改造一下前面的例子。

./sample/map.js

实现队列

有时候我们不希望所有动作一起发生,而是按照一定顺序,逐个进行。

let promise = doSomething();
promise = promise.then(doSomethingElse);
promise = promise.then(doSomethingElse2);
promise = promise.then(doSomethingElse3);
....

使用 .forEach()

function queue(things) {
  let promise = Promise.resolve();
  things.forEach( thing => {
    promise = promise.then( () => {
      return new Promise( resolve => {
        doThing(thing, () => {
          resolve();
        });
      });
    });
  });
  return promise;
}

queue(['lots', 'of', 'things', ....]);

使用 .reduce()

function queue(things) {
  return things.reduce( (promise, thing) => {
    return promise.then( () => {
      return new Promise( resolve => {
        doThing(thing, () => {
          resolve();
        });
      });
    });
  }, Promise.resolve());
}

queue(['lots', 'of', 'things', ....]);

两个常见错误:

....
  things.forEach( thing => {
    promise.then( () => {
      return new Promise( resolve => {
        doThing(thing, () => {
          resolve();
        });
      });
    });
  });
....

没有把 .then() 产生的新 Promise 实例赋给 promise,没有生成队列。

function queue(things) {
  return things.reduce( (promise, thing) => {
    let step = new Promise( resolve => {
      doThing(thing, () => {
        resolve();
      });
    });
    return promise.then( step );
  }, Promise.resolve());
}

Promise 实例创建之后,会立刻运行执行器代码,所以这个也无法达成队列的效果。

假设需求:

开发一个爬虫,抓取某网站。

let url = ['http://blog.meathill.com/'];
function fetchAll(urls) {
  return urls.reduce((promise, url) => {
    return promise.then( () => {
      return fetch(url);
    });
  }, Promise.resolve());
}
function fetch(url) {
  return spider.fetch(url)
    .then( content => {
      return saveOrOther(content);
    })
    .then( content => {
      let links = spider.findLinks(content);
      return fetchAll(links);
    });
}
fetchAll(url);

进阶:使用 Generator 遍历

关于 Generator 的详情,请参阅相关文档

简而言之,

Generator 可以在执行中中断,并等待唤起。

let generator = function* (urls) {
  let loaded = [];
  while (urls.length > 0) {
    let url = urls.unshift();
    yield spider.fetch(url)
      .then( content => {
        loaded.push(url);
        return saveOrOther(content);
      })
      .then( content => {
        let links = spider.findLinks(content);
        links = _.without(links, loaded);
        urls = urls.concat(links);
      });
  }
  return 'over';
};

function fetch(urls) {
  let iterator = generator();

  function next() {
    let result = iterator.next();
    if (result.done) {
      return result.value;
    }
    let promise = iterator.next().value;
    promise.then(next);
  }

  next();
}

let urls = ['http://blog.meathill.com'];
fetch(urls);

Promise.resolve()

  1. 参数为空,返回一个状态为 fulfilled 的 Promise 实例
  2. 参数是一个跟 Promise 无关的值,同上,不过 fulfuilled 响应函数会得到这个参数
  3. 参数为 Promise 实例,则返回该实例,不做任何修改
  4. 参数为 thenable,立刻执行它的 .then()
./sample/resolve.js

Promise.reject()

Promise.reject() 会返回一个状态为 rejected 的 Promise 实例。

Promise.reject() 不认 thenable

./sample/reject.js

Promise.race()

Promise.race() 功能类似 Promise.all(),不过它是有一个完成就算完成。

./sample/race.js

现实中的 Promise

把回调包装成 Promise

把回调包装成 Promise 是最常见的应用。

它有两个显而易见的好处:

  1. 可读性更好
  2. 返回的结果可以加入任何 Promise 队列
./sample/wrap.js

把任何异步操作包装成 Promise

假设需求:用户点击按钮,弹出确认窗体,用户确认和取消有不同的处理。

样式问题,不能使用 window.confirm()

// 弹出窗体
let confirm = popupManager.confirm('您确定么?');
confirm.promise
  .then(() => {
    // do confirm staff
  })
  .catch(() => {
    // do cancel staff
  });

// 窗体的构造函数
class Confirm {
  constructor() {
    this.promise = new Promise( (resolve, reject) => {
      this.confirmButton.onClick = resolve;
      this.cancelBUtton.onClick = reject;
    });
  }
}

jQuery

jQuery 已经实现了 Promise。参见 jqXHR

$.ajax(url, {
  dataType: 'json'
})
  .then(json => {
    // 该干啥干啥
  });

IE...

如果你需要在 IE 中使用 Promise,有两个选择:

  1. jQuery.defered
  2. Promise polyfill

Fetch API

Fetch API 是 XMLHttpRequest 的现代化替代方案,它更强大,也更友好。

它直接返回一个 Promise 实例。

fetch('some.json')
  .then( response => {
    return response.json();
  })
  .then( json => {
    // do something with the json
  })
  .catch( err => {
    console.log(err);
  });

Promise 的支持情况

caniuse

放手用吧,少年!

回顾,总结

相对于传统的回调模式,Promise 有着巨大的进步,值得我们学习和使用。

  1. Promise 可以很好的解决异步回调不好写的问题
  2. Promise 可以使用队列,并且在对象之间传递
  3. Promise 不引入新语言元素,大部分浏览器已经原生支持,可以放心使用

Promise 的不足

  1. Promise 引入了不少新概念、新写法,学习成本不低
  2. Promise 也会有嵌套,可能看起来还很复杂
  3. Promise 没有真正解决 return/try/catch 的问题

async/await

ES2017 新增运算符,增加新的语言元素,赋予 JavaScript 以顺序手法编写异步脚本的能力。

  • 既能保留异步运算的无阻塞特性
  • 还能使用同步写法
  • 还能正常使用 return/try/catch

async/await 仍然需要 Promise!

function resolveAfter2Seconds(x) {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(x);
    }, 2000);
  });
}

async function f1() {
  var x = await resolveAfter2Seconds(10);
  console.log(x); // 10
}
f1();

具体的内容请参考 MDN async 文档await 文档

一些 tips

这是我犯过的一些错误,希望成为大家前车之鉴。

  • .resolve() .reject() 不会自动 return
  • Promise 里必须 .resolve() .reject() throw err 才会改变状态,.then() 不需要。
  • .resolve() 只会返回一个值,返回多个值请用数组或对象。

Q&A

参考: