想要完全理解代码,需要理解 this 和闭包的含义。

Promise是什么

简单来说,Promise 主要就是为了解决异步回调的问题。用 Promise 来处理异步回调使得代码层次清晰,便于理解,且更加容易维护。其主流规范目前主要是 Promises/A+。对于 Promise 用法不熟悉的,可以参看篇文章——es6学习笔记5--promise,理解了再来看这篇文章,会对你有很大帮助的。

在开始前,我们先写一个 promise 应用场景来体会下 promise 的作用。目前谷歌和火狐已经支持 es6 的 promise。我们采用 setTimeout 来模拟异步的运行,具体代码如下:

 
function fn1(resolve, reject) {
setTimeout(function() {
console.log('步骤一:执行');
resolve('1');
},500);
} function fn2(resolve, reject) {
setTimeout(function() {
console.log('步骤二:执行');
resolve('2');
},100);
} new Promise(fn1).then(function(val){
console.log(val);
return new Promise(fn2);
}).then(function(val){
console.log(val);
return 33;
}).then(function(val){
console.log(val);
});
 

最终我们写的promise同样可以实现这个功能。

初步构建

下面我们来写一个简单的 promsie。Promise 的参数是函数 fn,把内部定义 resolve 方法作为参数传到 fn 中,调用 fn。当异步操作成功后会调用 resolve 方法,然后就会执行 then 中注册的回调函数。

 
function Promise(fn){
//需要一个成功时的回调
var callback;
//一个实例的方法,用来注册异步事件
this.then = function(done){
callback = done;
}
function resolve(){
callback();
}
fn(resolve);
}
 

加入链式支持

下面加入链式,成功回调的方法就得变成数组才能存储。同时我们给 resolve 方法添加参数,这样就不会输出 undefined。

 
function Promise(fn) {
var promise = this,
value = null;
promise._resolves = []; this.then = function (onFulfilled) {
promise._resolves.push(onFulfilled);
return this;
}; function resolve(value) {
promise._resolves.forEach(function (callback) {
callback(value);
});
} fn(resolve);
}
 
  • promise = this, 这样我们不用担心某个时刻 this 指向突然改变问题。

  • 调用 then 方法,将回调放入 promise._resolves 队列;

  • 创建 Promise 对象同时,调用其 fn, 并传入 resolve 方法,当 fn 的异步操作执行成功后,就会调用 resolve ,也就是执行promise._resolves 队列中的回调;

  • resolve 方法 接收一个参数,即异步操作返回的结果,方便传值。

  • then方法中的 return this 实现了链式调用。

但是,目前的 Promise 还存在一些问题,如果我传入的是一个不包含异步操作的函数,resolve就会先于 then 执行,也就是说 promise._resolves 是一个空数组。

为了解决这个问题,我们可以在 resolve 中添加 setTimeout,来将 resolve 中执行回调的逻辑放置到 JS 任务队列末尾。

 
    function resolve(value) {
setTimeout(function() {
promise._resolves.forEach(function (callback) {
callback(value);
});
},0);
}
 

引入状态

剖析 Promise 之基础篇说 这里存在一点问题: 如果 Promise 异步操作已经成功,之后调用 then  注册的回调再也不会执行了,而这是不符合我们预期的。

对于这句话不是很理解,有知道的可以留言说下,最好能给实例说明下。但我个人觉得是,then 中的注册的回调都会在 resolve 运行之前就添加到数组当中,不会存在不执行的情况啊。

接着上面的步伐,引入状态:

 
function Promise(fn) {
var promise = this,
value = null;
promise._resolves = [];
promise._status = 'PENDING'; this.then = function (onFulfilled) {
if (promise._status === 'PENDING') {
promise._resolves.push(onFulfilled);
return this;
}
onFulfilled(value);
return this;
}; function resolve(value) {
setTimeout(function(){
promise._status = "FULFILLED";
promise._resolves.forEach(function (callback) {
callback(value);
})
},0);
} fn(resolve);
}
 

每个 Promise 存在三个互斥状态:pending、fulfilled、rejected。Promise 对象的状态改变,只有两种可能:从 pending 变为 fulfilled 和从 pending 变为 rejected。只要这两种情况发生,状态就凝固了,不会再变了,会一直保持这个结果。就算改变已经发生了,你再对 Promise 对象添加回调函数,也会立即得到这个结果。这与事件(Event)完全不同,事件的特点是,如果你错过了它,再去监听,是得不到结果的。

加上异步结果的传递

目前的写法都没有考虑异步返回的结果的传递,我们来加上结果的传递:

 
    function resolve(value) {
setTimeout(function(){
promise._status = "FULFILLED";
promise._resolves.forEach(function (callback) {
value = callback(value);
})
},0);
}
 

串行 Promise

串行 Promise 是指在当前 promise 达到 fulfilled 状态后,即开始进行下一个 promise(后邻 promise)。例如我们先用ajax从后台获取用户的的数据,再根据该数据去获取其他数据。

这里我们主要对 then 方法进行改造:

 
    this.then = function (onFulfilled) {
return new Promise(function(resolve) {
function handle(value) {
var ret = isFunction(onFulfilled) && onFulfilled(value) || value;
resolve(ret);
}
if (promise._status === 'PENDING') {
promise._resolves.push(handle);
} else if(promise._status === FULFILLED){
handle(value);
}
}) };
 

then 方法该改变比较多啊,这里我解释下:

  • 注意的是,new Promise() 中匿名函数中的 promise (promise._resolves 中的 promise)指向的都是上一个 promise 对象, 而不是当前这个刚刚创建的。

  • 首先我们返回的是新的一个promise对象,因为是同类型,所以链式仍然可以实现。

  • 其次,我们添加了一个 handle 函数,handle 函数对上一个 promise 的 then 中回调进行了处理,并且调用了当前的 promise 中的 resolve 方法。

  • 接着将 handle 函数添加到 上一个promise 的 promise._resolves 中,当异步操作成功后就会执行 handle 函数,这样就可以 执行 当前 promise 对象的回调方法。我们的目的就达到了。

有些人在这里可能会有点犯晕,有必要对执行过程分析一下,具体参看以下代码:

new Promise(fn1).then(fn2).then(fn3)})

fn1, fn2, fn3的函数具体可参看最前面的定义。

  1. 首先我们创建了一个 Promise 实例,这里叫做 promise1;接着会运行 fn1(resolve);

  2. 但是 fn1 中有一个 setTimeout 函数,于是就会先跳过这一部分,运行后面的第一个 then 方法;

  3. then 返回一个新的对象 promise2,  promise2 对象的 resolve 方法和 then 方法的中回调函数 fn2 都被封装在 handle 中, 然后 handle 被添加到   promise1._resolves 数组中。

  4. 接着运行第二个 then 方法,同样返回一个新的对象 promise3, 包含 promise3 的 resolve 方法和 回调函数 fn3 的 handle 方法被添加到 promise2._resolves 数组中。

  5. 到此两个 then 运行结束。 setTimeout 中的延迟时间一到,就会调用 promise1的 resolve方法。

  6. resolve 方法的执行,会调用 promise1._resolves 数组中的回调,之前我们添加的 handle 方法就会被执行; 也就是 fn2 和 promsie2 的 resolve 方法,都被调用了。

  7. promise3 的 resolve 方法

  8. 至此所有回调执行结束

但这里还存在一个问题,就是我们的 then 里面函数不能对 Promise 对象进行处理。这里我们需要再次对 then 进行修改,使其能够处理 promise 对象。

 
this.then = function (onFulfilled) {
return new Promise(function(resolve) {
function handle(value) {
var ret = typeof onFulfilled === 'function' && onFulfilled(value) || value;
if( ret && typeof ret ['then'] == 'function'){
ret.then(function(value){
resolve(value);
});
} else {
resolve(ret);
}
}
if (promise._status === 'PENDING') {
promise._resolves.push(handle);
} else if(promise._status === FULFILLED){
handle(value);
}
}) };
 

在 then 方法里面,我们对 ret 进行了判断,如果是一个 promise 对象,就会调用其 then 方法,形成一个嵌套,直到其不是promise对象为止。同时 在 then 方法中我们添加了调用 resolve 方法,这样链式得以维持。

失败处理

异步操作不可能都成功,在异步操作失败时,标记其状态为 rejected,并执行注册的失败回调。

有了之前处理 fulfilled 状态的经验,支持错误处理变得很容易。毫无疑问的是,在注册回调、处理状态变更上都要加入新的逻辑:

 
    this.then = function (onFulfilled, onRejected) {
return new Promise(function(resolve, reject) {
function handle(value) {
.......
}
function errback(reason){
reason = isFunction(onRejected) && onRejected(reason) || reason;
reject(reason);
}
if (promise._status === 'PENDING') {
promise._resolves.push(handle);
promise._rejects.push(errback);
} else if(promise._status === 'FULFILLED'){
handle(value);
} else if(promise._status === 'REJECTED') {
  errback(promise._reason);
}
}) }; function reject(value) {
setTimeout(function(){
promise._status = "REJECTED";
promise._rejects.forEach(function (callback) {
promise._reason = callback( value);
})
},0);
}
 

添加Promise.all方法

Promise.all 可以接收一个元素为 Promise 对象的数组作为参数,当这个数组里面所有的 Promise 对象都变为 resolve 时,该方法才会返回。

具体代码如下:

 
Promise.all = function(promises){
if (!Array.isArray(promises)) {
throw new TypeError('You must pass an array to all.');
}
    // 返回一个promise 实例
return new Promise(function(resolve,reject){
var i = 0,
result = [],
len = promises.length,
  count = len;
      // 每一个 promise 执行成功后,就会调用一次 resolve 函数
function resolver(index) {
  return function(value) {
   resolveAll(index, value);
  };
 } function rejecter(reason){
reject(reason);
} function resolveAll(index,value){
       // 存储每一个promise的参数
result[index] = value;
       // 等于0 表明所有的promise 都已经运行完成,执行resolve函数
if( --count == 0){
resolve(result)
}
}
      // 依次循环执行每个promise
for (; i < len; i++) {
         // 若有一个失败,就执行rejecter函数
promises[i].then(resolver(i),rejecter);
}
});
}
 

Promise.all会返回一个 Promise 实例,该实例直到参数中的所有的 promise 都执行成功,才会执行成功回调,一个失败就会执行失败回调。

日常开发中经常会遇到这样的需求,在不同的接口请求数据然后拼合成自己所需的数据,通常这些接口之间没有关联(例如不需要前一个接口的数据作为后一个接口的参数),这个时候  Promise.all 方法就可以派上用场了。

添加Promise.race方法

该函数和 Promise.all 相类似,它同样接收一个数组,不同的是只要该数组中的任意一个 Promise 对象的状态发生变化(无论是 resolve 还是 reject)该方法都会返回。我们只需要对 Promise.all 方法稍加修改就可以了。

 
Promise.race = function(promises){
if (!Array.isArray(promises)) {
throw new TypeError('You must pass an array to race.');
}
return Promise(function(resolve,reject){
var i = 0,
len = promises.length; function resolver(value) {
resolve(value);
} function rejecter(reason){
reject(reason);
} for (; i < len; i++) {
promises[i].then(resolver,rejecter);
}
});
}
 

代码中没有类似一个 resolveAll 的函数,因为我们不需要等待所有的 promise 对象状态都发生变化,只要一个就可以了。

 

代码写完了,总要写几个实例看看效果啊,具体看下面的测试代码:

 
var getData100 = function(){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve('100ms');
},1000);
});
} var getData200 = function(){
return new Promise(function(resolve,reject){
setTimeout(function(){
resolve('200ms');
},2000);
});
}
var getData300 = function(){
return new Promise(function(resolve,reject){
setTimeout(function(){
reject('reject');
},3000);
});
} getData100().then(function(data){
console.log(data); // 100ms
return getData200();
}).then(function(data){
console.log(data); // 200ms
return getData300();
}).then(function(data){
console.log(data);
}, function(data){
console.log(data); // 'reject'
}); Promise.all([getData100(), getData200()]).then(function(data){
console.log(data); // [ "100ms", "200ms" ]
}); Promise.race([getData100(), getData200(), getData300()]).then(function(data){
console.log(data); // 100ms
});
Promise.resolve('resolve').then(function(data){
console.log(data); //'resolve'
})
Promise.reject('reject函数').then(function(data){
console.log(data);
}, function(data){
console.log(data); //'reject函数'
})

一个简单完整的promiseDemo的更多相关文章

  1. 实现iOS图片等资源文件的热更新化(五): 一个简单完整的资源热更新页面

    简介 一个简单的关于页面,有一个图片,版本号,App名称等,着重演示各个系列的文章完整集成示例. 动机与意义 这是系列文章的最后一篇.今天抽空写下,收下尾.文章本身会在第四篇的基础上,简单扩充下代码, ...

  2. Appium+python的一个简单完整的用例

    最近一直在忙,终于有时间来整理一下,传一个简单的用例,运行之后可以看到用例的报告,希望对大家有帮助. HTMLTestRunner这个包网上有很多,大家可以自己下载. 1 import unittes ...

  3. ubuntu gtk2.0 一个简单完整的窗口

    //gtk_main();开了线程,关闭窗口并不能退出程序,需要手动添加 //gtk2.0,window quit compelete example #include <gtk/gtk.h&g ...

  4. 【模块化编程】理解requireJS-实现一个简单的模块加载器

    在前文中我们不止一次强调过模块化编程的重要性,以及其可以解决的问题: ① 解决单文件变量命名冲突问题 ② 解决前端多人协作问题 ③ 解决文件依赖问题 ④ 按需加载(这个说法其实很假了) ⑤ ..... ...

  5. 基于Servlet、JSP、JDBC、MySQL的一个简单的用户注冊模块(附完整源代码)

    近期看老罗视频,做了一个简单的用户注冊系统.用户通过网页(JSP)输入用户名.真名和password,Servlet接收后通过JDBC将信息保存到MySQL中.尽管是个简单的不能再简单的东西,但麻雀虽 ...

  6. 计算机程序的思维逻辑 (60) - 随机读写文件及其应用 - 实现一个简单的KV数据库

    57节介绍了字节流, 58节介绍了字符流,它们都是以流的方式读写文件,流的方式有几个限制: 要么读,要么写,不能同时读和写 不能随机读写,只能从头读到尾,且不能重复读,虽然通过缓冲可以实现部分重读,但 ...

  7. 如何创建一个简单的Visual Studio Code扩展

    注:本文提到的代码示例下载地址>How to create a simple extension for VS Code VS Code 是微软推出的一款轻量级的代码编辑器,免费,开源,支持多种 ...

  8. 无聊的人用JS实现了一个简单的打地鼠游戏

    直入正题,用JS实现一个简单的打地鼠游戏 因为功能比较简单就直接裸奔JS了,先看看效果图,或者 在线玩玩 吧 如果点击颜色比较深的那个(俗称坏老鼠),将扣分50:如果点击颜色比较浅的那个(俗称好老鼠) ...

  9. JMeter基础之一 一个简单的性能测试

    JMeter基础之一 一个简单的性能测试 上一节中,我们了解了jmeter的一此主要元件,那么这些元件如何使用到性能测试中呢.这一节创建一个简单的测试计划来使用这些元件.该计划对应的测试需求. 1)测 ...

随机推荐

  1. C语言数组成绩排序

    #include<stdio.h> #define N 10 int main() { int s,i,j,tmp; int a[10]={78,56,38,99,81,86,39,100 ...

  2. 关于Burp Suite Intruder 的四种攻击方式

    以下面这一段参数为例,被§§包围的部分为需要破解的部分: user=§ss§&password=§zxcv§&imageField.x=17&imageField.y=1 (1 ...

  3. GET乱码以及POST乱码的解决方法

    GET乱码以及POST乱码的解决方法 作者:东坡下载  来源:uzzf  发布时间:2010-10-14 11:40:01  点击: 一.GET乱码的解决方法 在tomcat的server.xml文件 ...

  4. Jquery实现下拉tab切换

    //需求:分别点击html,css,js元素时展示对应的列表内容,其他列表内容要自动收起来 //代码如下 <!DOCTYPE html> <html lang="en&qu ...

  5. Job 失败了怎么办?【转】

    上一节讨论了 Job 执行成功的情况,如果失败了会怎么样呢? 修改 myjob.yml,故意引入一个错误: 先删除之前的 Job: 如果将 restartPolicy 设置为 OnFailure 会怎 ...

  6. VUE - 引入 npm 安装的模块 以及 uuid模块的使用

    <template>   <div>       <form @submit.prevent="addTodo">         <in ...

  7. ROS大型工程学习(四)一键安装所有缺失的依赖!!!

    在正文之前,我必须向rosdep道个歉,前面那篇常用命令行中没有它的位置,现在单独写一篇补充!! 突然发现rosdep有个惊人的功能, rosdep install --from-paths src ...

  8. 杭电oj1860:统计字符(字符串hash / 水题)

    统计字符 题目链接 Time Limit: 1000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others) Problem D ...

  9. Java If ... Else

    章节 Java 基础 Java 简介 Java 环境搭建 Java 基本语法 Java 注释 Java 变量 Java 数据类型 Java 字符串 Java 类型转换 Java 运算符 Java 字符 ...

  10. Spring容器的创建原理

    1.new ioc容器(AnnotationConfigApplicationContext 注解ioc) 2.refresh()方法调用 2.1 prepareRefresh()刷新前的预处理 a: ...