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

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

上一篇我们讲了同步链式处理数据函子的概念。这一节,我们来讲异步。用到的概念很简单,不需要有函数式编程的基础。当然如果你看了那篇 《在你身边你左右 --函数式编程别烦恼》 会更容易理解。这一篇我们会完成一个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);

复制代码
image.png
  • 函子的核心就是每个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)
复制代码

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

image.png
  • (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是如何执行的。

image.png
  • (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)
image.png

现在我们定义出这个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中的函数
image.png
  • 我们可以看到同样一个函子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);
复制代码

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

image.png

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

setTimeout(()=>{ d = a.then(add);} ,2000)
value:"1"
image.png
  • (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进行一下过滤

原文地址:https://juejin.im/post/5b41c159e51d4519277b6a39?utm_source=gold_browser_extension

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,053评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,527评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,779评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,685评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,699评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,609评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,989评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,654评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,890评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,634评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,716评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,394评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,976评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,950评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,191评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,849评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,458评论 2 342