从函数式编程的角度聊聊Promise的设计

425 阅读4分钟

接触过react的小伙伴们,相信大家对于函数式编程绝对不陌生,函数式编程的纯粹,无副作用,让人着迷。在我们工作中也会大量使用Promise,其实在Promise设计时也借鉴了函数式编程中的思想:Monad。暂时不牵扯的太深,先让我们先从最基本的概念:纯函数,函子(functor)说起吧!

1. 纯函数

纯函数对于相同的输入,永远会得到相同的输出,而且没有任何可观察的副作用,也不依赖外部环境的状态。见例子:

var arr = [1, 2, 3, 4, 5, 6]

// slice就是纯函数,没有副作用,固定输入,固定输出
arr.slice(0, 3) // [1, 2, 3]
//  相等
arr.slice(0, 3)

// splice就是不纯的,他有副作用,固定的输入,没有固定的输出
arr.splice(0, 3) // [1, 2, 3]
//  不等
arr.splice(0, 3) // [4, 5, 6]

2. 函子 functor

1)函子

函子是实现了map方法并遵守一些特殊规定的容器类型,本质是还是方法。我们先看一个简单的functor:

function functor(value) {
  this._value = value
}

functor.prototype.map = function(fn) {
  return new functor(fn(this._value))
}

console.log(new functor(2).map(x => x + 3)) // functor {_value: 5}

经常new functor不是很方便,而且看起来也不很函数式,所以,我们将代码改造成这样:

function functor(value) {
  this._value = value
}

functor.of = function(value) {
  return new functor(value)
}

functor.prototype.map = function(fn) {
  return functor.of(fn(this._value))
}

console.log(functor.of(2).map(x => x + 3)) // functor {_value: 5}

functor是对函数调用的抽象,我们赋予容器去调用函数的能力,通过map一个方法,容器可以自由的决定何时去操作这个函数,以致于拥有错误处理,异步处理等能力。

2)常见函子

(1)Maybe函子

可以检查value是否为null或者undefined的函子。

function Maybe(value) {
  this._value = value
}

Maybe.of = function(value) {
  return new Maybe(value)
}

Maybe.prototype.isNothing = function() {
  return this._value === null || this._value === undefined
}

Maybe.prototype.map = function(fn) {
  return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this._value))
}

// 读取age,操作age
Maybe.of({ name: '张三', age: 12 })
	.map(user => user.age)
	.map(age => age * 10) // Maybe {_value: 120}

// 读取age,此时age不存在
Maybe.of({ name: '张三' })
	.map(user => user.age)
	.map(age => age * 10) // Maybe {_value: null}

(2)Either函子

我的的容器无法做到对错误的异常捕获处理,而promise中是可以调用catch集中处理异常的。所以我们也需要这种函子。如果操作正确,就返回正确的结果,如果操作失败,则返回错误描述。Left和Right就是Either函子的子类。

// 异常处理
function Left(value) {
  this._value = value
}

Left.of = function(value) {
  return new Left(value)
}

// 注意这里,什么都不处理,只展示第一次of传入的值
Left.prototype.map = function(fn) {
  return this
}

// 正常流程
function Right(value) {
  this._value = value
}

Right.of = function(value) {
  return new Right(value)
}

Right.prototype.map = function(fn) {
  return Right.of(fn(this._value))
}

function parseJSON(str) {
  try {
    return Right.of(JSON.parse(str))
  } catch(e) {
    return Right.of(e)
  }
}

// 成功结果
parseJSON('{"name": "zs"}')
	.map(user => user.name) // Right {_value: "zs"}

// 异常结果
parseJSON('{name: "zs"}')
	.map(user => user.name) // Left {_value: 'SyntaxError'}

(3)IO函子

在我们工作中会遇到各种副作用的场景,或者依赖外部环境的,比如dom操作,接口请求,定时器等。IO函子和前面的函子不同的是,他传入的value是一个函数,他把不纯的操作包裹到一个函数中,延迟这个操作,所以IO函子入参就是包裹副作用的函数。

function IO(value) {
  this._value = value
}

IO.of = function(value) {
  return new IO(value)
}

IO.prototype.map = function(fn) {
  return IO.of(fn(this._value()))
}

// 获取页面title
IO.of(_ => window.document)
  .map(d => d.title) // IO {_value: '你的页面标签名称'}

(4)Monad函子

在实际应用中,我们可能有这样一种场景:

function Monad(value) {
  this._value = value
}

Monad.of = function(value) {
  return new Monad(value)
}

Monad.prototype.map = function(fn) {
  return Monad.of(fn(this._value))
}

Monad.of(Monad.of(3)).map(x => x + 2) // Monad {_value: "[object Object]2"}

这个时候发现,我们无法操作容器内的值了。所以我们增加了join方法

Monad.prototype.join = function() {
  return this._value ? this._value : Monad.of(null)
}

Monad.of(Monad.of(3)).join().map(x => x + 2) // Monad {_value: 5}

Monad可以将一个运算过程,通过函数拆解成互相连接的多个步骤。你只要提供下一步运算需要的函数,整个运算就可以自动进行下去。

3. Promise

Promise是解决异步编程的一种方案,替代多层嵌套的回调,Promise是个构造方法,用于封装异步操作,并获得成功或失败的结果。Promise一旦创建立即执行。

每个Promise有3种状态,pending:进行中,fullfilled:成功,rejected:失败。状态只能从pending到fullfilled,或者pending到rejected,状态不可逆。

同上面的函子的概念来讲,Promise本质上也是一个容器,提供了then方法让容器去调用,从而让函数调用自动执行。而且将包裹了副作用,不直接调用副作用,实现了纯函数的效果。

介绍Promise的文章很多,这里不再详细赘述,给大家手撸一个简易版的Promise吧。

const PENDING = 'pending'
const FULLFILLED = 'fulfilled'
const REJECT = 'reject'

function IPromise(executor) {
  this.state = PENDING
  this.value = null
  this.reason = null
  this.onFullFilledQueue = []
  this.onRejectedQueue = []

  const resolve = value => {
    if (this.state !== PENDING) {
      return
    }
    this.state = FULLFILLED
    this.value = value

    if (this.onFullFilledQueue.length === 0) {
      return
    }

    this.onFullFilledQueue.forEach(onFullFilled => {
      this.value = onFullFilled(this.value)
    })
  }

  const reject = value => {
    if (this.state !== PENDING) {
      return
    }
    this.state = REJECT
    this.reason = value

    if (this.onRejectedQueue.length === 0) {
      return
    }

    this.onRejectedQueue.forEach(onRejected => {
      this.reason = onRejected(this.reason)
    })
  }

  try {
    executor(resolve, reject)
  } catch (e) {
    reject(e)
  }
}

IPromise.prototype.then = function(onFullFilled, onRejected) {
  onFullFilled = typeof onFullFilled === 'function' ? onFullFilled : value => value
  onRejected = typeof onRejected === 'function' ? onRejected : value => value

  return new IPromise((resolve, reject) => {
    switch(this.state) {
      case PENDING:
        this.onFullFilledQueue.push(() => {
          try {
            resolve(onFullFilled(this.value))
          } catch (error) {
            reject(error)
          }
        })
        this.onRejectedQueue.push(() => {
          try {
            reject(onRejected(this.reason))
          } catch (error) {
            reject(error)
          }
        })
        break
      case FULLFILLED:
        try {
          resolve(onFullFilled(this.value))
        } catch (error) {
          reject(error)
        }
        break
      case REJECT:
        try {
          reject(onRejected(this.reason))
        } catch (error) {
          reject(error)
        }
        break
    }
  })
}

IPromise.prototype.catch = function(onRejected) {
  try {
    this.then(null, onRejected)
  } catch (error) {}
}

简易版的IPromise实现了状态变更,链式调用,catch捕获。

happy hack!