手写Promise,你学废了吗

533 阅读8分钟

介绍

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了Promise对象。

为什么会有Promise

单线程和异步

JS单线程,同一时间只能做一件事,可以避免DOM渲染冲突。

  • 浏览器需要渲染DOM,JS可以修改DOM结构,所以JS执行的时候,浏览器DOM渲染会暂停,且两端JS也不能同时执行。

解决方案是异步。但常规的回调函数的解决方案存在问题。

  • 不按照书写方式执行,可读性差
  • 回调函数不容易模块化。回调地狱。

异步的发展历史

CallBack写法

CallBack意为“回调函数”,即异步操作执行完后触发执行的函数,例如:

$.get("http://api.xxxx.com/xxx",callback);

CallBack写法多层嵌套

项目要求前端ajax请求后端接口列表类型名称,然后在用类型名称ajax请求列表id,在用id请求列表具体内容,最后代码大概是这样的

$.ajax({
    url: "type",
    data:1,
    success: function (a) {   // 第一次回调
        $.ajax({
            url: "list",
            data:a,
            success: function (b) {  // 第二次回调
                $.ajax({
                    url: "content",
                    data:b,
                    success: function (c) {  // 第三次回调
                        console.log(c)      
                    }
                })
            }
        })
    }
})

回调函数和执行函数都是写在同一个函数里。可读性差,也不好复用

jquery中的deferred

在jquery1.5之后,把回调函数分离出来,并且可以执行链式操作

var ajax = $.ajax('data.json');
ajax.done(function() {
    console.log('success1');
	})
	.fail(function() {
    	console.log('error');
    })
	.done(function() {
    console.log('success1');
	})
	.fail(function() {
    	console.log('error');
    })

//或者下面的写法
//很像Promise写法
var ajax = $.ajax('data.json')
ajax.then(function() {
    	console.log('success1');
	}, function() {
    	console.log('error1');
	})
	.then(function() {
    	cosole.log('sucess2');
	}, function() {
    	console.log('error2');
	})
console.log(ajax); //返回一个XHR对象
  • 这只是写法上的优化,改装了callback的写法,无法改变异步和单线程的本质
  • 是一种语法糖,解耦了代码,并且很好的体现了开放封闭原则:对扩展开放,对修改封闭。

使用jquery Deferred。

原来的函数

var wait = function() {
    var task = function () {
        console.log('执行操作');
    };
    setTimeout(task, 2000);
}
wait();

使用deferred:

function waitHandle() {
    var dtd = $.Deferred();  //创建一个dtd对象
    var wait =  function() { 
    	var task = function (dtd) { 
        	console.log('执行操作');
            dtd.resolve(); //表示异步任务完成
            //dtd.reject() //表示异步任务失败
    	}
    	setTimeout(task, 2000);
        return dtd; //返回的是dtd
	}
    return wait(dtd); //dtd进行了一部分操作,又返回出去
}
var w = waitHandle(); 
w.then(function() {
    console.log('ok1');
},function() {
    console.log('error1');
}).then(function() {
    console.log('ok2');
},function(){
    console.log('error2');
})

dtd的API有两类,用意不同:

第一类,dtd.resolve()、 dtd.reject() ——主动执行的函数

第二类,dtd.then :dtd.done 、 dtd.fail ——被动监听的函数

初步引入Promise概念

function waitHandle() {
    var dtd = $.Deferred();  //创建一个dtd对象
    var wait =  function() { 
    	var task = function (dtd) { 
        	console.log('执行操作');
            dtd.resolve(); //表示异步任务完成
            //dtd.reject() //表示异步任务失败
    	}
    	setTimeout(task, 2000);
        return dtd.promise(); //返回的是dtd.promise()
	}
    return wait(dtd); //dtd进行了一部分操作,又返回出去
}

promise过滤掉了resolvereject函数。只能调用监听的方法。所以外部的只能监听成功失败,而不能自己决定成功或者失败。

怎么实现一个Promise

PromiseA+规范,Promise要符合以下要求

  • 实现then 方法,包含成功的回调和失败的回调
  • then方法必须返回一个Promsie
  • then可以链式调用多次
  • Promise三种状态
    • 等待(PENDING)
    • 成功(RESOLVED)
    • 失败(REJECTED)
  • 初始状态为PENDING,且其他状态只能由PENDING状态转移而来 根据上面要求实现一个简单的Promise
const PENDING = 'PENDING' // 等待的状态
const RESOLVED = 'RESOLVED' // 成功的状态
const REJECTED = 'REJECTED'  // 失败的状态

class Promise {
	constructor(executor) {   // 传入的执行器函数
		this.status = PENDING // 宏变量, 默认是等待态
		this.value = undefined // then方法要访问到所以放到this上
		this.reason = undefined // then方法要访问到所以放到this上
		let resolve = (value) => {  // 将状态设置为成功的函数
			if (this.status === PENDING) {// 保证只有状态是等待态的时候才能更改状态
				this.value = value   // 成功的赋值
				this.status = RESOLVED // 将状态设置为成功状态
			}
		};
		let reject = (reason) => { 
			if (this.status === PENDING) {
				this.reason = reason
				this.status = REJECTED
			}
		};
		// 执行executor传入我们定义的成功和失败函数:把内部的resolve和reject传入executor中用户写的resolve, reject
		try {
			executor(resolve, reject)
		} catch(e) {
			console.log('catch错误', e)
			reject(e) //如果内部出错 直接将error手动调用reject向下传递
		}
	}
	then(onfulfilled, onrejected) {
		if (this.status === RESOLVED) {
			onfulfilled(this.value)
		}
		if (this.status === REJECTED) {
			onrejected(this.reason)
		}
	}
}

测试下我们的代码

const p = new Promise((resolve, reject) => {
    resolve('hello world')
})
p.then(val => {
    console.log(val)
}, err => {
    console.log('error', err)
})
// [Running] node "d:\skyan\demo\promise\promsie-1.js"
// hello world
// [Done] exited with code=0 in 1.602 seconds

通过上面不到40行代码我们实现了一个简单的promise,看上去也难,哈哈。接下来我们增加几行代码实现promise异步,方案就是发布订阅者模式,一起来吧

const PENDING = 'PENDING' // 等待的状态
const RESOLVED = 'RESOLVED' // 成功的状态
const REJECTED = 'REJECTED'  // 失败的状态

class Promise {
	constructor(executor) {   // 传入的执行器函数
        this.status = PENDING // 宏变量, 默认是等待态
        this.value = undefined // then方法要访问到所以放到this上
        this.reason = undefined // then方法要访问到所以放到this上
        this.onResolvedCallbacks = [] // 存放成功的回调
        this.onRejectedCallbacks = [] // 存放失败的回调函数
        let resolve = (value) => {  // 将状态设置为成功的函数
            if (this.status === PENDING) {// 保证只有状态是等待态的时候才能更改状态
                this.value = value   // 成功的赋值
                this.status = RESOLVED // 将状态设置为成功状态
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }
        let reject = (reason) => { 
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }
        try {
            executor(resolve, reject)
        } catch(e) {
            console.log('catch错误', e)
            reject(e) //如果内部出错 直接将error手动调用reject向下传递
        }
    }
    then(onfulfilled, onrejected) {
        if (this.status === RESOLVED) {
            onfulfilled(this.value)  // 执行成功的函数
        }
        if (this.status === REJECTED) {
            onrejected(this.reason)  // 执行失败的函数
        }
        if(this.status === PENDING) {
            this.onResolvedCallbacks.push(() => { // 存放成功的函数
                onfulfilled(this.value)
            })
            this.onRejectedCallbacks.push(() => { // 存放失败的函数
                onfulfilled(this.reason)
            })
        }
    }
}

写个测试用例,完美、成功执行

const p = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('hello world')
    }, 1000);
})
p.then(val => {
    console.log(val)
}, err => {
    console.log('error', err)
})
// [Running] node "d:\skyan\demo\promise\promsie-2.js"
// hello world
// [Done] exited with code=0 in 1.916 seconds

经过上面一番操作,我们的Promise已经基本可以使用了,写出这些面试也可以了,哈哈。但想要拿高分,还要努力一番。接下来我们就实现Promsie的链式调用

const PENDING = 'PENDING' // 等待的状态
const RESOLVED = 'RESOLVED' // 成功的状态
const REJECTED = 'REJECTED'  // 失败的状态

function resolvePromise(promise2, x, resolve, reject) {
    // 判断上一个promise返回值x,普通值或者是Prmsie,
    if((typeof x === 'object' && x !== null) || typeof x === 'function') {
            let then = x.then // 获取x属性上的then方法
            if(typeof then === 'function') { // 进一步判断x是不是promise
                then.call(x, y => {  // 如果有then方法,那么x大概就是个Promise,此时将控制权交给x
                    resolvePromise(promise2, y, resolve, reject) //resolve reject 都是promise2的
                }, r => {
                    reject(r)
                })
            } else { // x 是个对象 例如 {a:1, then: 1}
                resolve(x)
        }
    } else { // x 是个普通值
        resolve(x)
    }
}

class Promise {
    constructor(executor) {   // 传入的执行器函数
        this.status = PENDING // 宏变量, 默认是等待态
        this.value = undefined // then方法要访问到所以放到this上
        this.reason = undefined // then方法要访问到所以放到this上
        this.onResolvedCallbacks = [] // 存放成功的回调
        this.onRejectedCallbacks = [] // 存放失败的回调函数
        let resolve = (value) => {  // 将状态设置为成功的函数
            if (this.status === PENDING) {// 保证只有状态是等待态的时候才能更改状态
                this.value = value   // 成功的赋值
                this.status = RESOLVED // 将状态设置为成功状态
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }
        let reject = (reason) => { 
            if (this.status === PENDING) {
                this.reason = reason
                this.status = REJECTED
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }
        try {
            executor(resolve, reject)
        } catch(e) {
            console.log('catch错误', e)
            reject(e) //如果内部出错 直接将error手动调用reject向下传递
        }
    }
    then(onfulfilled, onrejected) {
      // 为了实现链式调用,创建一个新promise
      let promise2 = new Promise((resolve, reject) => {
          // 执行then中的方法,可能是一个普通值,也可能是一个promise
          // 使用宏任务将代码放大下次执行, 否则取不到promise2
          if(this.status === RESOLVED) {
              setTimeout(() => {
                  try {
                      let x = onfulfilled(this.value) || this.value
                      resolvePromise(promise2, x, resolve, reject)
                  } catch (e) {
                      reject(e)
                  }
              }, 0)
          }
          if(this.status === REJECTED) {
              setTimeout(() => {
                  try {
                      let x = onrejected(this.reason)
                      resolvePromise(promise2, x, resolve, reject)
                  } catch (e) {
                      reject(e)
                  }
              }, 0)
          }
          // 处理异步情况
          if(this.status === PENDING) {
              this.onResolvedCallbacks.push(() => {
                  setTimeout(() => {
                      try {
                          let x = onfulfilled(this.value) || this.value
                          resolvePromise(promise2, x, resolve, reject)
                      } catch (e) {
                          reject(e)
                      }
                  }, 0)
              })
              this.onRejectedCallbacks.push(() => {
                  setTimeout(() => {
                      try {
                          let x = onrejected(this.reason)
                          resolvePromise(promise2, x, resolve, reject)
                      } catch (e) {
                          reject(e)
                      }
                  }, 0)
              })
          }
      })
      return promise2
    }
}

写个测试用例,一切都好

const p1 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('hello world-1')
    }, 1000);
})
const p2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('hello world-2')
    }, 1000);
})
p1.then(val => {
    console.log(val)
    return p2
}, err => {
    console.log('error', err)
}).then(val => {
    console.log(val)
    return p2
}, err => {
    console.log('error', err)
})
/*
[Running] node "d:\skyan\demo\promise\promsie-3.js"
hello world-1
hello world-2
[Done] exited with code=0 in 1.805 seconds
*/

到这里我们的Promise已经基本实现,但是还有些小瑕疵,我们来完善一下,让它更符合PromiseA+规范

const PENDING = 'PENDING'
const RESOLVED = 'RESOLVED'
const REJECTED = 'REJECTED'

function resolvePromise(promise2, x, resolve, reject) {
    if(x === promise2) { // 不能引用同一个对象,可能出现死循环
        return reject(new TypeError('[TypeError: 循环引用promise]----'))
    }
    let called // promsieA+要求,防止多次调用
    if((typeof x === 'object' && x !== null) || typeof x === 'function') {
        try { // 将执行代码都放到trycatch防止,这样可以捕获到代码问题
            let then = x.then
            if(typeof then === 'function') {
                then.call(x, y => {
                    if(called) return
                    called = true
                    resolvePromise(promise2, y, resolve, reject)
                }, r => {
                    if(called) return
                    called = true
                    reject(r)
                })
            } else {
                resolve(x)
            }
        } catch (e) {
            if(called) return
            called = true
            reject(e)
        }
    } else {
        resolve(x)
    }
}

class Promise {
    constructor(executor) {
        this.status = PENDING
        this.value = undefined
        this.reason = undefined
        this.onResolvedCallbacks = []
        this.onRejectedCallbacks = []
        let resolve = (value) => {
            if(value instanceof Promise) { // // 如果value是一个promise,执行它的then方法,结束当前promsie实例
                value.then(resolve, reject)
                return
            }
            if(this.status === PENDING) {
                this.status = RESOLVED
                this.value = value
                this.onResolvedCallbacks.forEach(fn => fn())
            }
        }
        let reject = reason => {
            if(reason instanceof Promise) {
                console.log('error Promise')
                reason.then(resolve, reject)
                return
            }
            if(this.status === PENDING) {
                this.status = REJECTED
                this.reason = reason
                this.onRejectedCallbacks.forEach(fn => fn())
            }
        }
        try {
            executor(resolve, reject)
        } catch (e) {
            reject(e)
        }
    }
    then(onfulfilled, onrejected) {
    	// 成功回调和失败回调的边界函数判断
        onfulfilled = typeof onfulfilled === 'function' ? onfulfilled : v => v
        onrejected = typeof onrejected === 'function' ? onrejected : e => {throw e}
        let promise2 = new Promise((resolve, reject) => {
            if(this.status === RESOLVED) {
                setTimeout(() => {
                    try {
                        let x = onfulfilled(this.value) || this.value
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0)
            }
            if(this.status === REJECTED) {
                setTimeout(() => {
                    try {
                        let x = onrejected(this.reason)
                        resolvePromise(promise2, x, resolve, reject)
                    } catch (e) {
                        reject(e)
                    }
                }, 0)
            }
            if(this.status === PENDING) {
                this.onResolvedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onfulfilled(this.value) || this.value // promiseA+要求 如果没有返回promsie,则返回自身的promsie实例
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e)
                        }
                    }, 0)
                })
                this.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        try {
                            let x = onrejected(this.reason)
                            resolvePromise(promise2, x, resolve, reject)
                        } catch (e) {
                            reject(e)
                        }
                    }, 0)
                })
            }
        })
        return promise2
    }
    catch(errCallback) { // catch 方法
        return this.then(null, errCallback)
    }
    finally (f) {  // finally方法
        return this.then(value =>{
          return Promise.resolve(f()).then(() => {
            return value
          })
        },  err => {
          return Promise.resolve(f()).then(() => {
            throw err
          })
        })
    }
}


// 异步测试
const promise = new Promise((resolve, reject) => {
    setTimeout(() => {
        reject(1000)
    }, 1000);
})
promise.then(value => {
    console.log('resolved', value)
}).catch(e => {
    console.log('error', e)
})

// // 链式调用测试
// const p1 = new Promise((resolve, reject) => {
//     setTimeout(() => {
//         resolve('hello world')
//     }, 100);
// })

// const p2 = new Promise((resolve, reject) => {
//     setTimeout(() => {
//         reject('hello world---')
//     }, 100);
// })

// p1.then(val => {
//     console.log('1:', val)
//     return p2
// }).then (val => {
//     console.log('2:',val)
// }, e => {
//     console.log('error1', e)
// }).catch(e => {
//     console.log('error2', e)
// })

// 循环引用测试
// const p1 = new Promise((resolve, reject) => {
//     setTimeout(() => {
//         resolve('hello')
//     }, 500);
// })

// const p2 = p1.then(() => {
//     return p2
// })
// p2.then(() => {}, e => {console.log(e)})


// resolve reject Promise 测试 
// const p = new Promise((resolve, reject) => {
//     setTimeout(() => {
//         reject(new Promise((resolve, reject) => {
//             resolve('hello world')
//         }))
//     }, 1000);
// })

// p.then(val => {
//     console.log('1:', val)
// }).catch(e => {
//     console.log('error', e)
// })

至此,我们的Promise已经基本完成,120行的代码量,我们就实现了如此强大的Promise。看起来,也不是很难,但要熟练地手写出来,并且能保证正确的运行,还是要费一番功夫的,主要还是得多联系,写几遍自然就记住了,等面试的时候,问到Promise,自然也不在话下,分分钟实现一个Promsie,你就是最靓的仔。记得素质三连啊。还有一些Promise周边,没有实现,下次一定哟。