确认过眼神,你就是我的Promise~~

2,524 阅读9分钟

小哥哥、小姐姐,你们好,请把手伸出来,我给你们点东西。

1、JavaScript异步编程

  • 同步与异步
  • 回调函数
  • promise
  • generator
  • async+await

2、写一个符合规范的promise库

1、JavaScript异步编程

1-1、同步与异步

我们都知道js是单线程语言,这就导致了会有同步异步的概念。所谓同步,就是指脚本解释器在解析代码时,从上往下一行一行解释,第一行解释不完,就不去解释第二行。所谓异步,就是指,当解释到某一行中,发现有异步方法(比如settimeout、ajax、DOM点击事件等等),解释器不会去等待异步方法执行完,再往下解释。而是,将异步任务放到任务队列,当所有的同步代码全部执行完,也就是主线程没有可执行的代码了,就回去任务队列拿出之前遇到的异步任务,执行,执行完毕后再去任务队列调取下一个任务,如此循环。

一图胜千言

同步
一次只能服务一个人,请排好队。谁事儿多也没办法,后面只能等着。
异步
三个人同时吃饭,这是异步。如果是同步的话,只能一个人吃完下一个人再吃。

1-2、回调函数

相信大家对回调函数已经不陌生了,函数A作为参数被传递到函数B里,那么函数A就是回调函数。有什么用呢?请看代码

let doSomething = () => { console.log('do something') }
setTimeout(doSomething, 500);
console.log('a');

声明了一个doSomething函数,并作为第一个参数传递给了setTimeout函数,setTimeout函数会在合适的时机执行它。达到了异步编程的目的。这种方式用处有很多,node.js有大部分api都是通过回调来实现异步编程的。

1-3、promise 写法

回调函数这种形式有一个缺点,那就是如果异步任务比较多的话,并且多任务执行有先后顺序,那么回调函数很容易就形成多层嵌套。如下:

function doA() { }
function doB() { }
function doC() { }
function doD() { }

doA(function () {
    doB(function () {
        doC(function () {
            doD(function () {

            })
        })
    })
})

当改用promise后,瞬间清爽了许多。

new Promise(function(resolve,reject){
    resolve();//在合适的时机出发resolve
})
.then(doA,null)
.then(doB,null)
.then(doC,null)
.then(doD,null)

这也是这篇文章的重点讲解内容,一会我会一步一步按照规范编写一个promise库。彻底搞懂promise。

1-4、generator 写法

function* gen() {
    let a = yield doA();
    let b = yield doB();
    let c = yield doC();
    let d = yield doD();
}
let it = gen();//it是一个迭代器
it.next();//{ value: undefined, done: false }
it.next();{ value: undefined, done: false }
it.next();{ value: undefined, done: false }
it.next();{ value: undefined, done: false }
it.next();{ value: undefined, done: true }

可以看到,generator函数有另外一个功能,那就是可以暂停,不像普通函数,只要一执行,那就会一口气执行完。

1-5、async + await 写法

async function doSomething() {
    await doA();
    await doB();
    await doC();
    await doD();
}
doSomething();

是不是发现,这种写法更加简洁,就像在写同步代码一样。

2、写一个符合规范的promise库

2-1、实现最简单的一个Promise类

先来看Promise的用法,然后根据用法一步步编写Promise

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

})
p1.then(function (data) {
    console.log(data)
}, function (err) {
    console.log(err)
})

在实例化一个Promise时,传入一个函数作为参数,该函数接受两个参数,分别为resolve,reject,然后按照Promise/A+规范一个Promise类应该包含如下状态

  • status
  • value
  • reason
  • onResolvedCallbacks
  • onRejectedCallbacks 我们很容易就写出了Promise的原型。代码如下:
function Promise(executor) {
    let self = this;
    self.status = 'pending';
    self.value = undefined;
    self.reason = undefined;
    self.onResolvedCallbacks = [];
    self.onRejectedCallbacks = [];
    function resolve() { }
    function reject() { }
    executor(resolve, reject);
}
Promise.prototype.then = function (onFulfilled, onRejected) {

}

接下来我们一个一个方法去攻破。

2-2、 实现resolvereject 方法

resolve方法需要完成的事情是:

  • 将Promise的状态置为resolved
  • 更改self.value的值
  • 通知self.onResolvedCallbacks,并一一执行。

reject方法需要完成的事情是

  • 将Promise的状态置为rejectd
  • 更改self.reason的值
  • 通知self.onRejectedCallbacks,并一一执行。 代码如下:
 function resolve(value) {
        self.status = 'resolved';
        self.value = value;
        self.onResolvedCallbacks.forEach(item => item(value))
    }
function reject(reason) {
        self.status = 'rejected';
        self.reason = reason;
        self.onRejectedCallbacks.forEach(item => item(reason))
    }

2-3、 实现then方法

then方法的作用是收集到成功、失败的回调函数,将他们分别添加到成功和失败的数组中。也就是代码中,我们需要将onFulfilled添加到self.onResolvedCallbacks里,将onRejected添加到self.onRejectedCallbacks里。

Promise.prototype.then = function (onFulfilled, onRejected) {
    this.onResolvedCallbacks.push(onFulfilled);
    this.onRejectedCallbacks.push(onRejected);
}

写到这里,这个Promise其实已经可以用了,不过还有一个潜在的问题。那就是,当resolve方法被同步调用时,通过then方法加入到队列的函数没有被执行。只有resolve被异步调用时才会被执行,为什么呢。因为这里的then是同步的,resolve也被同步调用的话,那肯定是,先执行resolve后执行then,换句话说就是,先执行回调,后添加回调,这不是我们想看到的,要达到先添加回调,后执行回调的效果,我们稍作修改。

function resolve(value) {
        setTimeout(() => {
            if (self.status === 'pending') {
                self.status = 'resolved';
                self.value = value;
                self.onResolvedCallbacks.forEach(item => item(value))
            }
        });
    }
function reject(reason) {
        setTimeout(function () {
            if (self.status == 'pending') {
                self.value = value;
                self.status = 'rejected';
                self.onRejectedCallbacks.forEach(item => item(value));
            }
        });
    }

这里加入了状态判断,因为当Promise的状态一旦确定,就不能更改,所以状态只能是pending时,resolvereject才生效。

2-4、实现链式调用

先看一下Promise/A+规范中对then方法的返回值描述。

规范第一句就表明,then方法必须返回一个promise,以实现链式调用。现在我们需要关注的是,调用then方法时,传入的第一个参数(onFulfilled)的返回值问题。如果是一个普通值,那我们就把它继续传递下去,传递给then方法返回的promise里;如果是一个新的promise的话,那就需要将新的promisethen方法返回的promise关联起来。 具体如何关联,咱们慢慢来,这里有点绕,我先上张图看图说话。

图中有部分代码只需要注意三个promisep1xp2,为了不造成混淆,这三个东西我一次标到了图的左部分,他们是对应的。请大家先明白一句话,然后我们开始说。

调用promise的resolve方法,会执行该promise的then函数的第一个参数

(这里我那resolve举例,reject道理一样,就不赘述了。)

看图,请看图。

调用p1的resolve方法,那么a就会被执行。

调用p2的resolve方法,那么c就会被执行。

也就是说,你想执行a或者b或者c或者d,那么你得找到它属于哪个promise,例如:图中,a b 属于p1,c d属于p2.这个关系必须要明确。

假设你已经理解了上面的话,现在我们面临的问题来了。

c d 本来是属于p2的,执行还是不执行也得看p2调不调用resolve、reject。 现在要让c d执不执行不看p2了,得看x。为什么要看x,因为x这个回调函数是用户传递的,用户的意思是:我让这个回调返回一个promise,然后继续使用then方法添加成功或失败的回调,而且这两个回调啥时候执行,得看我返回的那个promise。 反应到图中就是这个意思:c d何时执行,看x何时调用resolve、reject。

希望你理解了。...继续

破解方法:将p2的resolve、reject放入到x的then方法里。 解释一下:x的resolve、reject是暴露给用户的,也就是说,这两个方法的执行权在用户手里,当用户执行resolve时,其实就执行了x的then方法的第一个参数,而x的then方法的第一个参数正好是p2的resolve,p2的resolve就被执行了,p2的resolve一执行,那么c就被执行了。就实现了x的resolve、reject控制着c d的执行与否。

说了这么多,上代码吧还是,改造后的then方法如下,加入了状态判断错误捕获

Promise.prototype.then = function (onFulfilled, onRejected) {
    let promise2;
    let self = this;
    if (self.status === 'resolve') {
        promise2 = Promise(function (resolve, reject) {
            setTimeout(() => {
                try {
                    let x = onFulfilled(self.value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
        })
    }
    if (self.satus === 'rejected') {
        promise2 = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2, x, resolve, reject)
                } catch (e) {
                    reject(e);
                }
            })
        })
    }
    if (self.status === 'pending') {
        promise2 = new Promise(function (resolve, reject) {
            self.onResolvedCallbacks.push(function (value) {
                try {
                    let x = onFulfilled(value);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            });
            self.onRejectedCallbacks.push(function (reason) {
                try {
                    let x = onRejected(reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            })
        })
    }
    return promise2;
}

再把resolvePromise方法写一下,因为多个地方用到了,所以就单独封装了。

function resolvePromise(promise2, x, resolve, reject) {
  if (promise2 === x) {
    return reject(new TypeError('循环引用'));
  }
  let then, called;

  if (x != null && ((typeof x == 'object' || typeof x == 'function'))) {
    try {
      then = x.then;
      if (typeof then == 'function') {
        then.call(x, function (y) {
          if (called) return;
          called = true;
          resolvePromise(promise2, y, resolve, reject);
        }, function (r) {
          if (called) return;
          called = true;
          reject(r);
        });
      } else {
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    resolve(x);
  }
}

到这里,我们写的promise已经支持链式调用了。我希望阅读本文的你,先去读懂那张图,一定要看懂,知道自己在干什么,就是思路要清晰,然后再去写代码。我刚接触的时候,就是一步步去捋思路,然后辅助画图去理解。用了好几天才弄懂。

2-5、实现catchallracePromise.resolve()Promise.reject()

相比较then方法,这几个方法就轻松许多了。直接上代码了,我会把注释写到代码里边 catch

Promise.prototype.catch = function (onRejected) {
  return this.then(null, onRejected);//原来这么简单
}

all

Promise.all = function (promises) {
  return new Promise(function (resolve, reject) {
    let result = [];//结果集
    let count = 0;//计数器,用来记录promise有没有执行完
    for (let i = 0; i < promises.length; i++) {
      promises[i].then(function (data) {
        result[i] = data;
        if (++count == promises.length) {
          resolve(result);//计数器满足条件时,触发resolve
        }
      }, function (err) {
        reject(err);
      });
    }
  });
}

race

// 只要有一个promise成功了 就算成功。如果第一个失败了就失败了
Promise.race = function (promises) {
  return new Promise(function (resolve, reject) {
      for (var i = 0; i < promises.length; i++) {
          promises[i].then(resolve,reject)
      }
  })
}

Promise.resolve()Promise.reject()

// 生成一个成功的promise
Promise.resolve = function (value) {
  return new Promise(function (resolve, reject) {
    resolve(value);
  })
}
// 生成一个失败的promise
Promise.reject = function (reason) {
  return new Promise(function (resolve, reject) {
    reject(reason);
  })
}

2-6、Promise的语法糖

Promise.deferred = Promise.defer = function () {
  var defer = {};
  defer.promise = new Promise(function (resolve, reject) {
    defer.resolve = resolve;
    defer.reject = reject;
  })
  return defer;
}

看一个例子

let fs = require('fs');
let Promise = require('./promise');
function read() {
   // 好处就是解决嵌套问题
   // 坏处错误处理不方便了
    let defer = Promise.defer();
    fs.readFile('./2.promise.js/a.txt','utf8',(err,data)=>{
      if(err)defer.reject(err);
      defer.resolve(data)
    });
    return defer.promise;
}
read().then(data=>{
  console.log(data);
});

说了很多,希望你们理解了,如果文中有错误或者大家有不懂的地方,欢迎留言。