Build Your Own Promise

一、JavaScript异步编程背景

​ 从客岁ES2015宣布至今,已过去了一年多,ES2015宣布的新的言语特征中最为盛行的也就莫过于Promise了,Promise使得如今JavaScript异步编程云云轻松舒服,以至逐步遗忘了曾那不堪回首的痛苦。实在从JavaScript降生,JavaScript中的异步编程就已涌现,比方点击鼠标、敲击键盘这些事宜的处置惩罚函数都是异步的,时刻到了2009年,Node.js横空出世,在全部Node.js的完成中,将回调形式的异步编程机制发挥的极尽描摹,Node的盛行也是的越来越多的JavaScripter最先了异步编程,然则回调形式的副作用也逐步展如今人们面前,毛病处置惩罚不够文雅以及嵌套回调带来的“回调地狱”。这些副作用使得人们从回调形式的温柔乡中逐步清醒过来,最先寻觅越发文雅的异步编程形式,路漫漫其修远兮、吾将高低而求索。时刻到了2015年,Promise挽救那些苦苦探究的前驱。利用它汗青使命的时期好像已到来。

​ 每一个事物的降生有他的汗青使命,更有其汗青成因,增进其被那些探究的前驱们所发明。相识nodejs或许熟习阅读器的人都晓得,JavaScript引擎是基于事宜轮回或单线程这两个特征的。越发甚者在阅读器中,更新UI(也就是阅读重视绘、重拍页面规划)和实行JavaScript代码也在一个单线程中,可想而知,一个线程就相当于只要一条马路,假如一辆马车抛锚在路上了壅塞了马路,那末别的马车也就拥堵在了那儿,这个单线程轻易被壅塞是一个原理,单线程也只能许可某一时刻点只能够实行一段代码。同时,JavaScript没有想它的哥哥姐姐们那末财大气粗,像Java或许C++,一个线程不够,那末再加一个线程,如许就能够同时实行多段代码了,然则如许就会带来的隐患就是状况不轻易保护,JavaScript挑选了单线程非壅塞式的体式格局,也就是异步编程的体式格局,就像上面的马车抛锚在了路上,那末把马车推到路边的维修站,让其他马车先过去,等马车修好了再回到马路上继承行驶,这就是单线程非壅塞体式格局。正如Promise的工作体式格局一样,经由过程Promise去处服务器提议一个要求,毕竟要求有收集开支,不能够立时就返回要求效果的,这个时刻Promise就处于pending状况,然则其并不会壅塞其他代码的实行,当要求返回时,修正Promise状况为fulfilled或许rejected(失利要求)。同时实行绑定到这两个状况上面的“处置惩罚函数”。这就是异步编程的形式,也就是Promise谨小慎微的工作体式格局,在下面一个部份将细致议论Promise。

二、Promise基础

​ 怎样一句话诠释Promise呢?Promise能够代指那些还没有完成的一些操纵,然则其在未来的某个时刻会返回某一特定的效果。

​ 当建立一个Promise实例后,其代表一个未知的值,在未来的某个时刻会返回一个胜利的返回值,或许失利的返回值,我们能够为这些返回值增添处置惩罚函数,当值返回时,处置惩罚函数被挪用。Promise老是处于下面三种状况之一:

  • pending: Promise的初始状况,也就是未被fulfilled或许rejected的状况。

  • fulfilled: 意味着promise代指的操纵已胜利完成。

  • rejected:意味着promise代指的操纵由于某些缘由失利。

一个处于pending状况的promise能够由于某个胜利返回值而发展为fulfilled状况,也有能够由于某些毛病而进入rejected状况,无论是进入fulfilled状况或许rejected状况,绑定到这两种状况上面的处置惩罚函数就会被实行。而且进入fulfilled或许rejected状况也就不能再返回pending状况了。

《Build Your Own Promise》

三、边学边写

上面说了那末多,实在都是铺垫。接下来我们就最先完成自身的Promise对象。go go go!!!

第一步:Promise组织函数

Promise有三种状况,pending、fulfilled、rejected。

const PENDING = 'PENDING' // Promise 的 初始状况
const FULFILLED = 'FULFILLED' // Promise 胜利返回后的状况
const REJECTED = 'REJECTED' // Promise 失利后的状况

有了三种状况后,那末我们怎样建立一个Promise实例呢?

const promise = new Promise(executor) // 建立Promise的语法

经由过程上面天生promise语法我们晓得,Promise实例是挪用Promise组织函数经由过程new操纵符天生的。这个组织函数我们能够先如许写:

class Promise {
    constructor(executor) {
        this.status = PENDING // 建立一个promise时,起首举行状况初始化。pending
        this.result = undefined // result属性用来缓存promise的返回效果,能够是胜利的返回效果,或失利的返回效果
    }
}

我们能够看到上面组织函数接收的参数executor。它是一个函数,而且接收其他两个函数(resolve和reject)作为参数,当resolve函数挪用后,promise的状况转化为fulfilled,而且实行胜利返回的处置惩罚函数(不必焦急背面会说到怎样增添处置惩罚函数)。当reject函数挪用后,promise状况转化为rejected,而且实行失利返回的处置惩罚函数。

如今我们的代码大概是如许的:

class Promise {
    constructor(executor) {
        this.status = PENDING 
        this.result = undefined
        executor(data => resolveProvider(this, data), err => rejectProvider(this, err))
    }
}

function resolveProvider(promise, data) {
    if (promise.status !== PENDING) return false
    promise.status = FULFILLED
}
function rejectProvider(promise, data) {
    if (promise.status !== PENDING) return false
    promise.status = FULFILLED
}

Dont Repeat Yourselt!!!我们能够看到上面代码背面两个函数基础雷同,实在我们能够把它整合成一个函数,在连系高阶函数的运用。

const statusProvider = (promise, status) => data => {
    if (promise.status !== PENDING) return false
    promise.status = status
    promise.result = data
}
class Promise {
    constructor(executor) {
        this.status = PENDING 
        this.result = undefined
        executor(statusProvider(this, FULFILLED), statusProvider(this, REJECTED))
    }
}

如今我们的代码就看上去简约多了。

第二步:为Promise增添处置惩罚函数

《Build Your Own Promise》

实在经由过程 new Promise(executor)已能够天生一个Promise实例了,以至我们能够经由过程通报到executor中的resolve和reject要领来转变promise状况,然则!如今的promise依旧没啥卵用!!!由于我们并没有给它增添胜利和失利返回的处置惩罚函数。

起首我们须要给我们的promise增添两个属性,successListener和failureListener用来离别缓存胜利处置惩罚函数和失利处置惩罚函数。

class Promise {
    constructor(executor) {
        this.status = PENDING
         this.successListener = []
         this.failureListener = []
        this.result = undefined
        executor(statusProvider(this, FULFILLED), statusProvider(this, REJECTED))
    }
}

怎样增添处置惩罚函数呢?ECMASCRIPT规范中说到,我们能够经由过程promise原型上面的then要领为promise增添胜利处置惩罚函数和失利处置惩罚函数,能够经由过程catch要领为promise增添失利处置惩罚函数。

const statusProvider = (promise, status) => data => {
    if (promise.status !== PENDING) return false
    promise.status = status
    promise.result = data
    switch(status) {
        case FULFILLED: return promise.successListener.forEach(fn => fn(data))
        case REJECTED: return promise.failurelistener.forEach(fn => fn(data))
    }
}
class Promise {
    constructor(executor) {
        this.status = PENDING
        this.successListener = []
        this.failurelistener = []
        this.result = undefined
        executor(statusProvider(this, FULFILLED), statusProvider(this, REJECTED))
    }
    /**
     * Promise原型上面的要领
     */
    then(...args) {
        switch (this.status) {
            case PENDING: {
                this.successListener.push(args[0])
                this.failurelistener.push(args[1])
                break
            }
            case FULFILLED: {
                args[0](this.result)
                break
            }
            case REJECTED: {
                args[1](this.result)
            }
        }
    }
    catch(arg) {
        return this.then(undefined, arg)
    }
}

我们如今的Promise基础初具雏形了。以至能够运用到一些简朴的场景中了。举个例子。

/*建立一个延时resolve的pormise*/
new Promise((resolve, reject) => {setTimeout(() => resolve(5), 2000)}).then(data => console.log(data)) // 5
/*建立一个实时resolve的promise*/
new Promise((resolve, reject) => resolve(5)).then(data => console.log(data)) // 5
/*链式挪用then要领还不能够运用!*/
new Promise(resolve=> resolve(5)).then(data => data).then(data => console.log(data))
// Uncaught TypeError: Cannot read property 'then' of undefined
第三步:Promise的链式挪用

Promise须要完成链式挪用,我们须要再次回忆下then要领的定义:

then要领为pormise增添胜利和失利的处置惩罚函数,同时then要领返回一个新的promise对象,这个新的promise对象resolve处置惩罚函数的返回值,或许当没有供应处置惩罚函数时直接resolve原始的值。

能够看出,promise能够链式挪用归功于then要领返回一个全新的promise,而且resolve处置惩罚函数的返回值,固然,假如then要领的处置惩罚函数自身就返回一个promise,那末久不必我们自身手动天生一个promise了。相识了这些,就最先动手写代码了。

const isPromise = object => object && object.then && typeof object.then === 'function'
const noop = () => {}

const statusProvider = (promise, status) => data => {
    // 同上面代码
}

class Promise {
    constructor(executor) {
        // 同上面代码
    }
    then(...args) {
        const child = new this.constructor(noop)

        const handler = fn => data => {
            if (typeof fn === 'function') {
                const result = fn(data)
                if (isPromise(result)) {
                    Object.assign(child, result)
                } else {
                    statusProvider(child, FULFILLED)(result)
                }    
            } else if(!fn) {
                statusProvider(child, this.status)(data)
            }
        }
        switch (this.status) {
            case PENDING: {
                this.successListener.push(handler(args[0]))
                this.failurelistener.push(handler(args[1]))
                break
            }
            case FULFILLED: {
                handler(args[0])(this.result)
                break
            }
            case REJECTED: {
                handler(args[1])(this.result)
                break
            }
        }
        return child
    }
    catch(arg) {
        return this.then(undefined, arg)
    }
}

​ 起首我们写了一个isPromise要领,用于推断一个对象是不是是promise。就是推断对象是不是有一个then要领,免责声明为了完成上的简朴,我们不辨别thenable和promise的区分,然则我们应该是晓得。一切的promise都是thenable的,而并非一切的thenable对象都是promise。(thenable对象是指带有一个then要领的对象,该then要领实在就是一个executor。)isPromise的作用就是用于推断then要领返回值是不是是一个promise,假如是promise,就直接返回该promise,假如不是,就新天生一个promise并返回该promise。

​ 由于须要链式挪用,我们对successListener和failureListener中处置惩罚函数举行了重写,并非直接push进去then要领接收的参数函数了,由于then要领须要返回一个promise,所以当then要领内里的处置惩罚函数被实行的同时,我们也须要对then要领返回的这个promise举行处置惩罚,要么resolve,要么reject掉。固然,大部份状况都是须要resolve掉的,只要当then要领没有增添第二个参数函数,同时挪用then要领的promise就是rejected的时刻,才须要把then要领返回的pormise举行reject处置惩罚,也就是挪用statusProvider(child, REJECTED)(data).

toy Promise完成的完全代码:

const PENDING = 'PENDING' // Promise 的 初始状况
const FULFILLED = 'FULFILLED' // Promise 胜利返回后的状况
const REJECTED = 'REJECTED' // Promise 失利后的状况

const isPromise = object => object && object.then && typeof object.then === 'function'
const noop = () => {}

const statusProvider = (promise, status) => data => {
    if (promise.status !== PENDING) return false
    promise.status = status
    promise.result = data
    switch(status) {
        case FULFILLED: return promise.successListener.forEach(fn => fn(data))
        case REJECTED: return promise.failurelistener.forEach(fn => fn(data))
    }
}

class Promise {
    constructor(executor) {
        this.status = PENDING
        this.successListener = []
        this.failurelistener = []
        this.result = undefined 
        executor(statusProvider(this, FULFILLED), statusProvider(this, REJECTED))
    }
    /**
     * Promise原型上面的要领
     */
    then(...args) {
        const child = new this.constructor(noop)

        const handler = fn => data => {
            if (typeof fn === 'function') {
                const result = fn(data)
                if (isPromise(result)) {
                    Object.assign(child, result)
                } else {
                    statusProvider(child, FULFILLED)(result)
                }    
            } else if(!fn) {
                statusProvider(child, this.status)(data)
            }
        }
        switch (this.status) {
            case PENDING: {
                this.successListener.push(handler(args[0]))
                this.failurelistener.push(handler(args[1]))
                break
            }
            case FULFILLED: {
                handler(args[0])(this.result)
                break
            }
            case REJECTED: {
                handler(args[1])(this.result)
                break
            }
        }
        return child
    }
    catch(arg) {
        return this.then(undefined, arg)
    }
}
四、怎样让我们的toy Promise变强健
  1. 在ECMAScript规范中,Promise组织函数上面还供应了一些静态要领,比方Promise.resolvePromise.rejectPromsie.allPromise.race。当我们有了上面的基础完成后,为我们的toy Promise增添上面这些新的功用肯定能让其越发有用。

  2. 在我们的基础完成中,我们并没有辨别thenable对象,实在Promise.resolvethen要领都能够接收一个thenable对象,并把该thenable对象转化为一个promise对象,假如想让我们的toy Promise用于临盆的话,这也是要斟酌的。

  3. 为了让我们的toy Promise变得更强健,我们须要具有强健的毛病处置惩罚机制,比方考证executor必需是一个函数、then要领的参数只能是函数或许undefined或null,又比方executor和then要领中抛出的毛病并不能够被window.onerror监测到,而只能够经由过程毛病处置惩罚函数来处置惩罚,这也是须要斟酌的要素。

  4. 假如我们的Promise polyfill是斟酌支撑多平台,那末主要斟酌的就是阅读器环境或Node.js环境,实在在这两个平台,原生Promise都是支撑两个事宜的。就拿阅读器端举例:

    • unhandledrejection: 在一个事宜轮回中,假如我们没有对promise返回的毛病举行处置惩罚,那末就会在window对象上面触发该事宜。

    • rejectionhandled:假如在一个事宜轮回后,我们才去对promise返回的毛病举行处置惩罚,那末就会在window对象上面监听到此事宜。

关于这两个事宜以及node.js平台上面相似的事宜请参考Nicholas C. Zakas新书<understanding es6>

Promise能够很棒的处置惩罚异步编程,要想学好它我以为最好的要领就是亲自动手去完成一个自身的Promise,下面的项目Jocs/promise是我的完成,迎接人人pr和star。

    原文作者:Jocs
    原文地址: https://segmentfault.com/a/1190000006974658
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞