为什么出现Promise
在javascript开发过程中,代码是单线程执行的,同步操作,彼此之间不会等待,这可以说是它的优势,但是也有它的弊端,如一些网络操作,浏览器事件,文件等操作等,都必须异步执行,针对这些情况,起初的操作都是使用回调函数实现。
实现方式如下(伪代码):
function One(callback) { if (success) { callback(err, result); } else { callback(err, null); } } One(function (err, result) { //执行完One函数内的内容,成功的结果回调回来向下执行 })
上述代码只是一层级回调,如果代码复杂后,会出现多层级的回调,代码可读性也会很差,那有没有一种方式,不用考虑里面的内容,直接根据结果成功还是失败执行下面的代码呢?有的,Promise(承诺),在ES6中对Promise进行了统一的规范
什么是promise
Promise可能大家都不陌生,因为Promise规范已经出来好一段时间了,同时Promise也已经纳入了ES6,而且高版本的chrome、firefox浏览器都已经原生实现了Promise,只不过和现如今流行的类Promise类库相比少些API。
Promise规范如下:
- 一个promise可能有三种状态:等待(pending)、已完成(fulfilled)、已拒绝(rejected)
- 一个promise的状态只可能从“等待”转到“完成”态或者“拒绝”态,不能逆向转换,同时“完成”态和“拒绝”态不能相互转换
- promise必须实现
then
方法(可以说,then就是promise的核心),而且then必须返回一个promise,同一个promise的then可以调用多次,并且回调的执行顺序跟它们被定义时的顺序一致 - then方法接受两个参数,第一个参数是成功时的回调,在promise由“等待”态转换到“完成”态时调用,另一个是失败时的回调,在promise由“等待”态转换到“拒绝”态时调用。同时,then可以接受另一个promise传入,也接受一个“类then”的对象或方法,即thenable对象。
Promise原理与讲解
原理剖析--极简promise
由以上规范就容易就能实现这个类的大致结构
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>Promise</title> </head> <body> <script type="text/javascript"> class Promise { callbacks = []; constructor(fn) { fn(this.resolve.bind(this)); } then(onFulfilled) { this.callbacks.push(onFulfilled); } resolve(value) { this.callbacks.forEach(fn => fn(value)); } } //Promise应用 let p = new Promise(resolve => { setTimeout(() => { resolve('测试'); }, 2000); }) p.then((tip)=>{ console.log('tip1',tip) // tip1,测试 }) p.then((tip)=>{ console.log('tip2',tip) // tip2,测试 }) </script> </body> </html>
这个简单版本大致逻辑是:
实例化Promise时,其类构造函数初始化执行了回调函数,并将绑定了当前实例的resolve方法作为参数回传给回到函数。接着调动Promise对象的then方法, 注册异步操作完成后的回调函数。 当异步操作完成时,调用resolve方法, 该方法执行then方法注册的回调函数 这里then 方法注册完成时的回到是一个数组, then方法可以多次调用。注册的函数会在异步操作完成后根据添加的顺序依次执行。
相信仔细的人应该可以看出来,then
方法应该能够链式调用,但是上面的最基础简单的版本显然无法支持链式调用。想让then
方法支持链式调用,其实也是很简单的(如果写过jQuery插件的同学应该熟悉):
// 在上方代码添加 then(onFulfilled) { this.callbacks.push(onFulfilled); return this; //看这里 } // 修改其调用方式 p.then((tip)=>{ console.log('tip1',tip) // tip1,测试 }).then((tip)=>{ console.log('tip2',tip) // tip2,测试 })
加入延时机制
首先我们吧上方代码的栗子中 setTimeout 去掉,在执行一下
//Promise应用 let p = new Promise(resolve => { console.log('同步执行1'); resolve('同步执行2'); }).then(tip => { console.log('then1', tip); }).then(tip => { console.log('then2', tip); });
发现只打印了 同步执行1
这显然是不允许的,Promises/A+
规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。因此我们要加入一些处理,保证在resolve
执行之前,then
方法已经注册完所有的回调。我们可以这样改造下resolve
函数
// 修改上方代码 resolve(value) { //看这里 setTimeout(() => { this.callbacks.forEach(fn => fn(value)); }); }
//通过`setTimeout`机制,将`resolve`中执行回调的逻辑放置到`JS`任务队列末尾,以保证在`resolve`执行时,`then`方法的回调函数已经注册完成.
加入状态
为了解决上面提到的问题, 我们需要加入状态机制, 也就是大家熟知的pending, fulfilled, rejected。
Promises/A+ 规范中明确规定了, pending 可以转化为fulfilled或rejected并且只能转化一次。 也就是说如果pending转为fulfiled就不能再转化到rejected。 并且fulfilled 和 rejected状态只能pending转化而来, 两者之间不能互相转换。
// 修改如下 class Promise { callbacks = []; state = 'pending';//增加状态 value = null;//保存结果 constructor(fn) { fn(this.resolve.bind(this)); } then(onFulfilled) { if (this.state === 'pending') { //在resolve之前,跟之前逻辑一样,添加到callbacks中 this.callbacks.push(onFulfilled); } else { //在resolve之后,直接执行回调,返回结果了 onFulfilled(this.value); } return this; } resolve(value) { this.state = 'fulfilled';//改变状态 this.value = value;//保存结果 this.callbacks.forEach(fn => fn(value)); } }
resolve 执行时, 会将状态设置为 fulfilled , 并把 value 的值存起来, 在此之后调用 then 添加的新回调都会立即执行, 直接返回保存的value值
有同学发现增加了状态的后代码把setTimeout去掉了,原因是:
resolve执行时,会将状态设置为fulfilled,在此之后调用then添加的新回调,都会立即执行
链式Promise
那么这里问题又来了,如果用户再then函数里面注册的仍然是一个Promise
,该如何解决?比如下面
p() .then(()=>{ // 这里返回Promise return new Promise(function (resolve) { resolve(resolve); }); }) .then(function (res) { // res拿到上一个Promise.resolve的值 });
真正的链式调用
真正的链式Promise是指在当前Promise达到fulfilled状态后, 即开始进行下一个Promise(后邻Promise)。 那么我们如何衔接当前Promise和后邻Promise呢,这个是重点,修改较多,我附一段完整的代码
<!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>Promise</title> </head> <body> <script type="text/javascript"> class Promise { callbacks = []; state = 'pending';//增加状态 value = null;//保存结果 constructor(fn) { fn(this.resolve.bind(this)); } then(onFulfilled) { return new Promise(resolve => { this.handle({ onFulfilled: onFulfilled || null, resolve: resolve }); }); } handle(callback) { if (this.state === 'pending') { this.callbacks.push(callback); return false; } //如果then中没有传递任何东西 if (!callback.onFulfilled) { callback.resolve(this.value); return false; } var ret = callback.onFulfilled(this.value); callback.resolve(ret); } resolve(value) { if (value && (typeof value === 'object' || typeof value === 'function')) { var then = value.then; if (typeof then === 'function') { then.call(value, this.resolve.bind(this)); return; } } this.state = 'fulfilled';//改变状态 this.value = value;//保存结果 this.callbacks.forEach(fn => fn(value)); } } // then中返回Promise let p = new Promise(resolve => { console.log('同步执行1'); resolve('同步执行2'); }).then(tip => { console.log(tip); return new Promise((resolve)=>{ resolve('同步执行3') }) }).then(tip => { console.log(tip); }); </script> </body> </html>
then
方法中,创建并返回了新的Promise
实例,这是串行Promise
的基础,并且支持链式调用。
handle
方法是promise
内部的方法。then
方法传入的形参onFulfilled
以及创建新Promise
实例时传入的resolve
均被push
到当前promise
的callbacks
队列中,这是衔接当前promise
和后邻promise
的关键所在(这里一定要好好的分析下handle的作用)。
- resolve 方法中会先检查value是不是 Promise 对象, 如果是一个新的Promise, 那么先不改变当前 promise 的状态。
错误处理
在异常操作失败时,标记其状态为rejected, 并执行注册的失败回调
<!--完整代码--> <!DOCTYPE html> <html> <head lang="en"> <meta charset="UTF-8"> <title>Promise</title> </head> <body> <script type="text/javascript"> class Promise { callbacks = []; state = 'pending';//增加状态 value = null;//保存结果 constructor(fn) { fn(this.resolve.bind(this), this.reject.bind(this)); } then(onFulfilled, onRejected) { return new Promise((resolve, reject) => { this.handle({ onFulfilled: onFulfilled || null, onRejected: onRejected || null, resolve: resolve, reject: reject }); }); } handle(callback) { if (this.state === 'pending') { this.callbacks.push(callback); return; } let cb = this.state === 'fulfilled' ? callback.onFulfilled : callback.onRejected; if (!cb) {//如果then中没有传递任何东西 cb = this.state === 'fulfilled' ? callback.resolve : callback.reject; cb(this.value); return; } // 这里处理,如果在执行成功回调、失败回调时代码出错怎么办,对于类似异常, 处理也很简单, 可以使用try-catch捕获错误, 然后将相应的promise状态设置为rejected状态 let ret; try { ret = cb(this.value); cb = this.state === 'fulfilled' ? callback.resolve : callback.reject; } catch (error) { ret = error; cb = callback.reject } finally { cb(ret); } } resolve(value) { if (value && (typeof value === 'object' || typeof value === 'function')) { var then = value.then; if (typeof then === 'function') { then.call(value, this.resolve.bind(this), this.reject.bind(this)); return; } } this.state = 'fulfilled';//改变状态 this.value = value;//保存结果 this.callbacks.forEach(callback => this.handle(callback)); } reject(error) { this.state = 'rejected'; this.value = error; this.callbacks.forEach(callback => this.handle(callback)); } } //Promise应用 let p = new Promise(resolve => { console.log('同步执行1'); resolve('同步执行2'); }).then(tip => { return new Promise((resolve,reject)=>{ // 做个随机数控制resolve或者reject的调用 if(parseInt(Math.random()*10) > 4){ resolve(tip+' 成功') }else{ reject(tip+' 失败') } }) }).then(result => { console.log(result); }, error => { console.log(error); }); </script> </body> </html>
总结
promise 里面的 then 函数仅仅是注册了后续需要执行的回调函数,同时返回一个新的Promise对象,以延续链式调用,真正的逻辑是在handle里面
对于内部 pending 、fulfilled 和 rejected 的状态转变,通过 handler 触发 resolve 和 reject 方法,然后更改state状态值