promise 介绍和实现

一、promise是什么?有什么特点?

Promise 是异步编程的一种解决方案,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。同时从代码上讲Promise是一个构造函数,自己身上有all、reject、resolve这几个眼熟的方法,原型上有then、catch等方法

promise.png

特点:

  • 对象的状态不受外界影响。Promise对象代表一个异步操作,有三种状态:pending(进行中)、fulfilled(已成功)和rejected(已失败)。只有异步操作的结果,可以决定当前是哪一种状态,任何其他操作都无法改变这个状态。
  • 一旦状态改变,就不会再变,任何时候都可以得到这个结果。Promise对象的状态改变,只有两种可能:从pending变为fulfilled和从pending变为rejected。只要这两个可能发生一个则不能再去改变。

二、常见异步编程的方案

在js的异步那篇文章中有提到很多的方式可以查看https://www.jianshu.com/p/1fbb9789cde3这里重点讲一下callback,promise的起源。下面我们用callback实现一个读取文件a和b之后打印出他们的内容的功能。(备注:下面的代码可以直接在vscode中安装 Code Runner 插件来run)

const fs = require('fs')
fs.readFile('a.txt', 'utf8', funcrion (err, data) {
    fs.readFile('b.txt', 'utf8', funcrion (err, data) {
        console.log(data)
    })
})

问题:

  1. 嵌套(回调地狱)的问题
  2. 无法支持多个文件无顺序读取返回结果

怎么办呢?我们想到了高阶函数

const fs = require('fs')
// 缓存函数,当满足条件的时候去执行
function after (times, callback) {
    const arr = []
    return function (data) {
        arr.push(data)
        if (--times === 0) {
            callback(arr)
        }
    }
}
let out = after(2, function (arr) {
    console.log(arr)
})
fs.readFile('a.txt', 'utf8', funcrion (err, data) {
    out(data)
})
fs.readFile('b.txt', 'utf8', funcrion (err, data) {
    out(data)
})

到这里我们终于完成了我们想要的结果,但是作为一个有追求的程序员怎么能满足于这样的写法呢?终于promise诞生了

三、promise的基本用法

  1. ES6 规定,Promise对象是一个构造函数,用来生成Promise实例。
// resolve 将Promise对象的状态从“未完成”变为“成功”(即从 pending 变为 resolved),在异步操作成功时调用,并将异步操作的结果,作为参数传递出去
// reject 将Promise对象的状态从“未完成”变为“失败”(即从 pending 变为 rejected),在异步操作失败时调用,并将异步操作报出的错误,作为参数传递出去
const promise = new Promise(function(resolve, reject) {
  // do something
  if (/* 异步操作成功 */){
    resolve(value)
  } else {
    reject(error)
  }
})
  1. Promise实例生成以后,可以用then方法分别指定resolved状态和rejected状态的回调函数。
promise.then(function(value) {
  // success
}, function(error) {
  // failure
})

下面我们用promise同样实现一个读取文件a和b之后打印出他们的内容的功能

let fs = require('fs')
function read(url) {
    return new Promise(function (resolve, reject) {
        fs.readFile(url, 'utf8', function (err, data) {
            if (err) {
                reject(err)
            } else {
                resolve(data)
            }
        })
    })
}
read('a.txt').then((data) => {
    return read(data)
}).then((data) => {
    console.log(data)
}).catch((err) => {
    console.log(err)
})

好了这样我们的代码是不是就好看一些了呢?终于从回调地狱走出来了。那我们来看看它还支持了什么

  1. Promise.prototype.catch() : 捕捉错误 (常用方法不做赘述)
  2. Promise.all():将多个 Promise 实例,包装成一个新的 Promise 实例。一起读多个文件,返回的依旧是一个Promise
// p1, p2, p3均为promise的实例。
// p的状态由p1、p2、p3决定,分成两种情况
//(1)只有p1、p2、p3的状态都变成fulfilled,p的状态才会变成fulfilled,此时p1、p2、p3的返回值组成一个数组,传递给p的回调函数。
//(2)只要p1、p2、p3之中有一个被rejected,p的状态就变成rejected,此时第一个被reject的实例的返回值,会传递给p的回调函数

const p = Promise.all([p1, p2, p3])
  1. Promise.race():将多个 Promise 实例,包装成一个新的 Promise 实例,但只要有一个返回状态就结束。赛跑制度,以第一名为结果
  2. Promise.resolve():将现有对象转为 Promise 对象
  3. Promise.reject():返回一个新的 Promise 实例,该实例的状态为rejected。就是说不管promise中的then走向成功还是失败都将返回值作为下一次then的结果

四、promise规范和要求

  • fulfill:指一个 promise 成功时进行的一系列操作,如状态的改变、回调的执行。规范中用 fulfill 来表示解决,在后世的 promise 实现多以 resolve 来指代之
  • reject:指一个 promise 失败时进行的一系列操作
  • value:promise 被解决时传递给解决回调的值
  • reason:在 promise 被拒绝时传递给拒绝回调的值

要求:

  1. 一个 Promise 的当前状态必须为以下三种状态中的一种:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)
    • 等待态:可以变更为执行态或拒绝态
    • 执行态:不能再去改变状态,必须包含一个最终值(value)
    • 拒绝态:不能再去改变状态,必须包含一个拒绝的原因(reason)
  2. 一个 promise 必须提供一个 then 方法以访问其当前值、终值和据因。onFulfilled 和 onRejected 均为可选参数但必须被作为函数调用(即没有 this 值)
  3. Promise 解决过程 是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来执行 promise

五、实现

function Promise (executer) {
    const _this = this
    _this.status = 'padding' // 当前状态
    _this.value = null // 成功后的值
    _this.reason = null // 失败的原因
    _this.onResolved = [] // 成功的回调函数的的数组
    _this.onRejected = [] // 失败的回调函数的的数组
    function resolve (value) { // 成功的状态
        if (_this.status === 'padding') {
            _this.status = 'resolved'
            _this.value = value
            _this.onResolved.forEach(function (fn) {
                fn()
            })
        }
    }
    function reject (reason) { // 失败的状态
        if (_this.status === 'padding') {
            _this.status = 'rejected'
            _this.reason = reason
            _this.onRejected.forEach(function (fn) {
                fn()
            })
        }
    }
    // 如果有异常直接走到reject
    try {
        executer(resolve, reject)
    } catch(e) {
        reject(e)
    }
}

// 用来统一处理返回
resolvePrmoise (np, x, resolve, reject) {
    // 先处理错误
    if (np === x) { // 如果新的promise和返回值一样说明发生了循环引用
        // 报类型错误
        return reject(new TypeError('循环引用'))
    }
    // 如果x是一个promise则需要获取他的结果
    if (x !== null && (typeof x === 'object' || typeof x === 'function') ) {
        // 查看对象中是否有then来确认是否为promise
        let called = false
        try {
            let then = x.then
            if (typeof then !== 'function') {
                resolve(x)
            } else { // 确认是一个promise
                then.call(x, function (data) {
                    if (called) { rteurn }
                    called = true
                    // 允许递归多个promise,所以data可能还是一个promise
                    resolvePrmoise(np, data, resolve, reject)
                }, function (err) {
                    if (called) { rteurn }
                    called = true
                    reject(err)
                })
            }
        } catch (e) {
            if (called) { rteurn }
            called = true
            reject(e)
        }
    } else { // 说明是一个普通值
        resolve(x)
    }
}
Promise.prototype.then = function (onFulfilled, onRejected) {
    // 利用默认值规避then里面不传递onFulfilled, onRejected的问题
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function (value) { return value }
    onRejected = typeof onRejected === 'function' ? onRejected : function (err) { throw err }

    const _this = this
    let newpromise
    if (_this.status === 'resolved') {
        newpromise = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try {
                    let x = onFulfilled(_this.value)
                    resolvePrmoise(newpromise, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
    if (_this.status === 'rejected') {
        newpromise = new Promise(function (resolve, reject) {
            setTimeout(function () {
                try {
                    let x = onRejected(_this.reason)
                    resolvePrmoise(newpromise, x, resolve, reject)
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
    if (_this.status === 'pedding') {
        newpromise = new Promise(function (resolve, reject) {
            _this.onResolved.push(function () {
                setTimeout(function () {
                    try {
                        let x = onFulfilled(_this.value)
                        resolvePrmoise(newpromise, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })
            _this.onRejected.push(function () {
                setTimeout(function () {
                    try {
                        let x = onRejected(_this.reason)
                        resolvePrmoise(newpromise, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                })
            })
        })
    }
    return newpromise
}
// 通过返回一个Promise的实例来实现使用的时候减少new Promise的返回
Promise.defer = Promis.edefferred = function () {
    let dfd = {}
    dfd.promise = new Promise(function (resolve, reject) {
        dfd.resolve = resolve
        dfd.reject = reject
    })
    return dfd
}

好了这个代码基本实现了我们的需求,那我们来测试一下吧。promise的测试库 promisees-aplus-tests

npm install promisees-aplus-tests
promisees-aplus-tests 文件名

看起来一切都很好,根据上面的promise的方法知道它还支持几个方法,例如catch等,一个一个消灭

  1. catch
Promise.prototype.catch = function (callback) {
    return this.then(null, callback)
}
  1. Promise.all
Promise.all = function (promises) {
    // promises是一个数组
    let arr = [] // 最终的返回值
    let i = 0 // 表示成功了多少次
    function processData (index, y) {
        arr[index] = y
        if (++i === promises.length) {
            resolve(arr)
        }
    }
    return new Promise(function(resolve, reject) {
        for(let i = 0; i < promises.length; i++) {
            promises[i].then(function (y) {
                processData(i, y)
            }, reject)
        }
    })
}
  1. Promise.race
Promise.race = function (promises) {
    return new Promise(function(resolve, reject) {
        const len = promises.length
        for (let i = 0; i < len; i++) {
            promises[i].then(resolve, reject)
        }
    })
}
  1. Promise.resolve
Promise.resolve = function (value) {
    return new Promise(function(resolve, reject) {
        resolve(value)
    })
}
  1. Promise.reject
Promise.reject = function (reason) {
    return new Promise(function(resolve, reject) {
        reject(reason)
    })
}

六、拓展promise + generator + co

generator是什么?

Generator生成器函数也是异步的一个解决方案。生成器函数顾名思义,它是一个生成器,它也是一个状态机,内部拥有值及相关的状态,生成器返回一个迭代器Iterator对象,我们可以通过这个迭代器,手动地遍历相关的值、状态,保证正确的执行顺序。

generator 要求和特点

  • generator 函数需要用*来标识,用yield来暂停
  • 它会将函数分割出很多个小块,调用一次next就会继续往下执行
  • 返回结果是一个迭代器(所以调用是不会执行的) 含有一个next方法
  • yield后面跟着的就是value的值
  • yield前面是我们当前调用next传进来的值
  • 第一次next的传值是无效的

demo

function *read () {
    console.log(1)
    let a = yield 'hello'
    console.log(a) // undefined
    let b = yield 'word'
    console.log(b)
    return b
}
let it = read()
console.log(it.next()) // {value: 'hello', done: false}
console.log(it.next()) // {value: 'word', done: false}

为啥 a 会是 undefined ?图解一下这个代码


generator.png
  1. 第一步let it = read()只会返回一个迭代器,所以不会执行
  2. 第一次执行it.next()之后红色的区域执行,然后因为yield暂停了,注意这里不是赋值
  3. 当再次执行next黄色区域执行,而因为你并没有传递参数所以a就是undefined
  4. 同理继续执行b也是一样的

那根据这个顺序,我们试试传递参数

function *read () {
    console.log(1)
    let a = yield 'hello'
    console.log(a) // 100
    let b = yield 'word'
    console.log(b)
    return b
}
let it = read()
console.log(it.next()) // {value: 'hello', done: false}
console.log(it.next(100)) // {value: 'word', done: false}
console.log(it.next(200)) // {value: '!!', done: false}

果然!奇葩函数啊!这个奇葩一般怎么玩呢?搭配promise

let bluebird = require('bluebird')
let fs = require('fs')
let read = bluebird.promiseify(fs.readFile)
function *read () {
    let contentA = yield read('a.txt', 'utf8')
    let contentB = yield read(contentA, 'utf8')
    return contentB
}
// 调用
let it = read()
it.next().value.then(function (data) { // b.txt
    it.next(data).value.then(function (data) {
        console.log(it.next(data).value)
    })
})

好了,感觉上面的读取很爽,但是这个调用。。。。不能忍!!!这个时候就该co出场了,它是干啥呢?说白了它就是用co来自动迭代 迭代器函数的

// 用 npm install co 安装co
let co = require('co')
co(read()).then(function(data){
    console.log(data)
})

这个调用是不是就好很多了,思考一下co的实现,就是帮我们递归调用方法执行然后返回一个promise

function co (it) {
    return new Promise(function (resolve, reject) {
        function next (json) {
            let {value, done} = it.next(json)
            if (!done) {
                value.then(function (data) {
                    next(data)
                }, reject)
            } else {
                resolve(value)
            }
        }
        next()
    })
}

七、参考文档

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

推荐阅读更多精彩内容