您可能属于以下其中某一类,本文中的相关概念

作者: 前端  发布:2019-11-21

Promise 异步流程控制

2017/10/04 · JavaScript · Promise

原文出处: 麦子谷   

JavaScript Event Loop 机制详解与 Vue.js 中实践应用

2017/09/09 · CSS · Event Loop, Vue

原文出处: 王下邀月熊   

JavaScript Event Loop 机制详解与 Vue.js 中实践应用归纳于笔者的现代 JavaScript 开发:语法基础与实践技巧系列文章。本文依次介绍了函数调用栈、MacroTask 与 MicroTask 执行顺序、浅析 Vue.js 中 nextTick 实现等内容;本文中引用的参考资料统一声明在 JavaScript 学习与实践资料索引。

=

me Dev Summit, happening on Oct 23rd and 24th. Learn more.

9159.com 1前言

最近部门在招前端,作为部门唯一的前端,面试了不少应聘的同学,面试中有一个涉及 Promise 的一个问题是:

网页中预加载20张图片资源,分步加载,一次加载10张,两次完成,怎么控制图片请求的并发,怎样感知当前异步请求是否已完成?

然而能全部答上的很少,能够给出一个回调 + 计数版本的,我都觉得合格了。那么接下来就一起来学习总结一下基于 Promise 来处理异步的三种方法。

本文的例子是一个极度简化的一个漫画阅读器,用4张漫画图的加载来介绍异步处理不同方式的实现和差异,以下是 HTML 代码:

JavaScript

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Promise</title> <style> .pics{ width: 300px; margin: 0 auto; } .pics img{ display: block; width: 100%; } .loading{ text-align: center; font-size: 14px; color: #111; } </style> </head> <body> <div class="wrap"> <div class="loading">正在加载...</div> <div class="pics"> </div> </div> <script> </script> </body> </html>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Promise</title>
  <style>
    .pics{
      width: 300px;
      margin: 0 auto;
    }
    .pics img{
      display: block;
      width: 100%;
    }
    .loading{
      text-align: center;
      font-size: 14px;
      color: #111;
    }
  </style>
</head>
<body>
  <div class="wrap">
    <div class="loading">正在加载...</div>
    <div class="pics">
    </div>
  </div>
  <script>
  </script>
</body>
</html>

1. 事件循环机制详解与实践应用

JavaScript 是典型的单线程单并发语言,即表示在同一时间片内其只能执行单个任务或者部分代码片。换言之,我们可以认为某个同域浏览器上下中 JavaScript 主线程拥有一个函数调用栈以及一个任务队列(参考 whatwg 规范);主线程会依次执行代码,当遇到函数时,会先将函数入栈,函数运行完毕后再将该函数出栈,直到所有代码执行完毕。当函数调用栈为空时,运行时即会根据事件循环(Event Loop)机制来从任务队列中提取出待执行的回调并执行,执行的过程同样会进行函数帧的入栈出栈操作。每个线程有自己的事件循环,所以每个 Web Worker有自己的,所以它才可以独立执行。然而,所有同属一个 origin 的窗体都共享一个事件循环,所以它们可以同步交流。

Event Loop(事件循环)并不是 JavaScript 中独有的,其广泛应用于各个领域的异步编程实现中;所谓的 Event Loop 即是一系列回调函数的集合,在执行某个异步函数时,会将其回调压入队列中,JavaScript 引擎会在异步代码执行完毕后开始处理其关联的回调。

9159.com 2

在 Web 开发中,我们常常会需要处理网络请求等相对较慢的操作,如果将这些操作全部以同步阻塞方式运行无疑会大大降低用户界面的体验。另一方面,我们点击某些按钮之后的响应事件可能会导致界面重渲染,如果因为响应事件的执行而阻塞了界面的渲染,同样会影响整体性能。实际开发中我们会采用异步回调来处理这些操作,这种调用者与响应之间的解耦保证了 JavaScript 能够在等待异步操作完成之前仍然能够执行其他的代码。Event Loop 正是负责执行队列中的回调并且将其压入到函数调用栈中,其基本的代码逻辑如下所示:

JavaScript

while (queue.waitForMessage()) { queue.processNextMessage(); }

1
2
3
while (queue.waitForMessage()) {
  queue.processNextMessage();
}

完整的浏览器中 JavaScript 事件循环机制图解如下:9159.com 3

在 Web 浏览器中,任何时刻都有可能会有事件被触发,而仅有那些设置了回调的事件会将其相关的任务压入到任务队列中。回调函数被调用时即会在函数调用栈中创建初始帧,而直到整个函数调用栈清空之前任何产生的任务都会被压入到任务队列中延后执行;顺序的同步函数调用则会创建新的栈帧。总结而言,浏览器中的事件循环机制阐述如下:

  • 浏览器内核会在其它线程中执行异步操作,当操作完成后,将操作结果以及事先定义的回调函数放入 JavaScript 主线程的任务队列中。
  • JavaScript 主线程会在执行栈清空后,读取任务队列,读取到任务队列中的函数后,将该函数入栈,一直运行直到执行栈清空,再次去读取任务队列,不断循环。
  • 当主线程阻塞时,任务队列仍然是能够被推入任务的。这也就是为什么当页面的 JavaScript 进程阻塞时,我们触发的点击等事件,会在进程恢复后依次执行。

前言

本文旨在简单讲解一下javascript中的Promise对象的概念,特性与简单的使用方法。并在文末会附上一份符合PromiseA+规范的Promise对象的完整实现。

注:本文中的相关概念均基于PromiseA+规范。

相关参考

JavaScript Promise迷你书

Promise/A+规范


JavaScript Promise:简介

单一请求

最简单的,就是将异步一个个来处理,转为一个类似同步的方式来处理。 先来简单的实现一个单个 Image 来加载的 thenable 函数和一个处理函数返回结果的函数。

JavaScript

function loadImg (url) { return new Promise((resolve, reject) => { const img = new Image() img.onload = function () { resolve(img) } img.onerror = reject img.src = url }) }

1
2
3
4
5
6
7
8
9
10
function loadImg (url) {
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = function () {
      resolve(img)
    }
    img.onerror = reject
    img.src = url
  })
}

异步转同步的解决思想是:当第一个 loadImg(urls[1]) 完成后再调用 loadImg(urls[2]),依次往下。如果 loadImg() 是一个同步函数,那么很自然的想到用__循环__。

JavaScript

for (let i = 0; i < urls.length; i++) { loadImg(urls[i]) }

1
2
3
for (let i = 0; i < urls.length; i++) {
  loadImg(urls[i])
}

当 loadImg() 为异步时,我们就只能用 Promise chain 来实现,最终形成这种方式的调用:

JavaScript

loadImg(urls[0]) .then(addToHtml) .then(()=>loadImg(urls[1])) .then(addToHtml) //... .then(()=>loadImg(urls[3])) .then(addToHtml)

1
2
3
4
5
6
7
loadImg(urls[0])
  .then(addToHtml)
  .then(()=>loadImg(urls[1]))
  .then(addToHtml)
  //...
  .then(()=>loadImg(urls[3]))
  .then(addToHtml)

那我们用一个中间变量来存储当前的 promise ,就像链表的游标一样,改过后的 for 循环代码如下:

JavaScript

let promise = Promise.resolve() for (let i = 0; i < urls.length; i++) { promise = promise .then(()=>loadImg(urls[i])) .then(addToHtml) }

1
2
3
4
5
6
let promise = Promise.resolve()
for (let i = 0; i < urls.length; i++) {
promise = promise
.then(()=>loadImg(urls[i]))
.then(addToHtml)
}

promise 变量就像是一个迭代器,不断指向最新的返回的 Promise,那我们就进一步使用 reduce 来简化代码。

JavaScript

urls.reduce((promise, url) => { return promise .then(()=>loadImg(url)) .then(addToHtml) }, Promise.resolve())

1
2
3
4
5
urls.reduce((promise, url) => {
  return promise
    .then(()=>loadImg(url))
    .then(addToHtml)
}, Promise.resolve())

在程序设计中,是可以通过函数的__递归__来实现循环语句的。所以我们将上面的代码改成__递归__:

JavaScript

function syncLoad (index) { if (index >= urls.length) return loadImg(urls[index]).then(img => { // process img addToHtml(img) syncLoad (index + 1) }) } // 调用 syncLoad(0)

1
2
3
4
5
6
7
8
9
10
11
function syncLoad (index) {
  if (index >= urls.length) return
      loadImg(urls[index]).then(img => {
      // process img
      addToHtml(img)
      syncLoad (index + 1)
    })
}
 
// 调用
syncLoad(0)

好了一个简单的异步转同步的实现方式就已经完成,我们来测试一下。 这个实现的简单版本已经实现没问题,但是最上面的正在加载还在,那我们怎么在函数外部知道这个递归的结束,并隐藏掉这个 DOM 呢?Promise.then() 同样返回的是 thenable 函数 我们只需要在 syncLoad 内部传递这条 Promise 链,直到最后的函数返回。

JavaScript

function syncLoad (index) { if (index >= urls.length) return Promise.resolve() return loadImg(urls[index]) .then(img => { addToHtml(img) return syncLoad (index + 1) }) } // 调用 syncLoad(0) .then(() => { document.querySelector('.loading').style.display = 'none' })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function syncLoad (index) {
  if (index >= urls.length) return Promise.resolve()
  return loadImg(urls[index])
    .then(img => {
      addToHtml(img)
      return syncLoad (index + 1)
    })
}
 
// 调用
syncLoad(0)
  .then(() => {
  document.querySelector('.loading').style.display = 'none'
})

现在我们再来完善一下这个函数,让它更加通用,它接受__异步函数__、异步函数需要的参数数组、__异步函数的回调函数__三个参数。并且会记录调用失败的参数,在最后返回到函数外部。另外大家可以思考一下为什么 catch 要在最后的 then 之前。

JavaScript

function syncLoad (fn, arr, handler) { if (typeof fn !== 'function') throw TypeError('第一个参数必须是function') if (!Array.isArray(arr)) throw TypeError('第二个参数必须是数组') handler = typeof fn === 'function' ? handler : function () {} const errors = [] return load(0) function load (index) { if (index >= arr.length) { return errors.length > 0 ? Promise.reject(errors) : Promise.resolve() } return fn(arr[index]) .then(data => { handler(data) }) .catch(err => { console.log(err) errors.push(arr[index]) return load(index + 1) }) .then(() => { return load (index + 1) }) } } // 调用 syncLoad(loadImg, urls, addToHtml) .then(() => { document.querySelector('.loading').style.display = 'none' }) .catch(console.log)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
function syncLoad (fn, arr, handler) {
  if (typeof fn !== 'function') throw TypeError('第一个参数必须是function')
  if (!Array.isArray(arr)) throw TypeError('第二个参数必须是数组')
  handler = typeof fn === 'function' ? handler : function () {}
  const errors = []
  return load(0)
  function load (index) {
    if (index >= arr.length) {
      return errors.length > 0 ? Promise.reject(errors) : Promise.resolve()
    }
    return fn(arr[index])
      .then(data => {
        handler(data)
      })
      .catch(err => {
        console.log(err)              
        errors.push(arr[index])
        return load(index + 1)
      })
      .then(() => {
        return load (index + 1)
      })
  }
}
 
// 调用
syncLoad(loadImg, urls, addToHtml)
  .then(() => {
    document.querySelector('.loading').style.display = 'none'
  })
  .catch(console.log)

demo1地址:单一请求 – 多个 Promise 同步化

至此,这个函数还是有挺多不通用的问题,比如:处理函数必须一致,不能是多种不同的异步函数组成的队列,异步的回调函数也只能是一种等。关于这种方式的更详细的描述可以看我之前写的一篇文章 Koa引用库之Koa-compose。

当然这种异步转同步的方式在这一个例子中并不是最好的解法,但当有合适的业务场景的时候,这是很常见的解决方案。

2. 函数调用栈与任务队列

在变量作用域与提升一节中我们介绍过所谓执行上下文(Execution Context)的概念,在 JavaScript 代码执行过程中,我们可能会拥有一个全局上下文,多个函数上下文或者块上下文;每个函数调用都会创造新的上下文与局部作用域。而这些执行上下文堆叠就形成了所谓的执行上下文栈(Execution Context Stack),便如上文介绍的 JavaScript 是单线程事件循环机制,同时刻仅会执行单个事件,而其他事件都在所谓的执行栈中排队等待:9159.com 4

而从 JavaScript 内存模型的角度,我们可以将内存划分为调用栈(Call Stack)、堆(Heap)以及队列(Queue)等几个部分:9159.com 5

其中的调用栈会记录所有的函数调用信息,当我们调用某个函数时,会将其参数与局部变量等压入栈中;在执行完毕后,会弹出栈首的元素。而堆则存放了大量的非结构化数据,譬如程序分配的变量与对象。队列则包含了一系列待处理的信息与相关联的回调函数,每个 JavaScript 运行时都必须包含一个任务队列。当调用栈为空时,运行时会从队列中取出某个消息并且执行其关联的函数(也就是创建栈帧的过程);运行时会递归调用函数并创建调用栈,直到函数调用栈全部清空再从任务队列中取出消息。换言之,譬如按钮点击或者 HTTP 请求响应都会作为消息存放在任务队列中;需要注意的是,仅当这些事件的回调函数存在时才会被放入任务队列,否则会被直接忽略。

譬如对于如下的代码块:

JavaScript

function fire() { const result = sumSqrt(3, 4) console.log(result); } function sumSqrt(x, y) { const s1 = square(x) const s2 = square(y) const sum = s1 + s2; return Math.sqrt(sum) } function square(x) { return x * x; } fire()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function fire() {
    const result = sumSqrt(3, 4)
    console.log(result);
}
function sumSqrt(x, y) {
    const s1 = square(x)
    const s2 = square(y)
    const sum = s1 + s2;
    return Math.sqrt(sum)
}
function square(x) {
    return x * x;
}
 
fire()

其对应的函数调用图(整理自这里)为:9159.com 6

这里还值得一提的是,Promise.then 是异步执行的,而创建 Promise 实例 (executor) 是同步执行的,譬如下述代码:

JavaScript

(function test() { setTimeout(function() {console.log(4)}, 0); new Promise(function executor(resolve) { console.log(1); for( var i=0 ; i<10000 ; i++ ) { i == 9999 && resolve(); } console.log(2); }).then(function() { console.log(5); }); console.log(3); })() // 输出结果为: // 1 // 2 // 3 // 5 // 4

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
(function test() {
    setTimeout(function() {console.log(4)}, 0);
    new Promise(function executor(resolve) {
        console.log(1);
        for( var i=0 ; i<10000 ; i++ ) {
            i == 9999 && resolve();
        }
        console.log(2);
    }).then(function() {
        console.log(5);
    });
    console.log(3);
})()
// 输出结果为:
// 1
// 2
// 3
// 5
// 4

我们可以参考 Promise 规范中有关于 promise.then 的部分:

JavaScript

promise.then(onFulfilled, onRejected) 2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1]. Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

1
2
3
4
5
promise.then(onFulfilled, onRejected)
 
2.2.4 onFulfilled or onRejected must not be called until the execution context stack contains only platform code. [3.1].
 
Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, and with a fresh stack. This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, or with a “micro-task” mechanism such as MutationObserver or process.nextTick. Since the promise implementation is considered platform code, it may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.

规范要求,onFulfilled 必须在执行上下文栈(Execution Context Stack) 只包含 平台代码(platform code) 后才能执行。平台代码指引擎,环境,Promise 实现代码等。实践上来说,这个要求保证了 onFulfilled 的异步执行(以全新的栈),在 then 被调用的这个事件循环之后。

正文

9159.com 7

并发请求

毕竟同一域名下能够并发多个 HTTP 请求,对于这种不需要按顺序加载,只需要按顺序来处理的并发请求,Promise.all 是最好的解决办法。因为Promise.all 是原生函数,我们就引用文档来解释一下。

Promise.all(iterable) 方法指当所有在可迭代参数中的 promises 已完成,或者第一个传递的 promise(指 reject)失败时,返回 promise。
出自 Promise.all() – JavaScript | MDN

那我们就把demo1中的例子改一下:

JavaScript

const promises = urls.map(loadImg) Promise.all(promises) .then(imgs => { imgs.forEach(addToHtml) document.querySelector('.loading').style.display = 'none' }) .catch(err => { console.error(err, 'Promise.all 当其中一个出现错误,就会reject。') })

1
2
3
4
5
6
7
8
9
const promises = urls.map(loadImg)
Promise.all(promises)
  .then(imgs => {
    imgs.forEach(addToHtml)
    document.querySelector('.loading').style.display = 'none'
  })
  .catch(err => {
    console.error(err, 'Promise.all 当其中一个出现错误,就会reject。')
  })

demo2地址:并发请求 – Promise.all

3. MacroTask(Task) 与 MicroTask(Job)

在面试中我们常常会碰到如下的代码题,其主要就是考校 JavaScript 不同任务的执行先后顺序:

JavaScript

// 测试代码 console.log('main1'); // 该函数仅在 Node.js 环境下可以使用 process.nextTick(function() { console.log('process.nextTick1'); }); setTimeout(function() { console.log('setTimeout'); process.nextTick(function() { console.log('process.nextTick2'); }); }, 0); new Promise(function(resolve, reject) { console.log('promise'); resolve(); }).then(function() { console.log('promise then'); }); console.log('main2'); // 执行结果 main1 promise main2 process.nextTick1 promise then setTimeout process.nextTick2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 测试代码
console.log('main1');
 
// 该函数仅在 Node.js 环境下可以使用
process.nextTick(function() {
    console.log('process.nextTick1');
});
 
setTimeout(function() {
    console.log('setTimeout');
    process.nextTick(function() {
        console.log('process.nextTick2');
    });
}, 0);
 
new Promise(function(resolve, reject) {
    console.log('promise');
    resolve();
}).then(function() {
    console.log('promise then');
});
 
console.log('main2');
 
// 执行结果
main1
promise
main2
process.nextTick1
promise then
setTimeout
process.nextTick2

我们在前文中已经介绍过 JavaScript 的主线程在遇到异步调用时,这些异步调用会立刻返回某个值,从而让主线程不会在此处阻塞。而真正的异步操作会由浏览器执行,主线程则会在清空当前调用栈后,按照先入先出的顺序读取任务队列里面的任务。而 JavaScript 中的任务又分为 MacroTask 与 MicroTask 两种,在 ES2015 中 MacroTask 即指 Task,而 MicroTask 则是指代 Job。典型的 MacroTask 包含了 setTimeout, setInterval, setImmediate, requestAnimationFrame, I/O, UI rendering 等,MicroTask 包含了 process.nextTick, Promises, Object.observe, MutationObserver 等。 二者的关系可以图示如下:9159.com 8

参考 whatwg 规范 中的描述:一个事件循环(Event Loop)会有一个或多个任务队列(Task Queue,又称 Task Source),这里的 Task Queue 就是 MacroTask Queue,而 Event Loop 仅有一个 MicroTask Queue。每个 Task Queue 都保证自己按照回调入队的顺序依次执行,所以浏览器可以从内部到JS/DOM,保证动作按序发生。而在 Task 的执行之间则会清空已有的 MicroTask 队列,在 MacroTask 或者 MicroTask 中产生的 MicroTask 同样会被压入到 MicroTask 队列中并执行。参考如下代码:

JavaScript

function foo() { console.log("Start of queue"); bar(); setTimeout(function() { console.log("Middle of queue"); }, 0); Promise.resolve().then(function() { console.log("Promise resolved"); Promise.resolve().then(function() { console.log("Promise resolved again"); }); }); console.log("End of queue"); } function bar() { setTimeout(function() { console.log("Start of next queue"); }, 0); setTimeout(function() { console.log("End of next queue"); }, 0); } foo(); // 输出 Start of queue End of queue Promise resolved Promise resolved again Start of next queue End of next queue Middle of queue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function foo() {
  console.log("Start of queue");
  bar();
  setTimeout(function() {
    console.log("Middle of queue");
  }, 0);
  Promise.resolve().then(function() {
    console.log("Promise resolved");
    Promise.resolve().then(function() {
      console.log("Promise resolved again");
    });
  });
  console.log("End of queue");
}
 
function bar() {
  setTimeout(function() {
    console.log("Start of next queue");
  }, 0);
  setTimeout(function() {
    console.log("End of next queue");
  }, 0);
}
 
foo();
 
// 输出
Start of queue
End of queue
Promise resolved
Promise resolved again
Start of next queue
End of next queue
Middle of queue

上述代码中首个 TaskQueue 即为 foo(),foo() 又调用了 bar() 构建了新的 TaskQueue,bar() 调用之后 foo() 又产生了 MicroTask 并被压入了唯一的 MicroTask 队列。我们最后再总计下 JavaScript MacroTask 与 MicroTask 的执行顺序,当执行栈(call stack)为空的时候,开始依次执行:

《这一段在我笔记里也放了好久,无法确定是否拷贝的。。。如果有哪位发现请及时告知。。。(*ฅ́˘ฅ̀*)♡》

  1. 把最早的任务(task A)放入任务队列
  2. 如果 task A 为null (那任务队列就是空),直接跳到第6步
  3. 将 currently running task 设置为 task A
  4. 执行 task A (也就是执行回调函数)
  5. 将 currently running task 设置为 null 并移出 task A
  6. 执行 microtask 队列
  • a: 在 microtask 中选出最早的任务 task X
  • b: 如果 task X 为null (那 microtask 队列就是空),直接跳到 g
  • c: 将 currently running task 设置为 task X
  • d: 执行 task X
  • e: 将 currently running task 设置为 null 并移出 task X
  • f: 在 microtask 中选出最早的任务 , 跳到 b
  • g: 结束 microtask 队列
  1. 跳到第一步

  2. 浅析 Vue.js 中 nextTick 的实现


在 Vue.js 中,其会异步执行 DOM 更新;当观察到数据变化时,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会一次推入到队列中。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际(已去重的)工作。Vue 在内部尝试对异步队列使用原生的 Promise.then 和 MutationObserver,如果执行环境不支持,会采用 setTimeout(fn, 0) 代替。

《因为本人失误,原来此处内容拷贝了 https://www.zhihu.com/question/55364497 这个回答,造成了侵权,深表歉意,已经删除,后续我会在 github 链接上重写本段》

而当我们希望在数据更新之后执行某些 DOM 操作,就需要使用 nextTick 函数来添加回调:

JavaScript

// HTML <div id="example">{{message}}</div> // JS var vm = new Vue({ el: '#example', data: { message: '123' } }) vm.message = 'new message' // 更改数据 vm.$el.textContent === 'new message' // false Vue.nextTick(function () { vm.$el.textContent === 'new message' // true })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// HTML
<div id="example">{{message}}</div>
 
// JS
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
  vm.$el.textContent === 'new message' // true
})

在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue ,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:

JavaScript

Vue.component('example', { template: '<span>{{ message }}</span>', data: function () { return { message: '没有更新' } }, methods: { updateMessage: function () { this.message = '更新完成' console.log(this.$el.textContent) // => '没有更新' this.$nextTick(function () { console.log(this.$el.textContent) // => '更新完成' }) } } })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '没有更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '更新完成'
      console.log(this.$el.textContent) // => '没有更新'
      this.$nextTick(function () {
        console.log(this.$el.textContent) // => '更新完成'
      })
    }
  }
})

src/core/util/env

JavaScript

/** * 使用 MicroTask 来异步执行批次任务 */ export const nextTick = (function() { // 需要执行的回调列表 const callbacks = []; // 是否处于挂起状态 let pending = false; // 时间函数句柄 let timerFunc; // 执行并且清空所有的回调列表 function nextTickHandler() { pending = false; const copies = callbacks.slice(0); callbacks.length = 0; for (let i = 0; i < copies.length; i++) { copies[i](); } } // nextTick 的回调会被加入到 MicroTask 队列中,这里我们主要通过原生的 Promise 与 MutationObserver 实现 /* istanbul ignore if */ if (typeof Promise !== 'undefined' && isNative(Promise)) { let p = Promise.resolve(); let logError = err => { console.error(err); }; timerFunc = () => { p.then(nextTickHandler).catch(logError); // 在部分 iOS 系统下的 UIWebViews 中,Promise.then 可能并不会被清空,因此我们需要添加额外操作以触发 if (isIOS) setTimeout(noop); }; } else if ( typeof MutationObserver !== 'undefined' && (isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === '[object MutationObserverConstructor]') ) { // 当 Promise 不可用时候使用 MutationObserver // e.g. PhantomJS IE11, iOS7, Android 4.4 let counter = 1; let observer = new MutationObserver(nextTickHandler); let textNode = document.createTextNode(String(counter)); observer.observe(textNode, { characterData: true }); timerFunc = () => { counter = (counter + 1) % 2; textNode.data = String(counter); }; } else { // 如果都不存在,则回退使用 setTimeout /* istanbul ignore next */ timerFunc = () => { setTimeout(nextTickHandler, 0); }; } return function queueNextTick(cb?: Function, ctx?: Object) { let _resolve; callbacks.push(() => { if (cb) { try { cb.call(ctx); } catch (e) { handleError(e, ctx, 'nextTick'); } } else if (_resolve) { _resolve(ctx); } }); if (!pending) { pending = true; timerFunc(); } // 如果没有传入回调,则表示以异步方式调用 if (!cb && typeof Promise !== 'undefined') { return new Promise((resolve, reject) => { _resolve = resolve; }); } }; })();

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
/**
* 使用 MicroTask 来异步执行批次任务
*/
export const nextTick = (function() {
  // 需要执行的回调列表
  const callbacks = [];
 
  // 是否处于挂起状态
  let pending = false;
 
  // 时间函数句柄
  let timerFunc;
 
  // 执行并且清空所有的回调列表
  function nextTickHandler() {
    pending = false;
    const copies = callbacks.slice(0);
    callbacks.length = 0;
    for (let i = 0; i < copies.length; i++) {
      copies[i]();
    }
  }
 
  // nextTick 的回调会被加入到 MicroTask 队列中,这里我们主要通过原生的 Promise 与 MutationObserver 实现
  /* istanbul ignore if */
  if (typeof Promise !== 'undefined' && isNative(Promise)) {
    let p = Promise.resolve();
    let logError = err => {
      console.error(err);
    };
    timerFunc = () => {
      p.then(nextTickHandler).catch(logError);
 
      // 在部分 iOS 系统下的 UIWebViews 中,Promise.then 可能并不会被清空,因此我们需要添加额外操作以触发
      if (isIOS) setTimeout(noop);
    };
  } else if (
    typeof MutationObserver !== 'undefined' &&
    (isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]')
  ) {
    // 当 Promise 不可用时候使用 MutationObserver
    // e.g. PhantomJS IE11, iOS7, Android 4.4
    let counter = 1;
    let observer = new MutationObserver(nextTickHandler);
    let textNode = document.createTextNode(String(counter));
    observer.observe(textNode, {
      characterData: true
    });
    timerFunc = () => {
      counter = (counter + 1) % 2;
      textNode.data = String(counter);
    };
  } else {
    // 如果都不存在,则回退使用 setTimeout
    /* istanbul ignore next */
    timerFunc = () => {
      setTimeout(nextTickHandler, 0);
    };
  }
 
  return function queueNextTick(cb?: Function, ctx?: Object) {
    let _resolve;
    callbacks.push(() => {
      if (cb) {
        try {
          cb.call(ctx);
        } catch (e) {
          handleError(e, ctx, 'nextTick');
        }
      } else if (_resolve) {
        _resolve(ctx);
      }
    });
    if (!pending) {
      pending = true;
      timerFunc();
    }
 
    // 如果没有传入回调,则表示以异步方式调用
    if (!cb && typeof Promise !== 'undefined') {
      return new Promise((resolve, reject) => {
        _resolve = resolve;
      });
    }
  };
})();

1.Promise简介

在了解javescript中的Promise实现之前有必要先了解一下Promise的概念。

Jake Archibald

并发请求,按顺序处理结果

Promise.all 虽然能并发多个请求,但是一旦其中某一个 promise 出错,整个 promise 会被 reject 。 webapp 里常用的资源预加载,可能加载的是 20 张逐帧图片,当网络出现问题, 20 张图难免会有一两张请求失败,如果失败后,直接抛弃其他被 resolve 的返回结果,似乎有点不妥,我们只要知道哪些图片出错了,把出错的图片再做一次请求或着用占位图补上就好。 上节中的代码 const promises = urls.map(loadImg) 运行后,全部都图片请求都已经发出去了,我们只要按顺序挨个处理 promises 这个数组中的 Promise 实例就好了,先用一个简单点的 for 循环来实现以下,跟第二节中的单一请求一样,利用 Promise 链来顺序处理。

JavaScript

let task = Promise.resolve() for (let i = 0; i < promises.length; i++) { task = task.then(() => promises[i]).then(addToHtml) }

1
2
3
4
let task = Promise.resolve()
for (let i = 0; i < promises.length; i++) {
  task = task.then(() => promises[i]).then(addToHtml)
}

改成 reduce 版本

JavaScript

promises.reduce((task, imgPromise) => { return task.then(() => imgPromise).then(addToHtml) }, Promise.resolve())

1
2
3
promises.reduce((task, imgPromise) => {
  return task.then(() => imgPromise).then(addToHtml)
}, Promise.resolve())

demo3地址:Promise 并发请求,顺序处理结果

5. 延伸阅读

  • 深入浅出 Node.js 全栈架构 – Node.js 事件循环机制详解与实践

    1 赞 3 收藏 评论

9159.com 9

什么是Promise?

关于Promise概念的解释,网上的各种资料众说纷纭,这里奉上笔者自己的理解。简单来说,Promise就是一套处理异步事件的方式和流程。promise在英文中的含义是约定,而针对异步事件特性的处理方式与这个含义非常吻合。

By

控制最大并发数

现在我们来试着完成一下上面的笔试题,这个其实都__不需要控制最大并发数__。 20张图,分两次加载,那用两个 Promise.all 不就解决了?但是用 Promise.all没办法侦听到每一张图片加载完成的事件。而用上一节的方法,我们既能并发请求,又能按顺序响应图片加载完成的事件。

JavaScript

let index = 0 const step1 = [], step2 = [] while(index < 10) { step1.push(loadImg(`./images/pic/${index}.jpg`)) index += 1 } step1.reduce((task, imgPromise, i) => { return task .then(() => imgPromise) .then(() => { console.log(`第 ${i + 1} 张图片加载完成.`) }) }, Promise.resolve()) .then(() => { console.log('>> 前面10张已经加载完!') }) .then(() => { while(index < 20) { step2.push(loadImg(`./images/pic/${index}.jpg`)) index += 1 } return step2.reduce((task, imgPromise, i) => { return task .then(() => imgPromise) .then(() => { console.log(`第 ${i + 11} 张图片加载完成.`) }) }, Promise.resolve()) }) .then(() => { console.log('>> 后面10张已经加载完') })

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
let index = 0
const step1 = [], step2 = []
 
while(index < 10) {
  step1.push(loadImg(`./images/pic/${index}.jpg`))
  index += 1
}
 
step1.reduce((task, imgPromise, i) => {
  return task
    .then(() => imgPromise)
    .then(() => {
      console.log(`第 ${i + 1} 张图片加载完成.`)
    })
}, Promise.resolve())
  .then(() => {
    console.log('>> 前面10张已经加载完!')
  })
  .then(() => {
    while(index < 20) {
      step2.push(loadImg(`./images/pic/${index}.jpg`))
      index += 1
    }
    return step2.reduce((task, imgPromise, i) => {
      return task
        .then(() => imgPromise)
        .then(() => {
          console.log(`第 ${i + 11} 张图片加载完成.`)
        })
    }, Promise.resolve())
  })
  .then(() => {
    console.log('>> 后面10张已经加载完')
  })

上面的代码是针对题目的 hardcode ,如果笔试的时候能写出这个,都已经是非常不错了,然而并没有一个人写出来,said…

demo4地址(看控制台和网络请求):Promise 分步加载 – 1

那么我们在抽象一下代码,写一个通用的方法出来,这个函数返回一个 Promise,还可以继续处理全部都图片加载完后的异步回调。

JavaScript

function stepLoad (urls, handler, stepNum) { const createPromises = function (now, stepNum) { let last = Math.min(stepNum + now, urls.length) return urls.slice(now, last).map(handler) } let step = Promise.resolve() for (let i = 0; i < urls.length; i += stepNum) { step = step .then(() => { let promises = createPromises(i, stepNum) return promises.reduce((task, imgPromise, index) => { return task .then(() => imgPromise) .then(() => { console.log(`第 ${index + 1

  • i} 张图片加载完成.`) }) }, Promise.resolve()) }) .then(() => { let current = Math.min(i + stepNum, urls.length) console.log(`>> 总共${current}张已经加载完!`) }) } return step }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function stepLoad (urls, handler, stepNum) {
const createPromises = function (now, stepNum) {
    let last = Math.min(stepNum + now, urls.length)
    return urls.slice(now, last).map(handler)
  }
  let step = Promise.resolve()
  for (let i = 0; i < urls.length; i += stepNum) {
    step = step
      .then(() => {
        let promises = createPromises(i, stepNum)
        return promises.reduce((task, imgPromise, index) => {
          return task
            .then(() => imgPromise)
            .then(() => {
              console.log(`第 ${index + 1 + i} 张图片加载完成.`)
            })
        }, Promise.resolve())
      })
      .then(() => {
        let current = Math.min(i + stepNum, urls.length)
        console.log(`>> 总共${current}张已经加载完!`)
      })
  }
return step
}

上面代码里的 for 也可以改成 reduce ,不过需要先将需要加载的 urls 按分步的数目,划分成数组,感兴趣的朋友可以自己写写看。

demo5地址(看控制台和网络请求):Promise 分步 – 2

但上面的实现和我们说的__最大并发数控制__没什么关系啊,最大并发数控制是指:当加载 20 张图片加载的时候,先并发请求 10 张图片,当一张图片加载完成后,又会继续发起一张图片的请求,让并发数保持在 10 个,直到需要加载的图片都全部发起请求。这个在写爬虫中可以说是比较常见的使用场景了。 那么我们根据上面的一些知识,我们用两种方式来实现这个功能。

为什么要使用Promise?

一个异步事件不会立刻返回结果,这时我们就需要预先规定一些操作,等待异步事件返回结果后,再去使用某种方式让预先规定的操作执行。在javascript的习惯中,我们常用回调函数(callback)去实现上述过程。下面是一个简单的示例:

例1

let asyncFunc = function(callback){

    let num = 100;

    setTimeout(function(){

        num += 100;

        callback(num);

    },2000);

};

function foo(value){

    console.log(value);  //value => 200

}

asyncFunc (foo);

上面就是一个简单的异步操作处理过程,asyncFunc就是一个异步的函数,执行后通过setTimeout方法在2秒返回了一个值,而foo则是一个回调函数,通过传入异步函数并且在返回结果后被调用的方式获取异步操作的结果。这里的回调函数就如同一个事先的约定,在异步操作返回结果后立即被实现。

那么,既然js中已经有处理异步事件的方法,为何还要引入Promise这个新的方式呢?实际上,上面这段代码只是简单展示下回调函数的基础使用,而在真正的使用场景中,我们不得不面对各种十分复杂的局面。通常在一个异步操作返回结果后执行的回调中还要进行另一个异步操作,而同一个异步操作返回结果后要执行的回调函数可不止一个。数个异步操作与回调函数彼此嵌套,时刻挑战者维护和使用者的神经。下面是一个彼此嵌套的例子:

例2

ajax(url1,function(value1){

    foo(value1);

    bar();

});

function foo(value){

    ajax(url2,function(value2){

        do something..

        ajax(url3,function(value3){

            ...

        })

    });

}

function bar(){ do something.. };

上面的例子模拟了一个js中一个常用的异步操作:发送ajax请求数据。在url1请求的回调中使用了foo和bar两个函数,而foo中又发送了url2,url3的请求。。。这样数层嵌套下来,最终导致代码非常的不直观,维护起来难度也直线上升,形成常说的“回调地狱”。

了解了传统上js处理异步操作的复杂和困难后,我们不禁思索,是否有方法能够更加简洁,直观的去解决异步操作的种种问题?答案就是我们这篇文章的主角:Promise。

Jake
Archibald

使用递归

假设我们的最大并发数是 4 ,这种方法的主要思想是相当于 4 个__单一请求__的 Promise 异步任务在同时运行运行,4 个单一请求不断递归取图片 URL 数组中的 URL 发起请求,直到 URL 全部取完,最后再使用 Promise.all 来处理最后还在请求中的异步任务,我们复用第二节__递归__版本的思路来实现这个功能:

JavaScript

function limitLoad (urls, handler, limit) { const sequence = [].concat(urls) // 对数组做一个拷贝 let count = 0 const promises = [] const load = function () { if (sequence.length <= 0 || count > limit) return count += 1 console.log(`当前并发数: ${count}`) return handler(sequence.shift()) .catch(err => { console.error(err) }) .then(() => { count -= 1 console.log(`当前并发数:${count}`) }) .then(() => load()) } for(let i = 0; i < limit && i < sequence.length; i++){ promises.push(load()) } return Promise.all(promises) }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function limitLoad (urls, handler, limit) {
  const sequence = [].concat(urls) // 对数组做一个拷贝
  let count = 0
  const promises = []
 
  const load = function () {
    if (sequence.length <= 0 || count > limit) return
    count += 1
    console.log(`当前并发数: ${count}`)
    return handler(sequence.shift())
      .catch(err => {
        console.error(err)
      })
      .then(() => {
        count -= 1
        console.log(`当前并发数:${count}`)
      })
      .then(() => load())
  }
 
  for(let i = 0; i < limit && i < sequence.length; i++){
    promises.push(load())
  }
  return Promise.all(promises)
}

设定最大请求数为 5,Chrome 中请求加载的 timeline :9159.com 10

demo6地址(看控制台和网络请求):Promise 控制最大并发数 – 方法1

2. Promise的特性及使用

在PromiseA+规范中做出了这样定义:promise是一个包含了兼容Promise规范then方法的对象或函数,与Promise最主要的交互方法是通过将函数传入它的then方法从而获取得Promise最终的值或Promise最终最拒绝(reject)的原因。

  这段定义有两个重点:1.Promise是一个对象或函数  2.它有一个then方法,能够获取prmose的最终结果。下面我们就来实际看一下Promise到底是如何处理异步事件的,我们将上面的例1使用Promise进行一下改写:

例3

let p = new Promise(function(resolve,reject){

    let value = 100;

    setTimeout(function(){

        value += 100;

        resolve(value);

    },2000);

});

p.then(function(value){

    console.log(value);      //value => 200

},function(err){

    do something...

});

初看之下其实并没有太大区别,但实际上Promise的威力在更复杂的场景下才能更好的发挥。我们先针对这个简单的例子来讲解下Promise的使用

首先通过 new 关键字实例化一个Promise对象,在这个对象中传入一个要执行异步操作的函数。这个函数包含两个形参:resolve和reject。这两个形参是Promise中定义的2个函数,分别在异步事件成功和失败时调用。例3中我们在2秒后调用了resolve函数,代表着异步事件成功,返回一个值。而在我们实例化Promise对象的同时,我们又调用了这个实例的then方法。then方法可以说是Promise方法中的核心,它即代表着Promise约定的这层含义,在then方法中接收2个函数作为参数,分别在异步事件成功时或失败时执行,并且两个函数的参数正是异步事件成功时返回的值或失败时原因。

其实,使用Promise对象来处理异步事件比起使用传统的回调函数的一个优点在于:Promise规范了处理异步事件的流程。我们不必再深入异步事件的内部,去分析种种状态变化后对应的回调究竟如何调用,也不必过多考虑异步事件内部发生错误时该如何捕获,我们只需要在合适的时候通知Promise返回成功或失败状态,剩下的事统统交给Promise去解决。

以上我们大致了解了Promise的处理流程,在详细讲解Promise对象中的方法之前有必要先了解一下Promise的状态概念。

一个Promise对象在实例化后可能拥有以下3种状态的其中之一:

Fulfilled - 当传入的异步事件成功返回值时的状态

Rejected - 当传入的异步事件失败或产生异常时的状态

Pending -  当传入的异步事件还没有结果返回时的状态

注意,任何时候Promise对象都只能处于以上其中状态的一种,当Promise对象处于Pending状态时,它可以转化成Fulfilled 或Rejected 状态,而当Promise对象处于Fulfilled 或Rejected状态时,它不能再转化成其他状态。

可以用一张图来直白的表示上面这段话

9159.com 11

                                                     (图片取自Promise迷你书)

在了解了Promise的三种状态后 ,接下来可以详细了解下Promise对象的几个方法

Human boy working on web standards at Google

使用 Promise.race

Promise.race 接受一个 Promise 数组,返回这个数组中最先被 resolve 的 Promise 的返回值。终于找到 Promise.race 的使用场景了,先来使用这个方法实现的功能代码:

JavaScript

function limitLoad (urls, handler, limit) { const sequence = [].concat(urls) // 对数组做一个拷贝 let count = 0 let promises const wrapHandler = function (url) { const promise = handler(url).then(img => { return { img, index: promise } }) return promise } //并发请求到最大数 promises = sequence.splice(0, limit).map(url => { return wrapHandler(url) }) // limit 大于全部图片数, 并发全部请求 if (sequence.length <= 0) { return Promise.all(promises) } return sequence.reduce((last, url) => { return last.then(() => { return Promise.race(promises) }).catch(err => { console.error(err) }).then((res) => { let pos = promises.findIndex(item => { return item == res.index }) promises.splice(pos, 1) promises.push(wrapHandler(url)) }) }, Promise.resolve()).then(() => { return Promise.all(promises) }) }

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function limitLoad (urls, handler, limit) {
  const sequence = [].concat(urls) // 对数组做一个拷贝
  let count = 0
  let promises
  const wrapHandler = function (url) {
    const promise = handler(url).then(img => {
      return { img, index: promise }
    })
    return promise
  }
  //并发请求到最大数
  promises = sequence.splice(0, limit).map(url => {
    return wrapHandler(url)
  })
  // limit 大于全部图片数, 并发全部请求
  if (sequence.length <= 0) {
    return Promise.all(promises)
  }
  return sequence.reduce((last, url) => {
    return last.then(() => {
      return Promise.race(promises)
    }).catch(err => {
      console.error(err)
    }).then((res) => {
      let pos = promises.findIndex(item => {
        return item == res.index
      })
      promises.splice(pos, 1)
      promises.push(wrapHandler(url))
    })
  }, Promise.resolve()).then(() => {
    return Promise.all(promises)
  })
}

设定最大请求数为 5,Chrome 中请求加载的 timeline :9159.com 12

demo7地址(看控制台和网络请求):Promise 控制最大并发数 – 方法2

在使用 Promise.race 实现这个功能,主要是不断的调用 Promise.race 来返回已经被 resolve 的任务,然后从 promises 中删掉这个 Promise 对象,再加入一个新的 Promise,直到全部的 URL 被取完,最后再使用 Promise.all 来处理所有图片完成后的回调。

resolve()

resolve方法是在一个Promise对象实例化时传入的任务函数的第一个参数,它的作用是让Promise进入“Fulfilled ”状态,resolve方法只接受一个参数,即异步事件的返回值value。

女士们,先生们,请做好准备,迎接网页开发史上的关键时刻。

写在最后

因为工作里面大量使用 ES6 的语法,Koa 中的 await/async 又是 Promise 的语法糖,所以了解 Promise 各种流程控制是对我来说是非常重要的。写的有不明白的地方和有错误的地方欢迎大家留言指正,另外还有其他没有涉及到的方法也请大家提供一下新的方式和方法。

reject()

reject方法与resolve方法正好相反,它是在一个Promise对象实例化时传入的任务函数的第二个参数,它的作用是让Promise进入“Rejected”状态,reject方法同样只接受一个参数,即异步事件失败或异常的原因reason。

[鼓点响起]

题外话

我们目前有 1 个前端的 HC,base 深圳,一家拥有 50 架飞机的物流公司的AI部门,要求工作经验三年以上,这是公司社招要求的。 感兴趣的就联系我吧,Email: d2hlYXRvQGZveG1haWwuY29t

Promise.prototype.then()

then方法是Promise对象方法的重中之重,它是Promise实例的方法,用来注册Promise对象成功时执行的回调函数(onFulfilled)和失败时执行的回调函数(onRejected)。一个then方法的返回值仍然是一个Promsie对象。因此,then方法支持链式调用,也就是一个一个then方法的返回值可以继续调用then。而相链接的then方法中,在上一个then方法的onFulfilled或onRejected回调函数中通过 return value(reason)的方式,把这个结果作为下一个then中的回调函数的参数被接收。onFulfilled和onRejected函数的返回值可以是任何javascript值,甚至一个Promise对象的成功或失败时的回调函数可以返回一个新的Promise对象。这样的特性使得例2中那种复杂的异步事件嵌套的场景处理得以简化。下面是使用Promise来重写的例2:

例4

let p1 = new Promise(function(resolve,reject){

    ajax(url1,function(value1){

        resolve(value1);

    });

});

p1.then(function(value1){

    return new Promise(function(resolve,reject){

        ajax(url2,function(value2){

            do something..

            resolve(value2);

        });

    })

}).then(function(value2){

    return new Promise(function(resolve,reject){

        ajax(url3,function(value3){

            ...

        });

    })

});

p1.then(bar);

function bar(){do something...};

可以看出,使用Promise改写后的代码结构上更加清晰,它把层层嵌套的函数转化成链式的调用then方法的形式,这样可以非常清晰的看出事件间的关系和执行顺序,大大降低了日后代码使用和维护的难度。

关于then方法还有几点补充:

1. then方法中的onFulfilled和onRejected方法都是可以省略的。

2. 当一个Promise失败返回了reason,而then方法中没有定义onRejected函数时,这个reason会被链式调用的下一个then方法的onRejected方法接收。

3. 一个Promise实例可以调用多次then方法,这些then注册的onFulfilled和onRejected函数会按照注册的顺序执行。

Promise 已获得 JavaScript 的原生支持!

参考资料

  • JavaScript Promise:简介 | Web | Google Developers
  • JavaScript Promise迷你书(中文版)

    1 赞 3 收藏 评论

9159.com 13

Promise.prototype.catch()

catch方法是一个then方法的语法糖,它只接受一个失败处理函数onRejected,实际上等同于以下代码:

new Promsie.then(null,function(){

    do something...

})

Promise.all()

all方法是Promsie对象的静态方法,使用方式是 Promise.all()。all方法接收的参数为一个包含数个Promise对象实例的数组,并返回一个新的Promise实例。当数组中所有的Promse实例都返回结果后,将所有数组中的Promise实例的成功返回值传入一个数组,并将这个数组注入到all方法返回的新实例的then方法中。下面是一个all方法的使用实例:

例5

let promiseArr = [

    new Promise(function(resolve,reject){

        setTimeout(function(){

            resolve(100)

        },1000)

    }),

    new Promise(function(resolve,reject){

        setTimeout(function(){

            resolve(200)

        },500)

    })

]

Promise.all(promiseArr).then(function(valArr){

    console.log(valArr)    // valArr  => [100,200]

},function(err){

    do something...

})

all方法值得注意的有两点:

1.数组中所有promise实例都成功后的返回值,在valArr中的顺序是按照promiseArr 中promise实例的顺序来排列的。

2.当任何一个promise失败后,all方法直接将返回的Promise对象的状态变为Rejected,并调用then方法的onRejected函数,把失败的原因传递出来。

Promise.resolve()

Promsie对象本身存在一个resolve方法,它的作用是立刻返回一个状态为Fulfilled的Promise对象实例。如果你在这个resolve方法中传入的是一个Promise实例的话,那么resolve方法会保持这个Promise实例的状态,并根据它最后返回的状态来调用resolve方法返回的Promise实例then方法的onResolve或onRejected函数。

其实这个方法最常用的场景是讲一个普通的值转换成一个Promise实例。一般来说不是很常用。

[烟花绽放、彩纸飘飘、人群沸腾]

Promise.reject()

与Promise.resolve()相反,它的作用是立刻返回一个状态为Rejected的Promise对象实例。实际上这个方法是一个语法糖,它等同于以下代码:

new Promise(function(resolve,reject){

    reject(reason);

})

以上就是一个ES6中的Promise对象中所包含的常用方法。

此刻,您可能属于以下其中某一类:

3. 一个符合PromiseA+规范的Promise对象的完整实现

  想必看到这里的一些读者会不禁思考,Promise对象究竟是如何实现的呢?我个人参考了一些资料实现了一个符合PromiseA+规范的Promise对象,把源代码贴在下面,有兴趣的朋友可以参考一下,实际上代码本身并不是很多,各位看完以后可以尝试用自己的方式再实现一遍。同时附上一个测试工具,里面包含了几百个测试用例,用来测试我们自己写的Promise是否完美的符合PromiseA+规范。

Compliances tests for Promises/A+

使用的方法很简单

npm i -g promises-aplus-tests

promises-aplus-tests Promise.js

安装后运行你的js文件就可以测试你的代码是否符合规范了。

下面就是我实现的Promise对象的代码

function MyPromise(task) {

    const _this = this;

    _this.status = 'pending';  //设定初始状态

    _this.value = undefined;

    _this.onFulfilledsList = [];  //onFulfilled函数序列

    _this.onRejectedsList = [];  //onRejected函数序列

    function resolve(value) {

        if (value instanceof MyPromise) {

            return value.then(resolve, reject);

        }

        //异步执行resolve或reject方法,保证代码的统一性和注册的回调函数按照正确的顺序执行

            if (_this.status === 'pending') {

                _this.status = 'fulfilled';

                _this.value = value;

                _this.onFulfilledsList.forEach(cb => cb(value))

            }

    }

    function reject(reason) {

            if (_this.status === 'pending') {

                _this.status = 'rejected';

                _this.reason = reason;

                _this.onRejectedsList.forEach(cb => cb(reason))

            }

    }

    try {

        task(resolve, reject);

    } catch (err) {

        throw new Error(err);

    }

}

function resolvePromise(promise2, x, resolve, reject) {

    if (x === promise2) {

        return reject(new TypeError('循环引用'));

    }

    //如果返回的是一个thenable对象,即一个拥有then方法的对象,那么使用它的then方法去获得它的最终返回值。目的是为了兼容其他Promise库

    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {

        let then, called;

        try {

            then = x.then;

            if (typeof then === 'function') {

                then.call(x, function (newx) {

                    if (called) return;  //防止重复调用

                    called = true;

                    resolvePromise(promise2, newx, resolve, reject);

                }, function (err) {

                    if (called) return;

                    called = true;

                    return reject(err);

                });

            } else {

                resolve(x);

            }

        } catch (err) {

            if (called) return;

            called = true;

            reject(err);

        }

    } else {

        resolve(x);

    }

}

MyPromise.prototype.then = function (onFulfilled, onRejected) {

    const _this = this;

    let promise2;

    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (data) {

        return data;

    };

    onRejected = typeof onRejected === 'function' ? onRejected : function (data) {

        throw data;

    };

    //为了支持同步代码,当then方法注册的时候如果Promise的状态已经改变,那么立即执行对应的函数

    if (_this.status === 'fulfilled') {

        promise2 = new MyPromise(function (resolve, reject) {

          setTimeout(function () {

            let x;

            try {

                x = onFulfilled(_this.value);

                resolvePromise(promise2, x, resolve, reject);

            } catch (err) {

                reject(err);

            }

          })

        })

    }

    if (_this.status === 'rejected') {

        promise2 = new MyPromise(function (resolve, reject) {

         setTimeout(function () {

            let x;

            try {

                x = onRejected(_this.reason);

                resolvePromise(promise2, x, resolve, reject);

            } catch (err) {

                reject(err);

            }

         )}

        })

    }

    if (_this.status === 'pending') {

        promise2 = new MyPromise(function (resolve, reject) {

            _this.onFulfilledsList.push(function (value) {

                setTimeout(function () {

                let x;

                try {

                    x = onFulfilled(value);

                    resolvePromise(promise2, x, resolve, reject);

                } catch (err) {

                    reject(err);

                }

                })

            });

            _this.onRejectedsList.push(function (reason) {

               setTimeout(function () {

                try {

                    let x = onRejected(reason);

                    resolvePromise(promise2, x, resolve, reject);

                } catch (err) {

                    reject(err);

                }

            })

        });

        })

    }

    return promise2;  //返回一个新的Promise实例,以便支持链式调用

};

MyPromise.prototype.catch = function (onRejected) {

    this.then(null, onRejected);

};

MyPromise.all = function (someValue) {

    let resolveValArr = [];

    let count = promiseLen = 0;

    let promise2;

    promise2 = new MyPromise(function (resolve, reject) {

        let iNow = 0;

        try {

            for (let item of someValue) {

                if (item !== null && typeof item === "object") {

                    try {

                        let then = item.then;

                        let index = iNow;

                        if (typeof then === 'function') {

                            promiseLen++;

                            then.call(item, function (value) {

                                resolveValArr[index] = value;

                                if (++count === promiseLen) {

                                    resolve(resolveValArr)

                                }

                            }, function (err) {

                                reject(err);

                            });

                        }

                    } catch (err) {

                        resolveValArr[iNow] = item;

                    }

                } else {

                    resolveValArr[iNow] = item;

                }

                iNow++;

            }

            if (iNow === 0) {

                return resolve(someValue);

            }

            if (promiseLen === 0) {

                return resolve(resolveValArr);

            }

        } catch (err) {

            reject(new TypeError('无法遍历的类型!'));

        }

    });

    return promise2;

};

MyPromise.race = function (someValue) {

    let promise2;

    promise2 = new MyPromise(function (resolve, reject) {

        let iNow = 0;

        try {

            for (let item of someValue) {

                if (item !== null && typeof item === "object") {

                    try {

                        let then = item.then;

                        then.call(item, function (value) {

                            resolve(value);

                        }, function (err) {

                            reject(err);

                        });

                    } catch (err) {

                        resolve(item);

                        break;

                    }

                } else {

                    resolve(item);

                    break;

                }

                iNow++;

            }

            if (iNow === 0) {

                return resolve(someValue);

            }

        } catch (err) {

            reject(new TypeError('无法遍历的类型!'));

        }

    });

    return promise2;

};

MyPromise.resolve = function (value) {

    let promise2;

    if (value !== null && (typeof value === 'object' || typeof value === 'function')) {

        promise2 = new MyPromise(function (resolve, reject) {

            try {

                let then = value.then;

                if (typeof value.then === 'function') {

                    then.call(value, function (data) {

                        resolve(data);

                    }, reject);

                } else {

                    resolve(value);

                }

            } catch (err) {

                reject(err);

            }

        })

    } else {

        promise2 = new MyPromise(function (resolve) {

            resolve(value);

        })

    }

    return promise2;

};

MyPromise.reject = function (reason) {

    return new MyPromise(function (resolve, reject) {

        reject(reason);

    })

};

module.exports = MyPromise;

//这是为了让代码能够测试而开放的接口,详见promises-aplus-tests中的相关描述

MyPromise.deferred = MyPromise.defer = function () {

    let deferred = {};

    deferred.promise = new MyPromise(function (resolve, reject) {

        deferred.resolve = resolve;

        deferred.reject = reject;

    });

    return deferred

};

-

人群在您身边欢呼雀跃,但是您感到莫名其妙。可能您甚至连“promise”是什么都不知道。因此您耸耸肩,但是从天而降的彩纸虽轻如鸿毛却让您无法释怀。如果真是这样,您也无需担心,我可花了很长的时间才弄明白为什么我应该关注它。您可能想从开始处开始。

尾声

本文参考了很多资料,如果你看到其他文章有类似的观点非常正常,不过笔者尽量使用了自己的理解去阐述Promise的相关知识。如果你发现本文中有哪些疏漏,欢迎发私信给我进行斧正。同时也可以在下面留言给我,我会一一查看,尽量回复。

您非常抓狂!觉得晚了一步,对吗?您可能之前使用过这些 Promise,但让您困扰的是,不同版本的 API 各有差异。JavaScript 官方版本的 API 是什么?您可能想要从术语开始。

您已知道这些,您会觉得那些上窜下跳的人很好笑,居然把它当作新闻。您可以先自豪一把,然后直接查看 API 参考

人们究竟为何欢呼雀跃?

JavaScript 是单线程工作,这意味着两段脚本不能同时运行,而是必须一个接一个地运行。在浏览器中,JavaScript
与因浏览器而异的其他 N 种任务共享一个线程。但是通常情况下 JavaScript
与绘制、更新样式和处理用户操作(例如,高亮显示文本以及与格式控件交互)处于同一队列。操作其中一项任务会延迟其他任务。

我们人类是多线程工作。您可以使用多个手指打字,可以一边开车一边与人交谈。唯一一个会妨碍我们的是打喷嚏,因为当我们打喷嚏的时候,所有当前进行的活动都必须暂停。这真是非常讨厌,尤其是当您在开车并想与人交谈时。您可不想编写像打喷嚏似的代码。

您可能已使用事件和回调来解决该问题。以下是一些事件:

var img1 = document.querySelector('.img-1');img1.addEventListener('load', function() { // woo yey image loaded});img1.addEventListener('error', function() { // argh everything's broken});

这可不会像打喷嚏那样打断您。我们获得图片、添加几个侦听器,之后 JavaScript 可停止执行,直至其中一个侦听器被调用。

遗憾的是,在上例中,事件有可能在我们开始侦听之前就发生了,因此我们需要使用图像的“complete”属性来解决该问题:

var img1 = document.querySelector('.img-1');function loaded() { // woo yey image loaded}if (img1.complete) { loaded();}else { img1.addEventListener('load', loaded);}img1.addEventListener('error', function() { // argh everything's broken});

这不会捕获出错的图像,因为在此之前我们没有机会侦听到错误。遗憾的是,DOM 也没有给出解决之道。而且,这还只是加载一个图像,如果加载一组图像,情况会更复杂。

事件并不总是最佳方法

事件对于同一对象上发生多次的事情(如 keyup、touchstart 等)非常有用。对于这些事件,实际您并不关注在添加侦听器之前所发生的事情。但是,如果关系到异步成功/失败,理想的情况是您希望:

img1.callThisIfLoadedOrWhenLoaded(function() { // loaded}).orIfFailedCallThis(function() { // failed});// and…whenAllTheseHaveLoaded([img1, img2]).callThis(function() { // all loaded}).orIfSomeFailedCallThis(function() { // one or more failed});

这是 promise 所执行的任务,但以更好的方式命名。如果 HTML 图像元素有一个返回 promise 的“ready”方法,我们可以执行:

img1.ready().then(function() { // loaded}, function() { // failed});// and…Promise.all([img1.ready(), img2.ready()]).then(function() { // all loaded}, function() { // one or more failed});

最基本的情况是,promise 有点类似于事件侦听器,但有以下两点区别:

promise 只能成功或失败一次,而不能成功或失败两次,也不能从成功转为失败或从失败转为成功。

如果 promise 已成功或失败,且您之后添加了成功/失败回调,则将会调用正确的回调,即使事件发生在先。

这对于异步成功/失败尤为有用,因为您可能对某些功能可用的准确时间不是那么关注,更多地是关注对结果作出的反应。

Promise 术语

Domenic Denicola 校对了本篇文章的初稿,并在术语方面给我打分为“F”。他把我留下来,强迫我抄写状态和结果 100 遍,并给我的父母写了封告状信。尽管如此,我还是对很多术语混淆不清,以下是几个基本的概念:

promise 可以是:

已执行 - 与 promise 有关的操作成功

已拒绝 - 与 promise 有关的操作失败

待定 - 尚未执行或拒绝

已解决 - 已执行或拒绝

本规范还使用术语 thenable 来描述类似于 promise 的对象,并使用then
方法。该术语让我想起前英格兰国家队教练 Terry Venables,因此我将尽可能不用这个术语。

Promise 在 JavaScript 中受支持!

Promise 有一段时间以库的形式出现,例如:

Q

when

WinJS

RSVP.js

以上这些与 JavaScript promise 都有一个名为 Promise/A+ 的常见标准化行为。如果您是 jQuery 用户,他们还有一个类似于名为 Deferred 的行为。但是,Deferred 与 Promise/A+ 不兼容,这就使得它们存在细微差异且没那么有用,因此需注意。此外,jQuery 还有 Promise 类型,但它只是 Deferred 的子集,因此仍存在相同的问题。

尽管 promise 实现遵照标准化行为,但其整体 API 有所不同。JavaScript promise 在 API 中类似于 RSVP.js。下面是创建 promise 的步骤:

var promise = new Promise(function(resolve, reject) { // do a thing, possibly async, then… if (/* everything turned out fine */) { resolve("Stuff worked!"); } else { reject(Error("It broke")); }});

Promise 构造函数包含一个参数和一个带有 resolve(解析)和 reject(拒绝)两个参数的回调。在回调中执行一些操作(例如异步),如果一切都正常,则调用 resolve,否则调用 reject。

与普通旧版 JavaScript 中的throw
一样,通常拒绝时会给出 Error 对象,但这不是必须的。Error 对象的优点在于它们能够捕捉堆叠追踪,因而使得调试工具非常有用。

以下是有关 promise 的使用示例:

promise.then(function(result) { console.log(result); // "Stuff worked!"}, function(err) { console.log(err); // Error: "It broke"});

then()
包含两个参数:一个用于成功情形的回调和一个用于失败情形的回调。这两个都是可选的,因此您可以只添加一个用于成功情形或失败情形的回调。

JavaScript promise 最初是在 DOM 中出现并称为“Futures”,之后重命名为“Promises”,最后又移入
JavaScript。在 JavaScript 中使用比在 DOM 中更好,因为它们将在如 Node.js 等非浏览器 JS
环境中可用(而它们是否会在核心 API 中使用 Promise 则是另外一个问题)。

尽管它们是 JavaScript 的一项功能,但 DOM 也能使用。实际上,采用异步成功/失败方法的所有新 DOM API 均使用 promise。Quota Management、Font Load Events、ServiceWorker、Web MIDIStreams 等等都已经在使用 promise。

浏览器支持和 polyfill

现在,promise 已在各浏览器中实现。

在 Chrome 32、Opera 19、Firefox 29、Safari 8 和 Microsoft Edge 中,promise 默认启用。

如要使没有完全实现 promise 的浏览器符合规范,或向其他浏览器和 Node.js 中添加 promise,请查看 polyfill(gzip 压缩大小为 2k)。

与其他库的兼容性

JavaScript promise API 将任何使用then()
方法的结构都当作 promise 一样(或按 promise 的说法为thenable
)来处理,因此,如果您使用返回 Q promise 的库也没问题,因为它能与新 JavaScript promise 很好地兼容。

如我之前所提到的,jQuery 的 Deferred 不那么有用。幸运的是,您可以将其转为标准 promise,这值得尽快去做:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

这里,jQuery 的$.ajax
返回了一个 Deferred。由于它使用then()
方法,因此Promise.resolve()
可将其转为 JavaScript promise。但是,有时 deferred 会将多个参数传递给其回调,例如:

var jqDeferred = $.ajax('/whatever.json');jqDeferred.then(function(response, statusText, xhrObj) { // ...}, function(xhrObj, textStatus, err) { // ...})

而 JS promise 会忽略除第一个之外的所有参数:

jsPromise.then(function(response) { // ...}, function(xhrObj) { // ...})

幸好,通常这就是您想要的,或者至少为您提供了方法让您获得所想要的。另请注意,jQuery 不遵循将 Error 对象传递到 reject 这一惯例。

复杂异步代码让一切变得更简单

对了,让我们写一些代码。比如说,我们想要:

启动一个转环来提示加载

获取一个故事的 JSON,确定每个章节的标题和网址

向页面中添加标题

获取每个章节

向页面中添加故事

停止转环

…但如果此过程发生错误,也要向用户显示。我们也想在那一点停止转环,否则,它将不停地旋转、眩晕并撞上其他 UI 控件。

当然,您不会使用 JavaScript 来提供故事,以 HTML 形式提供会更快,但是这种方式在处理 API 时很常见:多次提取数据,然后在全部完成后执行其他操作。

首先,让我们从网络中获取数据:

对 XMLHttpRequest 执行 promise

旧 API 将更新为使用 promise,如有可能,采用后向兼容的方式。XMLHttpRequest
是主要候选对象,不过,我们可编写一个作出 GET 请求的简单函数:

function get(url) { // Return a new promise. return new Promise(function(resolve, reject) { // Do the usual XHR stuff var req = new XMLHttpRequest(); req.open('GET', url); req.onload = function() { // This is called even on 404 etc // so check the status if (req.status == 200) { // Resolve the promise with the response text resolve(req.response); } else { // Otherwise reject with the status text // which will hopefully be a meaningful error reject(Error(req.statusText)); } }; // Handle network errors req.onerror = function() { reject(Error("Network Error")); }; // Make the request req.send(); });}

现在让我们来使用这一功能:

get('story.json').then(function(response) { console.log("Success!", response);}, function(error) { console.error("Failed!", error);})

点击此处了解实际操作,检查 DevTools 中的控制台以查看结果。现在我们无需手动键入XMLHttpRequest
即可作出 HTTP 请求,这真是太赞了,因为越少看到令人讨厌的书写得参差不齐的XMLHttpRequest
,我就越开心。

链接

then()
不是最终部分,您可以将各个then
链接在一起来改变值,或依次运行额外的异步操作。

改变值

只需返回新值即可改变值:

var promise = new Promise(function(resolve, reject) { resolve(1);});promise.then(function(val) { console.log(val); // 1 return val + 2;}).then(function(val) { console.log(val); // 3})

举一个实际的例子,让我们回到:

get('story.json').then(function(response) { console.log("Success!", response);})

这里的 response 是 JSON,但是我们当前收到的是其纯文本。我们可以将 get 函数修改为使用 JSON responseType
,不过我们也可以使用 promise 来解决这个问题:

get('story.json').then(function(response) { return JSON.parse(response);}).then(function(response) { console.log("Yey JSON!", response);})

由于JSON.parse()
采用单一参数并返回改变的值,因此我们可以将其简化为:

get('story.json').then(JSON.parse).then(function(response) { console.log("Yey JSON!", response);})

了解实际操作,检查 DevTools 中的控制台以查看结果。实际上,我们可以让getJSON()
函数更简单:

function getJSON(url) { return get(url).then(JSON.parse);}

getJSON()
仍返回一个 promise,该 promise 获取 URL 后将 response 解析为 JSON。

异步操作队列

您还可以链接多个then
,以便按顺序运行异步操作。

当您从then()
回调中返回某些内容时,这有点儿神奇。如果返回一个值,则会以该值调用下一个then()
。但是,如果您返回类似于 promise 的内容,下一个then()
则会等待,并仅在 promise 产生结果(成功/失败)时调用。例如:

getJSON('story.json').then(function(story) { return getJSON(story.chapterUrls[0]);}).then(function(chapter1) { console.log("Got chapter 1!", chapter1);})

这里我们向story.json
发出异步请求,这可让我们请求一组网址,随后我们请求其中的第一个。这是 promise 从简单回调模式中脱颖而出的真正原因所在。

您甚至可以采用更简短的方法来获得章节内容:

var storyPromise;function getChapter(i) { storyPromise = storyPromise || getJSON('story.json'); return storyPromise.then(function(story) { return getJSON(story.chapterUrls[i]); })}// and using it is simple:getChapter(0).then(function(chapter) { console.log(chapter); return getChapter(1);}).then(function(chapter) { console.log(chapter);})

直到getChapter
被调用,我们才下载story.json
,但是下次getChapter
被调用时,我们重复使用 story romise,因此story.json
仅获取一次。耶,Promise!

错误处理

正如我们之前所看到的,then()
包含两个参数:一个用于成功,一个用于失败(按照 promise 中的说法,即执行和拒绝):

get('story.json').then(function(response) { console.log("Success!", response);}, function(error) { console.log("Failed!", error);})

您还可以使用catch()

get('story.json').then(function(response) { console.log("Success!", response);}).catch(function(error) { console.log("Failed!", error);})

catch()
没有任何特殊之处,它只是then(undefined, func)
的锦上添花,但可读性更强。注意,以上两个代码示例行为并不相同,后者相当于:

get('story.json').then(function(response) { console.log("Success!", response);}).then(undefined, function(error) { console.log("Failed!", error);})

两者之间的差异虽然很微小,但非常有用。Promise 拒绝后,将跳至带有拒绝回调的下一个then()
(或具有相同功能的catch()
)。如果是then(func1, func2)
,则func1
或func2
中的一个将被调用,而不会二者均被调用。但如果是then(func1).catch(func2)
,则在func1
拒绝时两者均被调用,因为它们在该链中是单独的步骤。看看下面的代码:

asyncThing1().then(function() { return asyncThing2();}).then(function() { return asyncThing3();}).catch(function(err) { return asyncRecovery1();}).then(function() { return asyncThing4();}, function(err) { return asyncRecovery2();}).catch(function(err) { console.log("Don't worry about it");}).then(function() { console.log("All done!");})

以上流程与常规的 JavaScript try/catch 非常类似,在“try”中发生的错误直接进入catch()
块。以下是上述代码的流程图形式(因为我喜欢流程图):

蓝线表示执行的 promise 路径,红路表示拒绝的 promise 路径。

JavaScript 异常和 promise

当 promise 被明确拒绝时,会发生拒绝;但是如果是在构造函数回调中引发的错误,则会隐式拒绝。

var jsonPromise = new Promise(function(resolve, reject) { // JSON.parse throws an error if you feed it some // invalid JSON, so this implicitly rejects: resolve(JSON.parse("This ain't JSON"));});jsonPromise.then(function(data) { // This never happens: console.log("It worked!", data);}).catch(function(err) { // Instead, this happens: console.log("It failed!", err);})

这意味着,在 promise 构造函数回调内部执行所有与 promise 相关的任务很有用,因为错误会自动捕获并进而拒绝。

对于在then()
回调中引发的错误也是如此。

get('/').then(JSON.parse).then(function() { // This never happens, '/' is an HTML page, not JSON // so JSON.parse throws console.log("It worked!", data);}).catch(function(err) { // Instead, this happens: console.log("It failed!", err);})

错误处理实践

在我们的故事和章节中,我们可使用 catch 来向用户显示错误:

getJSON('story.json').then(function(story) { return getJSON(story.chapterUrls[0]);}).then(function(chapter1) { addHtmlToPage(chapter1.html);}).catch(function() { addTextToPage("Failed to show chapter");}).then(function() { document.querySelector('.spinner').style.display = 'none';})

如果获取story.chapterUrls[0]
失败(例如,http 500 或用户离线),它将跳过所有后续成功回调,包括getJSON()
中尝试将响应解析为 JSON 的回调,而且跳过将 chapter1.html 添加到页面的回调。然后,它将移至 catch 回调。因此,如果任一前述操作失败,“Failed to show chapter”将会添加到页面。

与 JavaScript 的 try/catch 一样,错误被捕获而后续代码继续执行,因此,转环总是被隐藏,这正是我们想要的。以上是下面一组代码的拦截异步版本:

try { var story = getJSONSync('story.json'); var chapter1 = getJSONSync(story.chapterUrls[0]); addHtmlToPage(chapter1.html);}catch (e) { addTextToPage("Failed to show chapter");}document.querySelector('.spinner').style.display = 'none'

您可能想出于记录目的而catch()
,而无需从错误中恢复。为此,只需再次抛出错误。我们可以使用getJSON()
方法执行此操作:

function getJSON(url) { return get(url).then(JSON.parse).catch(function(err) { console.log("getJSON failed for", url, err); throw err; });}

至此,我们已获取其中一个章节,但我们想要所有的章节。让我们尝试来实现。

并行式和顺序式:两者兼得

异步并不容易。如果您觉得难以着手,可尝试按照同步的方式编写代码。在本例中:

try { var story = getJSONSync('story.json'); addHtmlToPage(story.heading); story.chapterUrls.forEach(function(chapterUrl) { var chapter = getJSONSync(chapterUrl); addHtmlToPage(chapter.html); }); addTextToPage("All done");}catch (err) { addTextToPage("Argh, broken: "

  • err.message);}document.querySelector('.spinner').style.display = 'none'

试一下

这样可行(查看代码)!
但这是同步的情况,而且在内容下载时浏览器会被锁定。要使其异步,我们使用then()
来依次执行任务。

getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); // TODO: for each url in story.chapterUrls, fetch & display}).then(function() { // And we're all done! addTextToPage("All done");}).catch(function(err) { // Catch any error that happened along the way addTextToPage("Argh, broken: " + err.message);}).then(function() { // Always hide the spinner document.querySelector('.spinner').style.display = 'none';})

但是我们如何遍历章节的 URL 并按顺序获取呢?以下方法行不通

story.chapterUrls.forEach(function(chapterUrl) { // Fetch chapter getJSON(chapterUrl).then(function(chapter) { // and add it to the page addHtmlToPage(chapter.html); });})

forEach
不是异步的,因此我们的章节内容将按照下载的顺序显示,这就乱套了。我们这里不是非线性叙事小说,因此得解决该问题。

创建序列

我们想要将chapterUrls
数组转变为 promise 序列,这可通过then()
来实现:

// Start off with a promise that always resolvesvar sequence = Promise.resolve();// Loop through our chapter urlsstory.chapterUrls.forEach(function(chapterUrl) { // Add these actions to the end of the sequence sequence = sequence.then(function() { return getJSON(chapterUrl); }).then(function(chapter) { addHtmlToPage(chapter.html); });})

这是我们第一次看到Promise.resolve()
,这种 promise 可解析为您赋予的任何值。如果向其传递一个Promise
实例,它也会将其返回(注意:这是对本规范的一处更改,某些实现尚未遵循)。如果将类似于 promise 的内容(带有then()
方法)传递给它,它将创建以相同方式执行/拒绝的真正Promise
。如果向其传递任何其他值,例如Promise.resolve('Hello')
,它在执行时将以该值创建一个 promise。如果调用时不带任何值(如上所示),它在执行时将返回“undefined”。

此外还有Promise.reject(val)
,它创建的 promise 在拒绝时将返回赋予的值(或“undefined”)。

我们可以使用 array.reduce
将上述代码整理如下:

// Loop through our chapter urlsstory.chapterUrls.reduce(function(sequence, chapterUrl) { // Add these actions to the end of the sequence return sequence.then(function() { return getJSON(chapterUrl); }).then(function(chapter) { addHtmlToPage(chapter.html); });}, Promise.resolve())

这与之前示例的做法相同,但是不需要独立的“sequence”变量。我们的 reduce 回调针对数组中的每项内容进行调用。首次调用时,“sequence”为Promise.resolve()
,但是对于余下的调用,“sequence”为我们从之前调用中返回的值。array.reduce
确实非常有用,它将数组浓缩为一个简单的值(在本例中,该值为 promise)。

让我们汇总起来:

getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); return story.chapterUrls.reduce(function(sequence, chapterUrl) { // Once the last chapter's promise is done… return sequence.then(function() { // …fetch the next chapter return getJSON(chapterUrl); }).then(function(chapter) { // and add it to the page addHtmlToPage(chapter.html); }); }, Promise.resolve());}).then(function() { // And we're all done! addTextToPage("All done");}).catch(function(err) { // Catch any error that happened along the way addTextToPage("Argh, broken: " + err.message);}).then(function() { // Always hide the spinner document.querySelector('.spinner').style.display = 'none';})

试一下

这里我们已实现它(查看代码),即同步版本的完全异步版本。但是我们可以做得更好。此时,我们的页面正在下载,如下所示:

9159.com 14

浏览器的一个优势在于可以一次下载多个内容,因此我们一章章地下载就失去了其优势。我们希望同时下载所有章节,然后在所有下载完毕后进行处理。幸运的是,API 可帮助我们实现:

Promise.all(arrayOfPromises).then(function(arrayOfResults) { //...})

Promise.all
包含一组 promise,并创建一个在所有内容成功完成后执行的 promise。您将获得一组结果(即一组 promise 执行的结果),其顺序与您与传入 promise 的顺序相同。

getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); // Take an array of promises and wait on them all return Promise.all( // Map our array of chapter urls to // an array of chapter json promises story.chapterUrls.map(getJSON) );}).then(function(chapters) { // Now we have the chapters jsons in order! Loop through… chapters.forEach(function(chapter) { // …and add to the page addHtmlToPage(chapter.html); }); addTextToPage("All done");}).catch(function(err) { // catch any error that happened so far addTextToPage("Argh, broken: " + err.message);}).then(function() { document.querySelector('.spinner').style.display = 'none';})

试一下

根据连接情况,这可能比一个个依次加载要快几秒钟(查看代码),而且代码也比我们第一次尝试的要少。章节将按任意顺序下载,但在屏幕中以正确顺序显示。

9159.com 15

不过,我们仍可以提升用户体验。第一章下载完后,我们可将其添加到页面。这可让用户在其他章节下载完毕前先开始阅读。第三章下载完后,我们不将其添加到页面,因为还缺少第二章。第二章下载完后,我们可添加第二章和第三章,后面章节也是如此添加。

为此,我们使用 JSON 来同时获取所有章节,然后创建一个向文档中添加章节的顺序:

getJSON('story.json').then(function(story) { addHtmlToPage(story.heading); // Map our array of chapter urls to // an array of chapter json promises. // This makes sure they all download parallel. return story.chapterUrls.map(getJSON) .reduce(function(sequence, chapterPromise) { // Use reduce to chain the promises together, // adding content to the page for each chapter return sequence.then(function() { // Wait for everything in the sequence so far, // then wait for this chapter to arrive. return chapterPromise; }).then(function(chapter) { addHtmlToPage(chapter.html); }); }, Promise.resolve());}).then(function() { addTextToPage("All done");}).catch(function(err) { // catch any error that happened along the way addTextToPage("Argh, broken: " + err.message);}).then(function() { document.querySelector('.spinner').style.display = 'none';})

试一下

我们做到了(查看代码),两全其美!下载所有内容所花费的时间相同,但是用户可先阅读前面的内容。

9159.com 16

在这个小示例中,所有章节几乎同时下载完毕,但是如果一本书有更多、更长的章节,一次显示一个章节的优势便会更明显。

使用 Node.js-style 回调或事件来执行以上示例需两倍代码,更重要的是,没那么容易实施。然而,promise 功能还不止如此,与其他 ES6 功能组合使用时,它们甚至更容易。

友情赠送:promise 和 generator

以下内容涉及一整套 ES6 新增功能,但您目前在使用 promise 编码时无需掌握它们。可将其视为即将上映的好莱坞大片电影预告。

ES6 还为我们提供了 generator,它可让某些功能在某个位置退出(类似于“return”),但之后能以相同位置和状态恢复,例如:

function *addGenerator() { var i = 0; while (true) { i += yield i; }}

注意函数名称前面的星号,这表示 generator。yield 关键字是我们的返回/恢复位置。我们可按下述方式使用:

var adder = addGenerator();adder.next().value; // 0adder.next(5).value; // 5adder.next(5).value; // 10adder.next(5).value; // 15adder.next(50).value; // 65

但是这对于 promise 而言意味着什么呢?您可以使用返回/恢复行为来编写异步代码,这些代码看起来像同步代码,而且实施起来也与同步代码一样简单。对各行代码的理解无需过多担心,借助于帮助程序函数,我们可使用yield
来等待 promise 得到解决:

function spawn(generatorFunc) { function continuer(verb, arg) { var result; try { result = generatorverb; } catch (err) { return Promise.reject(err); } if (result.done) { return result.value; } else { return Promise.resolve(result.value).then(onFulfilled, onRejected); } } var generator = generatorFunc(); var onFulfilled = continuer.bind(continuer, "next"); var onRejected = continuer.bind(continuer, "throw"); return onFulfilled();}

…在上述示例中我几乎是从 Q 中逐字般过来,并针对 JavaScript promise 进行了改写。因此,我们可以采用显示章节的最后一个最佳示例,结合新 ES6 的优势,将其转变为:

spawn(function *() { try { // 'yield' effectively does an async wait, // returning the result of the promise let story = yield getJSON('story.json'); addHtmlToPage(story.heading); // Map our array of chapter urls to // an array of chapter json promises. // This makes sure they all download parallel. let chapterPromises = story.chapterUrls.map(getJSON); for (let chapterPromise of chapterPromises) { // Wait for each chapter to be ready, then add it to the page let chapter = yield chapterPromise; addHtmlToPage(chapter.html); } addTextToPage("All done"); } catch (err) { // try/catch just works, rejected promises are thrown here addTextToPage("Argh, broken: " + err.message); } document.querySelector('.spinner').style.display = 'none';})

试一下

这跟之前的效用完全相同,但读起来容易多了。Chrome 和 Opera 当前支持该功能(查看代码),而且 Microsoft Edge 中也可使用该功能(需要在about:flags
中打开 Enable experimental JavaScript features 设置)。在即将发布的版本中,该功能默认启用。

它将纳入很多新的 ES6 元素:promise、generator、let、for-of。我们生成一个 promise 后,spawn
帮助程序将等待该 promise 来解析并返回一个终值。如果 promise 拒绝,spawn 会让 yield 语句抛出异常,我们可通过常规的
JavaScript try/catch 来捕获此异常。异步编码竟如此简单!

此模式非常有用,在 ES7 中它将以异步功能的形式提供。它几乎与上述编码示例相同,但无需使用spawn
方法。

Promise API 参考

所有方法在 Chrome、Opera、Firefox、Microsoft Edge 和 Safari 中均可使用,除非另有说明。polyfill 为所有浏览器提供以下方法。

静态方法

方法汇总

Promise.resolve(promise);

返回 promise(仅当promise.constructor == Promise
时)

Promise.resolve(thenable);

从 thenable 中生成一个新 promise。thenable 是具有 then() 方法的类似于 promise 的对象。

Promise.resolve(obj);

在此情况下,生成一个 promise 并在执行时返回obj

Promise.reject(obj);

生成一个 promise 并在拒绝时返回obj
。为保持一致和调试之目的(例如堆叠追踪),obj
应为instanceof Error

Promise.all(array);

生成一个 promise,该 promise 在数组中各项执行时执行,在任意一项拒绝时拒绝。每个数组项均传递给Promise.resolve
,因此数组可能混合了类似于 promise 的对象和其他对象。执行值是一组有序的执行值。拒绝值是第一个拒绝值。

Promise.race(array);

生成一个 Promise,该 Promise 在任意项执行时执行,或在任意项拒绝时拒绝,以最先发生的为准。

注:我对Promise.race
的实用性表示怀疑;我更倾向于使用与之相对的Promise.all
,它仅在所有项拒绝时才拒绝。

构造函数

构造函数

new Promise(function(resolve,reject) {});

resolve(thenable)

Promise 依据thenable
的结果而执行/拒绝。

resolve(obj)

Promise 执行并返回obj

reject(obj)

Promise 拒绝并返回obj
。为保持一致和调试(例如堆叠追踪),obj 应为instanceof Error

在构造函数回调中引发的任何错误将隐式传递给reject()

实例方法

的锦上添花

实例方法

promise.then(onFulfilled,onRejected)

当/如果“promise”解析,则调用onFulfilled
。当/如果“promise”拒绝,则调用onRejected

两者均可选,如果任意一个或两者都被忽略,则调用链中的下一个onFulfilled
/onRejected

两个回调都只有一个参数:执行值或拒绝原因。then()
将返回一个新 promise,它相当于从onFulfilled
/onRejected
中返回、
通过Promise.resolve
传递的值。如果在回调中引发了错误,返回的 promise 将拒绝并返回该错误。

promise.catch(onRejected)

对promise.then(undefined,onRejected)

Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、Addy Osmani、Arthur Evans 和 Yutaka Hirano 对本篇文章进行了校对,提出了建议并作出了修正,特此感谢!

此外,Mathias Bynens 负责本篇文章的更新部分,特此致谢。

Except as otherwise noted, the content of this page is licensed under the Creative Commons Attribution 3.0 License, and code samples are licensed under the Apache 2.0 License. For details, see our Site Policies. Java is a registered trademark of Oracle and/or its affiliates.

Last updated 六月 16, 2017.

本文由9159.com发布于前端,转载请注明出处:您可能属于以下其中某一类,本文中的相关概念

关键词: