图片 2

关于Promise的一些个人理解

第二章 Promise 原理

总结

  首先,Promise是一个对象,如同其字面意思一样,代表了未来某时间才会知道结果的时间,不受外界因素的印象。Promise一旦触发,其状态只能变为fulfilled或者rejected,并且已经改变不可逆转。Promise的构造函数接受一个函数作为参数,该参数函数的两个参数分别为resolve和reject,其作用分别是将Promise的状态由pending转化为fulfilled或者rejected,并且将成功或者失败的返回值传递出去。then有两个函数作为Promise状态改变时的回调函数,当Promise状态改变时接受传递来的参数并调用相应的函数。then中的回调的过程为异步操作。catch方法是对.then(null,rejectFn)的封装(语法糖),用于指定发生错误时的回掉函数。一般来说,建议不要再then中定义rejected状态的回调函数,应该使用catch方法代替。all和race都是竞速函数,all结束的时间取决于最慢的那个,其作为参数的Promise函数一旦有一个状态为rejected,则总的Promise的状态就为rejected;而race结束的时间取决于最快的那个,一旦最快的那个Promise状态发生改变,那个其总的Promise的状态就变成相应的状态,其余的参数Promise还是会继续进行的。

  当然在es7时代,也出现了await/async的异步方案,这会是我们以后谈论的。

图片 1

图片 2

 

var http = {
    get: function(url) {
        var promise = new Promise(function(resolve, reject) {
            $.ajax({
                url: url,
                method: 'get',
                success: function(data) {
                    resolve(data);
                },
                error: function(xhr, statusText) {
                    reject(statusText);
                }
            });
        });
        return promise;
    }
};
http.get('solve.php').then(function(data) {
    return data;
}, function(err) {
    return Promise.reject('Sorry, file not Found.');
}).then(function(data) {
    document.write(data);
}, function(err) {
    document.write(err);
});

二、基于事件响应的异步模型

@朴灵 写的 EventProxy 就是基于事件响应的异步模型,按理说,这个实现的逻辑是最清晰的,不过代码量稍微多一点。

function taskA(){
    setTimeout(function(){
        var result = “A”;
        E.emit(“taskA”, result);
    }, 1000);
}

function taskB(){
    setTimeout(function(){
        var result = “B”;
        E.emit(“taskB”, result);
    }, 1000);
}

E.all([“taskA”, “taskB”], function(A, B){
    return A + B;
});

我没有看他的源码,但是想想,应该是这个逻辑。只需要在消息中心管理各个
emit 以及消息注册。这里的错误处理值得思考下。

在半年前,也写过一篇关于异步编程的文章:JavaScript异步编程原理,感兴趣的可以去读一读。

Promise是什么?

  Promise是JS异步编程中的重要概念,异步抽象处理对象,是目前比较流行Javascript异步编程解决方案之一。这句话说的很明白了,Promise是一种用于解决异步问题的思路、方案或者对象方式。在js中,经常使用异步的地方是Ajax交互。比如在es5时代,jQueryajax的使用success来完成异步的:

$.ajax({
   url:'/xxx',
   success:()=>{},
   error: ()=>{}
})

  这种方法可以清楚的让读代码的人明白那一部分是Ajax请求成功的回调函数和失败的回调函数。但是问题来了,当一次请求需要连续请求多个接口时,这段代码仿佛进入了一团乱麻中:

// 第一次 
$.ajax({
     url:'/xxx',
     success:()=>{
         // 第二次
         $.ajax({
             url:'/xxx',
             success:()=>{
               // 第三次
               $.ajax({
                  url:'/xxx',
                  success:()=>{
                   // 可能还会有
                  },
                  error: ()=>{}
                })
             },
             error: ()=>{}
        })
     },
     error: ()=>{}
}) 

  也许因为success和error这两个函数的存在,理解这段代码会很简单,但是当我们更改需求的时候,这将成为一个棘手的问题。这就是回调地狱。

  当然,这是es5时代。当js这门语言发展到es6时代时,Promise的出现给异步带来了变革。Promise提供一个then,来为异步提供回调函数:

$.ajax({
    url:'/xxx',
}).then( ()=>{
   // 成功的回调
}, ()=>{
  // 失败的回调 
})

  而其先进之处则是,可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作。

Promise.reject()方法会返回一个被reject()的Promise对象,因此使得我们可以继续走下一个then()中的onRejected(reason)方法。
以上便是我对Promise以及Promise的执行过程的一个基本理解,这是我参考的文档:MDN:
Promise

读完这篇文章,预计会消耗你 40 分钟的时间。

then

  then方法用于注册当状态变为fulfilled或者reject时的回调函数:

// onFulfilled 是用来接收promise成功的值
// onRejected 是用来接收promise失败的原因
promise.then(onFulfilled, onRejected);

  需要注意的地方是then方法是异步执行的。

// resolve(成功) onFulfilled会被调用
const promise = new Promise((resolve, reject) => {
   resolve('fulfilled'); // 状态由 pending => fulfilled
});
promise.then(result => { // onFulfilled
    console.log(result); // 'fulfilled' 
}, reason => { // onRejected 不会被调用
})

// reject(失败) onRejected会被调用
const promise = new Promise((resolve, reject) => {
   reject('rejected'); // 状态由 pending => rejected
});
promise.then(result => { // onFulfilled 不会被调用
}, reason => { // onRejected 
    console.log(rejected); // 'rejected'
})
var http = {
    get: function(url) {
        var promise = new Promise(function(resolve, reject) {
            $.ajax({
                url: url,
                method: 'get',
                success: function(data) {
                    resolve(data);
                },
                error: function(xhr, statusText) {
                    reject(statusText);
                }
            });
        });
        return promise;
    }
};
http.get('data.php').then(function(data) {
    document.write(data);
}, function(err) {
    document.write(err);
});

/* data.php文件 */
<?php
echo '{"name":"Tom","age":"22"}';

三、包含错误传递的 Promise

真的很羡慕你能看到这么详细的文章,当然,后面会更加精彩!

没有错误处理的 Promise 只能算是一个半成品,虽说可以通过在最外层加一个
try..catch
来捕获错误,但没法具体定位是哪个事务发生的错误。并且这里的错误不仅仅包含
JavaScript Error,还有诸如 ajax 返回的 data code 不是 200 的情况等。

先看一个浏览器内置 Promise 的实例(该代码可在现代浏览器下运行):

new Promise(function(resolve, reject){
    resolve(“start”);
}).then(function(data){
    console.log(data);
    throw “error”;
}).catch(function(err){
    console.log(err);
    return “end”;
}).then(function(data){
    console.log(data)
});

Promise 的回调和 then 方法都是接受两个参数:

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

promise.then(
    function(value){/* code here */}, 
    function(reason){/* code here */}
);

事务处理过程中,如果有值返回,则作为 value,传入到 resolve
函数中,若有错误产生,则作为 reason 传入到 reject 函数中处理。

在初始化 Promise 对象时,若传入的回调中没有执行 resolve 或者
reject,这需要我们主动去启动事务队列。

promise.resolve();
promise.reject();

上面两种都是可以启动一个队列的。这里跟第二章第二节的 resolve
函数用法类似。Promise 对象还提供了 catch 函数,起用法等价于下面所示:

promise.catch();
// 等价于
promise.then(null, function(reason){});

还有两个 API:

promise.all();
promise.race();

后续再讲。先看看这个有错误处理的 Promise 是如何实现的。

function Promise(resolver){
    this.status = “pending”;
    this.value = null;
    this.handlers = [];
    this._doPromise.call(this, resolver);
}

_doPromise 方法在实例化 Promise 函数时就执行。如果送入的回调函数
resolver 中已经 resolve 或者 reject
了,程序就已经启动了,所以在实例化的时候就开始判断。

_doPromise: function(resolver){
    var called = false, self = this;
    try{
        resolver(function(value){
            // 如果没有 call 则继续,并标记 called 为 true
            !called && (called = !0, self.resolve(value));
        }, function(reason){
            // 同上
            !called && (called = !0, self.reject(reason));
        });
    } catch(e) {
        // 同上,捕获错误,传递错误到下一个 then 事务
        !called && (called = !0, self.reject(e));
    }
},

只要 resolve 或者 reject 就会标记程序 called 为
true,表示程序已经启动了。

resolve: function(value) {
    try{
        if(this === value){
            throw new TypeError(‘流程已完成,不能再次开启流程!’);
        } else {
            // 如果还有子事务队列,继续执行
            value && value.then && this._doPromise(value.then);
        }
        // 执行完了之后标记为完成
        this.status = “fulfilled”;
        this.value = value;
        this._dequeue();
    } catch(e) {
        this.reject(e);
    }
},
reject: function(reason) {
    // 标记状态为出错
    this.status = “rejected”;
    this.value = reason;
    this._dequeue();
},

可以看到,每次 resolve 的时候都会用一个 try..catch 包裹来捕获未知错误。

_dequeue: function(){
    var handler;
    // 执行事务,直到队列为空
    while (this.handlers.length) {
        handler = this.handlers.shift();
        this._handle(handler.thenPromise, handler.onFulfilled, handler.onRejected);
    }
},

无论是 resolve 还是 reject
都会让程序往后奔流,直到结束所有事务,所以这两个方法中都有 _dequeue
函数。

_handle: function(thenPromise, onFulfilled, onRejected){
    var self = this;

    setTimeout(function() {
        // 判断下次操作采用哪个函数,reject 还是 resolve
        var callback = self.status == “fulfilled” 
                       ? onFulfilled 
                       : onRejected;
        // 只有是函数才会继续回调
        if (typeof callback === ‘function’) {
            try {
                self.resolve.call(thenPromise, callback(self.value));
            } catch(e) {
                self.reject.call(thenPromise, e);
            }
            return;
        }
        // 否则就将 value 传递给下一个事务了
        self.status == “fulfilled”
                        ? self.resolve.call(thenPromise, self.value) 
                        : self.reject.call(thenPromise, self.value);
    }, 1);
},

这个函数跟上一节提到的 _fire 类似,如果 callback 是
function,就会进入子事务队列,处理完了之后退回到主事务队列。最后一个
then 方法,将事务推进队列。

then: function(onFulfilled, onRejected){
    var thenPromise = new Promise(function() {});

    if (this.status == “pending”) {
        this.handlers.push({
            thenPromise: thenPromise,
            onFulfilled: onFulfilled,
            onRejected: onRejected
        });
    } else {
        this._handle(thenPromise, onFulfilled, onRejected);
    }

    return thenPromise;
}

如果第二节没有理解清楚,这一节也会让人头疼,这一部分讲的比较粗糙。

Promise的几种方法

/* 用于描述思维的代码 */
function Promise(executor) {
    // 共有三种状态:pending(准备)、fulfilled(完成)、rejected(拒绝)
    this.PromiseStatus = 'pending';
    // 用于存储返回的数据
    this.PromiseValue;
    // 完成
    var resolve = function(reson) {};
    // 拒绝
    var reject = function(reson) {};
    ...
    // 开始执行承诺
    executor(resolve, reject);
}
Promise.prototype.then = function() {};
Promise.prototype.chain = function() {};
Promise.prototype.catch = function() {};
...

ECMAScript 6 已经将异步操作纳入了规范,现代浏览器也内置了 Promise
对象供我们进行异步编程,那么此刻,还在等啥?赶紧学习学习 Promise
的内部原理吧!

catch

  catch在链式写法中可以捕获前面then中发送的异常。

fn = new Promise(function (resolve, reject) {
  let num = Math.ceil(Math.random() * 10)
  if (num > 5) {
    resolve(num)
  } else {
    reject(num)
  }
})
fn..then((res)=>{
  console.log(res)
}).catch((err)=>{
  console.log(`err==>${err}`)
})

  其实,catch相当于then(null,onRejected),前者只是后者的语法糖而已。

PromiseStatus:用于记录Promise对象的三种状态,这三中状态分别是:
pending:待定状态,Promise对象刚被初始化的状态
fulfilled:完成状态,承诺被完成了的状态
rejected:拒绝状态,承诺完成失败的状态
Promise的初始状态是pending。随后会更具承诺完成的情况更改PromiseStatus的值。

第三章 异步编程

  “金三银四,金九银十”,都是要收获的季节。面对各种面试题,各种概念、原理都要去记,挺枯燥的。本文是面向面试题和实际使用谈一下Promise。

提醒:本文中的内容是我看了一些文档后自己的理解,可能会有一些理解上的错误。若简友发现了可以提出来,我会及时修改。_

二、模型

为了让代码流程更加清晰,我们假想着能够按照下面的流程来跑程序:

new Promise(ready).then(getTpl).then(getData).then(makeHtml).resolve();

先将要事务按照执行顺序依次 push 到事务队列中,push 完了之后再通过
resolve 函数启动整个流程。

整个流程的操作模型如下:

promise(ok).then(ok_1).then(ok_2).then(ok_3).reslove(value)------+
         |         |          |          |                       |
         |         |          |          |        +=======+      |
         |         |          |          |        |       |      |
         |         |          |          |        |       |      |
         +---------|----------|----------|--------→  ok() ←------+
                   |          |          |        |   ↓   |
                   |          |          |        |   ↓   |
                   +----------|----------|--------→ ok_1()|
                              |          |        |   ↓   |
                              |          |        |   ↓   |
                              +----------|--------→ ok_2()|
                                         |        |   ↓   |
                                         |        |   ↓   |
                                         +--------→ ok_3()-----+
                                                  |       |    |       
                                                  |       |    ↓
@ Created By Barret Lee                           +=======+   exit

在 resolve 之前,promise 的每一个 then 都会将回调函数压入队列,resolve
后,将 resolve
的值送给队列的第一个函数,第一个函数执行完毕后,将执行结果再送入下一个函数,依次执行完队列。一连串下来,一气呵成,没有丝毫间断。

resolve、reject

  Promise.resolve 返回一个fulfilled状态的promise对象,Promise.reject
返回一个rejected状态的promise对象。

Promise.resolve('hello').then(function(value){
    console.log(value);
});

Promise.resolve('hello');
// 相当于
const promise = new Promise(resolve => {
   resolve('hello');
});

// reject反之

PromiseStatus:用于记录返回的数据或者错误。当承诺完成时,会把返回的数据赋给PromiseStatus。如果承诺执行失败了,那么失败的原因也会赋给此变量。

三、简单的封装

如果了解 Promise,可以移步下方,看看对 Promise 的封装:

Github: 
DEMO: 

如果还不是很了解,可以往下阅读全文,了解一二。

race

  promise.race()方法也可以处理一个promise实例数组但它和promise.all()不同,从字面意思上理解就是竞速,那么理解起来上就简单多了,也就是说在数组中的元素实例那个率先改变状态,就向下传递谁的状态和异步结果。但是,其余的还是会继续进行的。

let p1 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('1s') //1s后输出
    resolve(1)
  },1000)
})
let p10 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('10s') //10s后输出
    resolve(10) //不传递
  },10000)
})
let p5 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('5s') //5s后输出
    resolve(5) //不传递
  },5000)
})
Promise.race([p1, p10, p5]).then((res)=>{
    console.log(res); // 最后输出
})

  因此,在这段代码的结尾我们的结果为

1s
1
5s
10s

  我们可以根据race这个属性做超时的操作:

//请求某个图片资源
let requestImg = new Promise(function(resolve, reject){
        var img = new Image();
        img.onload = function(){
            resolve(img);
        }
    });
//延时函数,用于给请求计时
let timeOut = new Promise(function(resolve, reject){
        setTimeout(function(){
            reject('图片请求超时');
        }, 5000);
    });

Promise.race([requestImg, timeout]).then((res)=>{
    console.log(res);
}).catch((err)=>{
    console.log(err);
});

resolve()和reject():Promise构造函数中有两个闭包的函数resolve()和reject()。在new一个新的Promise的时候会传递一件你需要做的事情(executor)。这个executor是一个函数,在Promise的构造函数中它会被传入两个参数,这两个参数即我们的两个闭包的函数resolve()和reject(),以便你在executor中判定是否完成了你的承诺。

一、小结

文章比较长,阅读了好几天别人写的东西,自己提笔还是比较轻松的,本文大概花费了
6 个小时撰写。

本文主要解说了 Promise
的应用场景和实现原理,如果你能够顺畅的读完全文并且之处文中的一些错误,说明你已经悟到了:)

Promise
使用起来不难,但是理解其原理还是有点偏头痛的,所以下面列举的几篇相关阅读也建议读者点进去看看。

Promise的原理

  在Promise的内部,有一个状态管理器的存在,有三种状态:pending、fulfilled、rejected。

    (1) promise 对象初始化状态为 pending。

    (2) 当调用resolve(成功),会由pending => fulfilled。

    (3) 当调用reject(失败),会由pending => rejected。

  因此,看上面的的代码中的resolve(num)其实是将promise的状态由pending改为fulfilled,然后向then的成功回掉函数传值,reject反之。但是需要记住的是注意promsie状态
只能由 pending => fulfilled/rejected,
一旦修改就不能再变(记住,一定要记住,下面会考到)。

  当状态为fulfilled(rejected反之)时,then的成功回调函数会被调用,并接受上面传来的num,进而进行操作。promise.then方法每次调用,都返回一个新的promise对象
所以可以链式写法(无论resolve还是reject都是这样)。

/* 用于描述思维的代码 */
executor(resolve, reject) {
    ...
    resolve(value);
    ...
}
...
resolve(value) {
    PromiseStatus = 'fulfilled';
    PromiseValue = value;
    ...
    // 接着调用回调链中的回调函数
}

一、什么是 Promise ?

那么,什么是 Promise ?

Promise 可以简单理解为一个事务,这个事务存在三种状态:

  1. 已经完成了 resolved
  2. 因为某种原因被中断了 rejected
  3. 还在等待上一个事务结束 pending

上文中我们举了一个栗子,获取模板和数据之后再将拼合的数据插入到 DOM
中,这里我们将整个程序分解成多个事务:

事务一:     获取模板
               ↓
事务二:     获取数据
               ↓
事务三: 拼合之后插入到 DOM

在事务一结束之前,也就是模板代码从服务器拉取过来之前,事务二和事务三都处于
pending
状态,他们必须等待上一个事务结束。而事务一结束之后会将自身状态标记为
resolved,并把该事务中处理的结果移交给事务二继续处理(当然,这里如果没有数据返回,事务二就不会获得上一个事务的数据),依次类推,直到最后一个事务操作结束。

在事务操作的过程中,若遇到错误,比如事务一获取数据存在跨域问题,那事务就会操作失败,此时它会将自身的状态标记为
rejected,由于后续事务都是承接前一事务的,前一事务已经宣告工程已经玩不成了,那么后续的所有事务都会将自己标记为
rejected,其标记理由(reason)就是出错事务的报错信息(这个报错信息可以使用
try…catch 来捕获,也可以通过程序自身来捕获,如 ajax 的 onerror
事件、ajax 返回的状态码为 404 等)。

小结:Promise
就是一个事务的管理器。他的作用就是将各种内嵌回调的事务用流水形式表达,其目的是为了简化编程,让代码逻辑更加清晰。

由于整个程序的实现比较难理解,对于 Promise,我们将分为两部分阐述:

  • 无错误传递的
    Promise,也就是事务不会因为任何原因中断,事务队列中的事项都会被依次处理,此过程中
    Promise 只有 pending 和 resolved 两种状态,没有 rejected 状态。
  • 包含错误的
    Promise,每个事务的处理都必须使用容错机制来获取结果,一旦出错,就会将错误信息传递给下一个事务,如果错误信息会影响下一个事务,则下一个事务也会
    rejected,如果不会,下一个事务可以正常执行,依次类推。

Promise相关的面试题

1.

const promise = new Promise((resolve, reject) => {
    console.log(1);
    resolve();
    console.log(2);
});
promise.then(() => {
    console.log(3);
});
console.log(4);

  输出结果为:1,2,4,3。

  解题思路:then方法是异步执行的。

2.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('success')
    reject('error')
  }, 1000)
})
promise.then((res)=>{
  console.log(res)
},(err)=>{
  console.log(err)
})

  输出结果:success

  解题思路:Promise状态一旦改变,无法在发生变更。

3.

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

  输出结果:1

  解题思路:Promise的then方法的参数期望是函数,传入非函数则会发生值穿透。

4.

setTimeout(()=>{
  console.log('setTimeout')
})
let p1 = new Promise((resolve)=>{
  console.log('Promise1')
  resolve('Promise2')
})
p1.then((res)=>{
  console.log(res)
})
console.log(1)

  输出结果:

    Promise1
    1
    Promise2
    setTimeout

  解题思路:这个牵扯到js的执行队列问题,整个script代码,放在了macrotask
queue中,执行到setTimeout时会新建一个macrotask
queue。但是,promise.then放到了另一个任务队列microtask
queue
中。script的执行引擎会取1个macrotask
queue中的task,执行之。然后把所有microtask
queue
顺序执行完,再取setTimeout所在的macrotask
queue按顺序开始执行。(具体参考)

 5.

Promise.resolve(1)
    .then((res) => {
        console.log(res);
        return 2;
    })
    .catch((err) => {
        return 3;
    })
    .then((res) => {
        console.log(res);
    });

  输出结果:1  2

  解题思路:Promise首先resolve(1),接着就会执行then函数,因此会输出1,然后在函数中返回2。因为是resolve函数,因此后面的catch函数不会执行,而是直接执行第二个then函数,因此会输出2。

6.

const promise = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('开始');
resolve('success');
}, 5000);
});

const start = Date.now();
promise.then((res) => {
console.log(res, Date.now() - start);
});

promise.then((res) => {
console.log(res, Date.now() - start);
});

  输出结果:

    开始

    success 5002

    success 5002

  解题思路:promise
.then或者.catch可以被调用多次,但这里 Promise
构造函数只执行一次。或者说 promise
内部状态一经改变,并且有了一个值,那么后续每次调用.then 或者.catch都会直接拿到该值。

7.

let p1 = new Promise((resolve,reject)=>{
  let num = 6
  if(num<5){
    console.log('resolve1')
    resolve(num)
  }else{
    console.log('reject1')
    reject(num)
  }
})
p1.then((res)=>{
  console.log('resolve2')
  console.log(res)
},(rej)=>{
  console.log('reject2')
  let p2 = new Promise((resolve,reject)=>{
    if(rej*2>10){
      console.log('resolve3')
      resolve(rej*2)
    }else{
      console.log('reject3')
      reject(rej*2)
    }
  })
  return p2
}).then((res)=>{
  console.log('resolve4')
  console.log(res)
},(rej)=>{
  console.log('reject4')
  console.log(rej)
})

  输出结果:

    reject1
    reject2
    resolve3
    resolve4
    12

  解题思路:我们上面说了Promise的先进之处在于可以在then方法中继续写Promise对象并返回。

8.重头戏!!!!实现一个简单的Promise

function Promise(fn){
  var status = 'pending'
  function successNotify(){
      status = 'fulfilled'//状态变为fulfilled
      toDoThen.apply(undefined, arguments)//执行回调
  }
  function failNotify(){
      status = 'rejected'//状态变为rejected
      toDoThen.apply(undefined, arguments)//执行回调
  }
  function toDoThen(){
      setTimeout(()=>{ // 保证回调是异步执行的
          if(status === 'fulfilled'){
              for(let i =0; i< successArray.length;i ++)    {
                  successArray[i].apply(undefined, arguments)//执行then里面的回掉函数
              }
          }else if(status === 'rejected'){
              for(let i =0; i< failArray.length;i ++)    {
                  failArray[i].apply(undefined, arguments)//执行then里面的回掉函数
              }
          }
      })
  }
  var successArray = []
  var failArray = []
  fn.call(undefined, successNotify, failNotify)
  return {
      then: function(successFn, failFn){
          successArray.push(successFn)
          failArray.push(failFn)
          return undefined // 此处应该返回一个Promise
      }
  }
}

  解题思路:Promise中的resolve和reject用于改变Promise的状态和传参,then中的参数必须是作为回调执行的函数。因此,当Promise改变状态之后会调用回调函数,根据状态的不同选择需要执行的回调函数。

executor():
executor()函数中执行的代码就是子程序需要完成事。在executor()函数内如果调用了resolve(),resolve()则会把Promise对象的状态PromiseStatus修改为fulfilled,把resolve(value)中的value值赋给Promise对象的PromiseValue。然后再依次执行由then()方法构成的回调链中的回调函数。同样,在executor()中调用reject()的过程也是类似的。调用过程如下:

一、场景再现

由于 javascript
的单线程性质,我们必须等待上一个事件执行完成才能处理下一步,如下:

// DOM ready之后执行
$(document).ready(function(){
    // 获取模板
    $.get(url, function(tpl){
        // 获取数据
        $.get(url2, function(data){
            // 构建 DOMString
            makeHtml(tpl, data, function(str){
                // 插入到 DOM 中
                $(obj).html(str);
            });
        });
    });
});

为了减少首屏数据的加载,我们将一些模板和所有数据都放在服务器端,当用户操作某个按钮时,需要将模板和数据拼接起来插入到
DOM 中,这个过程还必须在 DOMReady
之后才能执行。这种情况是十分常见的,如果异步操作再多一些,整个代码的缩进让人看着很不舒服,为了优雅地处理这个问题,ECMAScript
6 引入了 Promise 的概念,目前一些现代浏览器已经支持这些新东西了!

Promise的用法

  说完了Promise是什么,下面让我们研究一下Promise怎么使用。首先,Promise是一个对象,因此,我们使用new的方式新建一个。然后给它传一个函数作为参数,这个函数呢也有两个参数,一个叫resolve(决定),一个叫reject(拒绝),这两个参数也是函数。紧接着,我们使用then来调用这个Promise:

const fn = new Promise(function (resolve, reject) {
  setTimeout(()=>{
    let num = Math.ceil(Math.random() * 10) // 假设num为7
    if (num > 5) {
      resolve(num) //返回7
    } else {
      reject(num)
    }
  },2000)
})
fn.then((res)=>{
  console.log(res) // 7
},(err)=>{
  console.log(err)
})

  这就是最简单的Promise的使用。假设2秒钟之后生成随机数为7,因此resolve回调函数运行,then走第一个函数,console.log(7)。假设2秒钟之后生成随机数为3,因此reject回调函数运行,then走第二个函数,console.log(3)。

  那你可能说了,Promise要是就这点能耐也没什么大不了的啊?我们上面说了Promise的先进之处在于可以在then方法中继续写Promise对象并返回,然后继续调用then来进行回调操作:

fn = new Promise(function (resolve, reject) {
  let num = Math.ceil(Math.random() * 10)
  if (num > 5) {
    resolve(num)
  } else {
    reject(num)
  }
})
// 第一次回调
fn.then((res)=>{
  console.log(`res==>${res}`)
  return new Promise((resolve,reject)=>{
    if(2*res>15){
      resolve(2*res)
    }else{
      reject(2*res)
    }
  })
},(err)=>{
  console.log(`err==>${err}`)
}).then((res)=>{ // 第二次回调
  console.log(res)
},(err)=>{
  console.log(`err==>${err}`)
})

  这就可以代替了上面类似es5时代的jQurey的success的嵌套式的回调地狱的产生,让代码清爽了许多。这里的resolve就相当于以前的success。

新的ES6中引入了Promise,目的是让回调更为优雅。层层嵌套的回调会让javascript失去美感和可读性,同时javascript也推荐采用链式的方式去书写函数调用。于是Promise就应运而生。Promise即承诺的意思,new一个Promise就是新建一个承诺。在新建一个承诺的时候你需要做两件事情:
1. 指定承诺所需完成的事
2. 设置承诺是否实现的标准
下面我们来定义一个承诺,并让程序实现我们的这个承诺。
承诺的内容是:获取data.php的数据
承诺是否实现的评判是:是否获取data.php的数据
这里我们会用到jQuery.ajax()方法,这会让我们的代码显得简单精炼。下面是代码:

第一章 了解 Promise

all

  但从字面意思上理解,可能为一个状态全部怎么样的意思,让我看一下其用法,就可以看明白这个静态方法:

var   p1 = Promise.resolve(1),
      p2 = Promise.reject(2),
      p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then((res)=>{
    //then方法不会被执行
    console.log(results);
}).catch((err)=>{
    //catch方法将会被执行,输出结果为:2
    console.log(err);
});

  大概就是作为参数的几个promise对象一旦有一个的状态为rejected,则all的返回值就是rejected。

  当这几个作为参数的函数的返回状态为fulfilled时,至于输出的时间就要看谁跑的慢了:

let p1 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('1s') //1s后输出
    resolve(1)
  },1000)
})
let p10 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('10s') //10s后输出
    resolve(10)
  },10000)
})
let p5 = new Promise((resolve)=>{
  setTimeout(()=>{
    console.log('5s') //5s后输出
    resolve(5)
  },5000)
})
Promise.all([p1, p10, p5]).then((res)=>{
    console.log(res); // 最后输出
})

  这段代码运行时,根据看谁跑的慢的原则,则会在10s之后输出[1,10,5]。over,all收工。

有这样一个问题,如果在第一个then()中执行了onRejected(reason)回调函数,并且没有return任何值的时候,那么下一个then()中将会调用onFulfilled(value)方法,而不是onRejected(reason)。因为在执行上一个then()的onRejected(reason)回调函数的时候并没有出现错误或异常,所以PromiseStatus的状态就被更改为fulfilled了。为了避免这个问题,我们可以在onRejected(reson)中返回一个Promise对象并reject()。代码如下,我们要去访问一个并不存在的文件solve.php:

一、jQuery 中的 Defferred 对象

或许你在面试的时候,有面试官问你:

$.ajax() 执行后返回的结果是什么?

在 jQuery1.5 版本就已经引入了 Defferred 对象,当时为了引入这个东西,整个
jQuery 都被重构了。Defferred 跟 Promise
类似,它表示一个还未完成任务的对象,而 Promise
确切的说,是一个代表未知值的对象。

$.ajax({
    url: url
}).done(function(data, status, xhr){
    //…
}).fail(function(){
    //…
});

回忆下第二章第一节中的 Promise,是不是如出一辙,只是 jQuery
还提供了更多的语法糖:

$.ajax({
    url: url,
    success: function(data){
        //…
    },
    error: funtion(){
        //…
    }    
});

他允许将 done 和 fail 两个函数的回调放在 ajax 初始化的参数 success 和
fail 上,其原理还是一样的,同样,还有这样的东西:

$.when(taskOne, taskTwo).done(function () {
    console.log(“都执行完毕后才会输出我!”);
}).fail(function(){
    console.log(“只要有一个失败,就会输出我!”)
});

当 taskOne 和 taskTwo 都完成之后才执行 done 回调,这个浏览器内置的 Promise 也有对应的函数:

Promise.all([true, Promise.resolve(1), …]).then(function(value){
    //....
});

浏览器内置的 Promise 还提供了一个 API:

Promise.race([true, Promise.resolve(1), …]).then(function(value){
    //....
}, function(reason){
    //…
});

只要 race 参数中有一个 resolve 或者 reject,then 回调就会出发。

在http对象中只定义了一个get方法。在get方法中我们new了一个Promise的对象,而不是直接使用jQuery.ajax()方法去获取我们想要的数据。在这个get方法的最后我们返回的就是我们new的这个promise对象。对于这类有延迟的操作我们都可以使用Promise去处理,返回给主线程的是一个Promise的对象,而不是直接的结果,这是延迟程序对主线程的一个承诺。主线程可以通过返回的Promise的对象获取数据或者处理错误。这使得异步处理变得简单而优雅。
在jQuery.ajax()中我们使用了两个方法来判断是否实现了我们的承诺,它们分别是resove和reject方法。如果jQuery.ajax()执行了success,并且返回的data中有数据,那么我们就判定这个承诺已经实现了(fulfilled),则调用resolve方法。如果jQuery.ajax()执行了error,那么我们就判定这个承诺被拒绝了(rejected),则调用reject方法。
那resove和reject这两个方法分别执行了哪些操作呢?这个我们就需要从Promise这个构造函数本身的结构说起。Promise这个构造函数结构大概是下面这样的:

第四章 小结

then(onFulfilled,
onRejected):
这个方法实际上是把onFulfilled()函数和onRejected()函数添加到Promise对象的回调链中。回调链就像一个由函数组构成的队列,每一组函数都是由至少一个函数构成(onFulfilled()
或者 onRejected() 或者 onFulfilled() 和
onRejected())。当resolve()或者reject()方法执行的时候,回调链中的回调函数会根据PromiseStatus的状态情况而被依次调用。

二、无错误传递的 Promise(简化版的 Promise)

首先,我们需要用一个变量(status)来标记事务的状态,然后将事务(affair)也保存到
Promise 对象中。

var Promise = function(affair){
    this.state = “pending”;
    this.affair = affair || function(o) { return o; };
    this.allAffairs = [];
};

Promise 有两个重要的方法,一个是 then,另一个是 resolve:

  • then,将事务添加到事务队列(allAffairs)中
  • resolve,开启流程,让整个操作从第一个事务开始执行

在操作事务之前,我们会先把各种事务依次放入事务队列中,这里会用到 then
方法:

Promise.prototype.then = function (nextAffair){
    var promise = new Promise();
    if (this.state == ‘resloved’){
        // 如果当前状态是已完成,则这个事务将会被立即执行
        return this._fire(promise, nextAffair);
    }else{
        // 否则将会被加入队列中
        return this._push(promise, nextAffair);
    }
};

如果整个操作已经完成了,那 then 方法送进的事务会被立即执行,

Promise.prototype._fire = function (nextPromise, nextAffair){
    var nextResult = nextAffair(this.result);
    if (nextResult instanceof Promise){
        nextResult.then(function(obj){
            nextPromise.resolve(obj);
        });
    }else{
        nextPromise.resolve(nextResult);
    }
    return nextPromise;
};

被立即执行之后会返回一个结果,这个结果会被传递到下一个事务中作为原料,但是这里需要考虑两种情况:
  1. 异步,如果这个结果也是一个 Promise,则需要等待这个 Promise
    执行完毕再将最终的结果传到下一个事务中。
  2. 同步,如果这个结果不是 Promise,则直接将结果传递给下一个事务。

第一种情况还是比较常见的,比如我们在一个事务中有一个子事务队列需要处理,此时必须等待子事务完成才能回到主事务队列中。

Promise.prototype.resolve = function (obj){
    if (this.state != ‘pending’) {
        throw ‘流程已完成,不能再次开启流程!’;
    }
    this.state = ‘resloved’;
    // 执行该事务,并将执行结果寄存到 Promise 管理器上
    this.result = this.affair(obj);
    for (var i = 0, len = this.allAffairs.length; i < len; ++i){
        // 往后执行事务
        var affair = this.allAffairs[i];
        this._fire(affair.promise, affair.affair);
    }
    return this;
};

resolve
接受一个参数,这个数据是交给第一个事务来处理的,因为第一个事务的启动可能需要点原料,这个数据就是原料,它也可以是空。该事物处理完毕之后,将操作结果(result)寄存在
Promise
对象上,方便引用,然后将结果(result)作为原料送入下一个事务。依次类推。

我们看到 then 方法中还调用了一个 _push
,这个方法的作用是将事务推进事务管理器(Promise)。

Promise.prototype._push = function (nextPromise, nextAffair){
    this.allAffairs.push({
        promise: nextPromise,
        affair: nextAffair
    });
    return nextPromise;
};

以上操作,我们就实现了一个简单的事务管理器,可以测试下下面的代码:

// 初始化事务管理器
var promise = new Promise(function(data){
    console.log(data);
    return 1;
});
// 添加事务
promise.then(function(data){
    console.log(data);
    return 2;
}).then(function(data){
    console.log(data);
    return 3;
}).then(function(data){
    console.log(data);
    console.log(“end”);
});
// 启动事务
promise.resolve(“start”);

可以看到依次输出的结果为:

> start
> 1
> 2
> 3
> end

由于上述实现十分简陋,链式调用没做太好的处理,请读者自行完善。

下面是一个异步操作演示:

var promise = new Promise(function(data){
    console.log(data);
    return “end”;
});
promise.then(function(data){
    // 这里需要返回一个 Promise,让主事务切换到子事务处理
    return (function(data){
        // 创建一个子事务
        var promise = new Promise();
        setTimeout(function(){
            console.log(data);
            // 一秒之后才启动子事务,模拟异步延时
            promise.resolve();
        }, 1000);
        return promise;
    })(data);
});
promise.resolve(“start”);

可以看到依次输出的结果为:

> start
> end (1s之后输出)

将函数写的稍微好看点:

function delay(data){
    // 创建一个子事务
    var promise = new Promise();
    setTimeout(function(){
        console.log(data);
        // 一秒之后才启动子事务,模拟异步延时
        promise.resolve();
    }, 1000);
    return promise;
}
// 主事务
var promise = new Promise(function(data){
    console.log(data);
    return “end”;
});
promise.then(delay);
promise.resolve(“start”);

onFulfilled(value)和onRejected(reason):参数value和reason的实参都是PromiseValue。这里有一点值得注意,如果onFulfilled(value)和onRejected(reason)这两个回调函数中return返回值不是一个Promise的对象,那么这个返回值会被赋给PromiseValue,并在下一个then()的onFulfilled(value)和onRejected(reason)中做为实参使用。但如果这个返回值是一个Promise的对象,那么剩下的由then()构造的回调链会转交给新的Promise对象并完成调用。

Ajax 出现的时候,刮来了一阵异步之风,现在 Nodejs
火爆,又一阵异步狂风刮了过来。需求是越来越苛刻,用户对性能的要求也是越来越高,随之而来的是页面异步操作指数般增长,如果不能恰当的控制代码逻辑,我们就会陷入无穷的回调地狱中。

发表评论

电子邮件地址不会被公开。 必填项已用*标注