ES6 异步编程(一)——Promise


嘿,大家好呀,今天这篇文章和大家聊聊 ES6 的异步编程实现。当然,ES6 中提出了多种解决方案,我们今天先聊聊 Promise 这个异步编程解决方案。

异步编程

JavaScript 中为什么会有异步编程这个概念呢?老生常谈的答案,因为 JavaScript 是单线程的。那么是什么造就了 JavaScript 的单线程呢?在浏览器中,单线程的 JavaScript 的运行机制又是如何的呢?好的,我们先来扯扯这两个问题。

JavaScript 单线程

单线程是历史的原因。很简单,这和 JavaScript 的诞生使命有关。JavaScript 作为浏览器的脚本语言,最初的使命(当然现在大部分也还是 )是用来操作 DOM,产出各种酷炫的用户交互体验。对于浏览器来说,DOM 是浏览器这个环境的一份公共资源。如果 JavaScript 是多线程的,简单点来说,就是两个线程的话,在这种环境中都要考虑很多同步编程的复杂性问题。例如:一个线程要删除一个 DOM 节点,但是另一个线程同时要修改这个 DOM 节点,DOM 是一个共有资源,如何做资源的 lock,然和去调度线程任务,等等这些复杂的问题,对于浏览器这个场景来说,太庞大了。当然,这只是一个部分原因。所以,设计之初,JavaScript 就是以单线程的身份降临世间的。那么,遇到耗时的阻塞任务怎么办,单线程被堵死,那用户岂不是只能盯着浏览器页面,啥子都做不了啥?这时候就需要异步啦,让异步来平息阻塞任务这些暴民。浏览器环境是如何去做的呢?我们接着看。。。

JavaScript 浏览器运行机制

Event Loop

好,让我们来瞻仰一下大神 Philip Roberts 的分析图:


这张图分析的很清晰,stack 是 execution contexts,执行上下文,我们可以理解为 JavaScript 执行的主线程。可能用一个等式来说明更加简洁明了:

1. one thread == one call stack == one thing at a time

在主线程执行的过程中,会执行各种各样的 function,产生各种异步操作,如 DOM 操作、ajax 请求,setTimeout() 等等。这些操作,会在 task queue(callback queue)中产生对应的 event,当 stack 空闲时,event loop 的机制就会去 task queue 中取排在最前面的 event 进入 stack 执行,以此往复。。。

就是这个原因,所以,下面代码两种写法都是可以的:

1. // 写法一:
2. var req = new XMLHttpRequest();
3. req.open( 'GET', url);
4. req.onload = function (){};
5. req.onerror = function (){};
6. req.send();
1. // 写法二:
2. var req = new XMLHttpRequest();
3. req.open( 'GET', url);
4. req.send();
5. req.onload = function (){};
6. req.onerror = function (){};

异步编程解决方案

这也有一个发展的过程,最早的回调函数,到 Promise 对象,再到 Generator 和现在 ES6 的 async 函数,每一次解决方案都在解决不同的编程痛点吧。这里面,Promise 就是我们今天要谈论的对象。Promise 这个异步解决方案很有特点,它非常的“基础”、灵活,灵活到现在很多工具库的异步操作都是它来分装的(经常看库的同学有没有深有体会啊,如果理解的不是太好,很容易迷失在各种 Promise .then return 中)。 
通俗的来说,一个 Promise 对象,代表着一个目前不可用,但是将来某个时间点可以被解析的值,当然,这个上面绑定这好几个处理函数喽(如:.then.all)。

Promise 机制

Promise 对象拥有三种状态,如下:

  • unfulfilled/pending
  • fulfilled/resolved
  • failed/rejected

状态的转换只能由 pending 到 resolved,或者是 pending 到 rejected。关键点在于,状态一旦转变,是不可更改的。详细的实现机制,有太多的文章在解释了,大多数都是从 Promise 这个状态机的特性出发,进行实现的,我就不在这里啰嗦了。api 的基本使用,MDN 上面很清楚,我们主要来了解一下遇到 Promise 的注意点,和我们用 Promise 能做哪些有趣的事情呢?

注意点

  1. new Promise() 会立即执行,而且是无法取消的;
  2. Promise 不会出现回调函数那种错过监听就得不到结果的情况,只要你有 Promise 这个对象了,随时可以 .then() 它;
  3. new Promise() 返回一个 Promise 对象,.then() 方法返回的也是 Promise 实例,但是是一个新的 Promise 实例,不是原来那个了哦,因此,可以有下面这种链式写法:
1. fetch( 'url') .then(res => res.json()) .then(...)

实用举例

1. 异步图片加载
1. function loadImageAsync(url) {
2. return new Promise( function(resolve, reject) {
3. var image = new Image();
4.
5. image.onload = function() {
6. resolve(image);
7. };
8.
9. image.onerror = function() {
10. reject( new Error( 'Could not load image at ' + url));
11. };
12.
13. image.src = url;
14. })
15.}
16. var imgSrc = 'http://images2015.cnblogs.com/blog/1059788/201702/1059788-20170214095743082-1492420313.gif';
17. var img1 = loadImageAsync(imgSrc);
18. // img1 可以添加到 DOM 中去了
2. 延时函数
1. function delay(time) {
2. return new Promise( function(resolve,reject){
3. setTimeout( resolve, time );
4. });
5.}
6.
7.delay( 1000).then( function STEP2(){
8. console.log( "step 2 (after 1000ms)");
9. console.log( new Date().getSeconds());
10. return delay( 5000);
11.}).then( function STEP3(){
12. console.log( "step 3 (after another 5000ms)" );
13. console.log( new Date().getSeconds());
14.});
3. api 请求的统一封装
1. // fetch 返回就是 Promise 对象
2.fetch(url, config).then( (response) => {
3. if (response.status === 403) {
4. Toast.notice( '您发送的内容被服务器阻止!!!', 1000);
5. return new Response( '{"status_code": 403}');
6. }
7. return response;
8. }).then( (response) => response.json(), (error) => ( console && console.log( 'An error occured.', error) // eslint-disable-line no-console
9. )).then( (json) => {
10. if (json.status_code === 4206) {
11. // Login.exit();
12. Toast.notice( '登录已过期,请重新登录', 1000);
13. // 第三方登录站 token 过期,后台执行了清 cookie 的操作,前端登出刷新页面
14. Login.exit();
15. } else if (json.status_code === 1001) {
16. Toast.notice(json.message, 1000);
17. } else if (json.status_code === 2001) {
18. Toast.notice( "请求超时,请稍后重试!", 1000);
19. } else if (json.status_code === 200) {
20. return json;
21. }
22. return json;
23. });

ok,最后再啰嗦一个点,关于 Promise.all()。Promise.all 里的任务列表 [asyncTask(1),asyncTask(2),asyncTask(3)],是按顺序发起的,由于它们都是异步的,互相之间并不阻塞,每个任务完成时机是不确定的。尽管如此,所有任务结束之后,它们的结果仍然是按顺序地映射到 resultList 里,这样就能和Promise.all里的任务列表 [asyncTask(1),asyncTask(2),asyncTask(3)] 一一对应起来了。


猜你喜欢

转载自blog.csdn.net/tangxiaolang101/article/details/78520021