函数式编程之Promise的奇幻漂流

3,331 阅读8分钟

上一篇我们讲了同步链式处理数据函子的概念。这一节,我们来讲异步。用到的概念很简单,不需要有函数式编程的基础。当然如果你看了那篇 《在你身边你左右 --函数式编程别烦恼》 会更容易理解。这一篇我们会完成一个Promise代码的编写。本文会从实现一个只有十几行代码能够解决异步链式调用问题的简单的Promise开始。然后逐渐完善增加功能。

  • 实现简单的异步Promise函子
  • 能够同时调用同一Promise函子
  • 增加reject回调函数
  • 增加Promise状态

本文代码在我的github

1 实现简单的Promise函子

我们先来回顾一下同步链式调用。

class Functor{
       constructor (value) {
          this.value = value ;
       }      
       map (fn) {
         return Functor.of(fn(this.value))
       }
    }
Functor.of = function (val) {
     return new Functor(val);
}

Functor.of(100).map(add1).map(add1).map(minus10)

// var  a = Functor.of(100);
// var  b = a.map(add1);
// var  c = b.map(add1);
// var  d = c.map(minus10);

  • 函子的核心就是每个functor都是一个新的对象
  • 通过map中传递进去的函数fn去处理数据
  • 用得到的值去生成新的函子

那么如果当a的值是异步产生的,我们该何如传入this.value值呢?

function executor(resolve){
  setTimeout(()=>{resolve(100)},500)
}

我们模拟一下通过setTimeout500毫秒后拿到数据100。其实也很简单,我们可以传进去一个resolve回调函数去处理这个数据。

class Functor {
   constructor (executor) {
      let _this = this;
      this.value = undefined;

      function resolve(value){
          _this.value = value;
      }
      executor(resolve)
   } 
}

var a = new Functor(executor);

  • 我们讲executor传入并立即执行
  • 在resolve回调函数中我们能够拿到value值
  • 我们定义resolve回调函数讲value的值赋给this.value

这样我们就轻松的完成了a这个对象的赋值。那么我们怎么用方法去处理这个数据呢?

  • 显然在拿到回调函数值之后,我们应该能让map里的fn去继续处理数据
  • 处理完这个数据,我们交给下一个函数的resolve去继续处理
  • 所以我们定义了一个callback函数,
  • 在调用map时,将就包含fn处理数据,和执行下一个对象的resolve的函数赋值给它
  • 然后在自己的resolve拿到值之后,我们执行这个callback
class Functor {
   constructor (executor) {
      let _this = this;
      this.value = undefined;
      this.callback = null;
      function resolve(value){
          _this.value = value;
          _this.callback()
      }
      executor(resolve)
   } 
  
   map (fn) {
       let  self = this;
       return new Functor((resolve) => {
          self.callback = function(){
              let data =  fn(self.value)   
              resolve(data)
           }
       })
   }    
}
new Functor(executor).map(add1).map(add1)

现在我们已经实现了异步的链式调用,我们来具体分析一下,都发生了什么。

  • (1)a = new Functor(executor)的时候,我们进行了初始化, executor(resolve)开始执行
  • (2)b =a.map(add1)的时,先进行了初始化 new Functor(),然后执行 executor(resolve)
  • (3)b中executor(resolve)执行结束,将一个函数赋值a中的callback

注意:这时map中this指向的是a函子,但是 new Functor((resolve) => {}中resolve是B的

  • (4)最后return 一个新的函子b
  • (5)c =b.map(add1)的时,同样,给b中的callback赋值
  • (6)然后返回一个新的函子c,此时没有map的调用,c中的callback就是null

我们再来分析一下异步结束之后,回调函数中的resolve是如何执行的。

  • (1)resolve 先_this.value = value;把a中的value进行修改
  • (2)在执行_this.callback(),先let data = fn(self.value) 计算出处理后的data
  • (3)调用b中的resolve函数继续处理
  • (4)b中也是,先给value赋值,然后处理数据
  • (5)再调用c中的resolve,并把处理好的数据传给他
  • (6)先给C中value赋值,然后再处理数据,最后调用callback时因为不是函数会报错,之后我们会解决

本节代码:promise1.js

嗯,这就是promise作为函子实现的处理异步操作的基本原理。它已经能够解决了简单的异步调用问题。虽然代码不多,但这是promise处理异步调用的核心。接下来我们会不断继续实现其他功能。

2 同时调用同一个Promise函子

如果我们像下面同时调用a这个函子。你会发现,它实际上只执行了c。

var a = new Functor(executor);
var b = a.map(add);
var c = a.map(minus);

原因很简单,因为上面我们学过,b先给a的callback赋值,然后c又给a的callback赋值。所以把b给覆盖掉了就不会执行啦。解决这个问题很简单,我们只需要让callback变成一个数组就解决啦。

class MyPromise {
   constructor (executor) {
      let _this = this;
      this.value = undefined;
      this.callbacks = [];
      function resolve(value){
          _this.value = value;
          _this.callbacks.forEach(item => item())
      }
      executor(resolve)
   } 
  
   then (fn) {
       return new MyPromise((resolve) => {
          this.callbacks.push (()=>{
              let data =  fn(this.value) 
              console.log(data)         
              resolve(data)
           })
       })
   }    
}

var a = new MyPromise(executor);
var b = a.then(add).then(minus);
var c = a.then(minus);

  • 我们定义了callbacks数组,每次的调用a的then方法时。都将其存到callbacks数组中。
  • 当回调函数拿到值时,在resolve中遍历执行每个函数。
  • 如果callbacks是空,forEach就不会执行,这也解决了之前把错的问题
  • 然后我们进一步改了函子的名字(MyPromise),将map改成then
  • 简化了return中,let self = this;

3 增加reject回调函数

我们都知道,在异步调用的时候,我们往往不能拿到数据,返回一个错误的信息。这一小节,我们对错误进行处理。

function executor(resolve,reject){
  fs.readFile('./data.txt',(err, data)=>{
    if(err){ 
       console.log(err)
       reject(err)
    }else {
       resolve(data)
    }
  })
}
  • 我们现在用node异步读取一个文件
  • 成功执行 resolve(data),失败执行 reject(err)

现在我们定义出这个reject

class MyPromise {
  constructor (executor) {
    let _this = this;
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    function resolve(value){
      _this.value = value;
      _this.onResolvedCallbacks.forEach(item => item())
    }
    function reject(reason){
      _this.reason = reason;
      _this.onRejectedCallbacks.forEach(item => item());
    }
    executor(resolve, reject);
  } 
  then (fn,fn2) {
    return new MyPromise((resolve,reject) => {
      this.onResolvedCallbacks.push (()=>{
        let data =  fn(this.value) 
        console.log(data)         
        resolve(data)
      })
      this.onRejectedCallbacks.push (()=>{
        let reason =  fn2(this.reason) 
        console.log(reason)         
        reject(reason)
      })
    })
  }    
}
  • 其实很简单,就是我们就是在executor多传递进去一个reject
  • 根据异步执行的结果去判断执行resolve,还是reject
  • 然后我们在MyPromise为reject定义出和resolve同样的方法
  • 然后我们在then的时候应该传进去两个参数,fn,fn2

本节代码:promise3.js

这时候将executor函数封装到asyncReadFile异步读取文件的函数

function asyncReadFile(url){
  return new MyPromise((resolve,reject) => {
    fs.readFile(url, (err, data) => {
      if(err){ 
         console.log(err)
         reject(err)
      }else {
         resolve(data)
      }
    })
  })
}
var a = asyncReadFile('./data.txt');
a.then(add,mismanage).then(minus,mismanage);

这就是我们平时封装异步Promise函数的过程。但这是过程有没有觉得在哪见过。如果之前executor中的'./data.txt'我们是通过参数传进去的那么这个过程不就是上一节我们提到的柯里化。

本节代码:promise4.js

我们再来总结一下上面的过程。

  • 我们先进行了初始化,去执行传进来的 executor函数,并把处理的函数push进入callback数组中
  • 在reslove或reject执行时,我们去执行callback中的函数

  • 我们可以看到同样一个函子a在不同时期有着不一样的状态。
  • 显然如果在reslove()或者 reject( )之后我们再添加then()方法是不会有作用的

那么我们如何解决reslove之后a函子的then调用问题呢,其实reslove之后,我们已经有了value值,那不就是我们最开始讲的普通函子的链式调用吗?所以现在我们只需要标记出,函子此时的状态,再决定如何调用then就好啦

4 增加Promise状态

  • 我们定义进行中的状态为pending
  • 已成功执行后为fulfilled
  • 失败为rejected
class MyPromise {
  constructor (executor) {
    let _this = this;
    this.status = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onResolvedCallbacks = [];
    this.onRejectedCallbacks = [];
    function resolve(value){
      if (_this.status === 'pending') {
        _this.status = 'fulfilled';
        _this.value = value;
        _this.onResolvedCallbacks.forEach(item => item())
      }
    }
    function reject(reason){
      if (_this.status === 'pending') {
        _this.status = 'rejected';  
        _this.reason = reason;
        _this.onRejectedCallbacks.forEach(item => item());
      }
    }
    executor(resolve, reject);
  } 
  then (fn,fn2) {
     return new MyPromise((resolve,reject) => {
      if(this.status === 'pending'){
        this.onResolvedCallbacks.push (()=>{
          let data =  fn(this.value) 
          console.log(data)         
          resolve(data)
        })
        this.onRejectedCallbacks.push (()=>{
          let reason =  fn2(this.reason) 
          console.log(reason)         
          reject(reason)
        })
      }
      if(this.status === 'fulfilled'){
          let x = fn(this.value)
          resolve(x)
      }
      if(this.status === 'rejected'){
          let x = fn2(this.value)
          reject(x)
      }
    })
  }    
}

var a = asyncReadFile('./data.txt');
a.then(add,mismanage).then(add,mismanage).then(add,mismanage);

我们分析一下上面这个过程

其实就多了一个参数,然后判断了一下,很简单。那么我们现在来分析一下,当我们调用fulfilled状态下的a的执行过程

setTimeout(()=>{ d = a.then(add);} ,2000)
value:"1"

  • (1)先执行new MyPromise(),初始化d
  • (2)然后执行 executor(resolve, reject);fn开始执行,算出新的值x
  • (3)传给d的resolve执行,
  • (4)修改stauts和value的状态
  • (5)return 出新的函子,可以继续链式调用

我们来想一个问题,如果(2)中fn是一个异步操作,d后边继续调用then方法,此刻pending状态就不会改变,直到resolve执行。那么then的方法就会加到callback上。就又回到我们之前处理异步的状态啦。所以这就是为什么Promise能够解决回调地狱

参考代码:promise5.js

好了,我们现在来看传进去的方法fn(this.value) ,我们需要用上篇讲的Maybe函子去过滤一下。

5 Maybe函子优化

 then (onResolved,onRejected) {
     
     onResolved = typeof onResolved === 'function' ? onResolved : function(value) {}
     onRejected = typeof onRejected === 'function' ? onRejected : function(reason) {}

     return new MyPromise((resolve,reject) => {
      if(this.status === 'pending'){
        this.onResolvedCallbacks.push (()=>{
          let x =  onResolved(this.value) 
          resolve(x)
        })
        this.onRejectedCallbacks.push (()=>{
          let x =  onRejected(this.reason)
          reject(x)
        })
      }
      if(this.status === 'fulfilled'){
          let x = onResolved(this.value)
          resolve(x)
      }
      if(this.status === 'rejected'){
          let x = onRejected(this.value)
          reject(x)
      }
    })
  }    
  • Maybe函子很简单,对onResolved和onRejected进行一下过滤

参考代码:promise6.js

这一篇先写到这里吧。最后总结一下,Promise的功能很强大,就是少年派的奇幻漂流一样。虽然旅程绚烂多彩,但始终陪伴你的只有那只老虎。Promise也是一样,只要掌握其核心函子的概念,其他问题就比较好理解啦。这里只实现了一个简单的Promise,更强大的功能,我们慢慢加吧。