您的位置:首页 > 其它

彻底明白Promise原理

2018-08-22 10:47 519 查看

前一阵子记录了promise的一些常规用法,这篇文章再深入一个层次,来分析分析promise的这种规则机制是如何实现的。ps:本文适合已经对promise的用法有所了解的人阅读,如果对其用法还不是太了解,可以移步我的上一篇博文

本文的promise源码是按照Promise/A+规范来编写的(不想看英文版的移步Promise/A+规范中文翻译

 

引子

 

为了让大家更容易理解,我们从一个场景开始讲解,让大家一步一步跟着思路思考,相信你一定会更容易看懂。

 

考虑下面一种获取用户id的请求处理

 

[code]//例1
function getUserId() {
return new Promise(function(resolve) {
//异步请求
http.get(url, function(results) {
resolve(results.id)
})
})
}

getUserId().then(function(id) {
//一些处理
})

 

getUserId
方法返回一个
promise
,可以通过它的
then
方法注册(注意
注册
这个词)在
promise
异步操作成功时执行的回调。这种执行方式,使得异步调用变得十分顺手。


原理剖析

 

那么类似这种功能的

Promise
怎么实现呢?其实按照上面一句话,实现一个最基础的雏形还是很easy的。

 

极简promise雏形

 

[code]function Promise(fn) {
var value = null,
callbacks = [];  //callbacks为数组,因为可能同时有很多个回调

this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
};

function resolve(value) {
callbacks.forEach(function (callback) {
callback(value);
});
}

fn(resolve);
}

 

上述代码很简单,大致的逻辑是这样的:

 

  1. 调用
    then
    方法,将想要在
    Promise
    异步操作成功时执行的回调放入
    callbacks
    队列,其实也就是注册回调函数,可以向观察者模式方向思考;
  2. 创建
    Promise
    实例时传入的函数会被赋予一个函数类型的参数,即
    resolve
    ,它接收一个参数value,代表异步操作返回的结果,当一步操作执行成功后,用户会调用
    resolve
    方法,这时候其实真正执行的操作是将
    callbacks
    队列中的回调一一执行;

 

可以结合

例1
中的代码来看,首先
new Promise
时,传给
promise
的函数发送异步请求,接着调用
promise
对象的
then
属性,注册请求成功的回调函数,然后当异步请求发送成功时,调用
resolve(results.id)
方法, 该方法执行
then
方法注册的回调数组。

 

相信仔细的人应该可以看出来,

then
方法应该能够链式调用,但是上面的最基础简单的版本显然无法支持链式调用。想让
then
方法支持链式调用,其实也是很简单的:

[code]this.then = function (onFulfilled) {
callbacks.push(onFulfilled);
return this;
};

 

see?只要简单一句话就可以实现类似下面的链式调用:

 

[code]// 例2
getUserId().then(function (id) {
// 一些处理
}).then(function (id) {
// 一些处理
});

 


加入延时机制

 

细心的同学应该发现,上述代码可能还存在一个问题:如果在

then
方法注册回调之前,
resolve
函数就执行了,怎么办?比如
promise
内部的函数是同步函数:

 

[code]// 例3
function getUserId() {
return new Promise(function (resolve) {
resolve(9876);
});
}
getUserId().then(function (id) {
// 一些处理
});

 

这显然是不允许的,

Promises/A+
规范明确要求回调需要通过异步方式执行,用以保证一致可靠的执行顺序。因此我们要加入一些处理,保证在
resolve
执行之前,
then
方法已经注册完所有的回调。我们可以这样改造下
resolve
函数:

 

[code]function resolve(value) {
setTimeout(function() {
callbacks.forEach(function (callback) {
callback(value);
});
}, 0)
}

 

上述代码的思路也很简单,就是通过

setTimeout
机制,将
resolve
中执行回调的逻辑放置到
JS
任务队列末尾,以保证在
resolve
执行时,
then
方法的回调函数已经注册完成.

 

但是,这样好像还存在一个问题,可以细想一下:如果

Promise
异步操作已经成功,这时,在异步操作成功之前注册的回调都会执行,但是在
Promise
异步操作成功这之后调用的
then
注册的回调就再也不会执行了,这显然不是我们想要的。


加入状态

 

恩,为了解决上一节抛出的问题,我们必须加入状态机制,也就是大家熟知的

pending
fulfilled
rejected

 

Promises/A+
规范中的2.1
Promise States
中明确规定了,
pending
可以转化为
fulfilled
rejected
并且只能转化一次,也就是说如果
pending
转化到
fulfilled
状态,那么就不能再转化到
rejected
。并且
fulfilled
rejected
状态只能由
pending
转化而来,两者之间不能互相转换。一图胜千言:

 

 

改进后的代码是这样的:

 

[code]function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];

this.then = function (onFulfilled) {
if (state === 'pending') {
callbacks.push(onFulfilled);
return this;
}
onFulfilled(value);
return this;
};

function resolve(newValue) {
value = newValue;
state = 'fulfilled';
setTimeout(function () {
callbacks.forEach(function (callback) {
callback(value);
});
}, 0);
}

fn(resolve);
}

 

上述代码的思路是这样的:

resolve
执行时,会将状态设置为
fulfilled
,在此之后调用
then
添加的新回调,都会立即执行。

 

这里没有任何地方将

state
设为
rejected
,为了让大家聚焦在核心代码上,这个问题后面会有一小节专门加入。


链式Promise

 

那么这里问题又来了,如果用户再then函数里面注册的仍然是一个

Promise
,该如何解决?比如下面的
例4

 

[code]// 例4
getUserId()
.then(getUserJobById)
.then(function (job) {
// 对job的处理
});

function getUserJobById(id) {
return new Promise(function (resolve) {
http.get(baseUrl + id, function(job) {
resolve(job);
});
});
}

 

这种场景相信用过

promise
的人都知道会有很多,那么类似这种就是所谓的链式
Promise

 

链式

Promise
是指在当前
promise
达到
fulfilled
状态后,即开始进行下一个
promise
(后邻
promise
)。那么我们如何衔接当前
promise
和后邻
promise
呢?(这是这里的难点)。

 

其实也不是辣么难,只要在

then
方法里面
return
一个
promise
就好啦。
Promises/A+
规范中的2.2.7就是这么说哒(微笑脸)~

 

下面来看看这段暗藏玄机的

then
方法和
resolve
方法改造代码:

[code]function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];

this.then = function (onFulfilled) {
return new Promise(function (resolve) {
handle({
onFulfilled: onFulfilled || null,
resolve: resolve
});
});
};

function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}
//如果then中没有传递任何东西
if(!callback.onFulfilled) {
callback.resolve(value);
return;
}

var ret = callback.onFulfilled(value);
callback.resolve(ret);
}

function resolve(newValue) {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve);
return;
}
}
state = 'fulfilled';
value = newValue;
setTimeout(function () {
callbacks.forEach(function (callback) {
handle(callback);
});
}, 0);
}

fn(resolve);
}

 

我们结合

例4
的代码,分析下上面的代码逻辑,为了方便阅读,我把
例4
的代码贴在这里:

[code]// 例4
getUserId()
.then(getUserJobById)
.then(function (job) {
// 对job的处理
});

function getUserJobById(id) {
return new Promise(function (resolve) {
http.get(baseUrl + id, function(job) {
resolve(job);
});
});
}

 

  1. then
    方法中,创建并返回了新的
    Promise
    实例,这是串行
    Promise
    的基础,并且支持链式调用。
  2. handle
    方法是
    promise
    内部的方法。
    then
    方法传入的形参
    onFulfilled
    以及创建新
    Promise
    实例时传入的
    resolve
    均被
    push
    到当前
    promise
    callbacks
    队列中,这是衔接当前
    promise
    和后邻
    promise
    的关键所在(这里一定要好好的分析下handle的作用)。
  3. getUserId
    生成的
    promise
    (简称
    getUserId promise
    )异步操作成功,执行其内部方法
    resolve
    ,传入的参数正是异步操作的结果
    id
  4. 调用
    handle
    方法处理
    callbacks
    队列中的回调:
    getUserJobById
    方法,生成新的
    promise
    getUserJobById promise
  5. 执行之前由
    getUserId promise
    then
    方法生成的新
    promise
    (称为
    bridge promise
    )的
    resolve
    方法,传入参数为
    getUserJobById promise
    。这种情况下,会将该
    resolve
    方法传入
    getUserJobById promise
    then
    方法中,并直接返回。
  6. getUserJobById promise
    异步操作成功时,执行其
    callbacks
    中的回调:
    getUserId bridge promise
    中的
    resolve
    方法
  7. 最后执行
    getUserId bridge promise
    的后邻
    promise
    callbacks
    中的回调。

 

更直白的可以看下面的图,一图胜千言(都是根据自己的理解画出来的,如有不对欢迎指正):

 

 

失败处理

 

在异步操作失败时,标记其状态为

rejected
,并执行注册的失败回调:

 

[code]//例5
function getUserId() {
return new Promise(function(resolve) {
//异步请求
http.get(url, function(error, results) {
if (error) {
reject(error);
}
resolve(results.id)
})
})
}

getUserId().then(function(id) {
//一些处理
}, function(error) {
console.log(error)
})

 

有了之前处理

fulfilled
状态的经验,支持错误处理变得很容易,只需要在注册回调、处理状态变更上都要加入新的逻辑:

 

[code]function Promise(fn) {
var state = 'pending',
value = null,
callbacks = [];

this.then = function (onFulfilled, onRejected) {
return new Promise(function (resolve, reject) {
handle({
onFulfilled: onFulfilled || null,
onRejected: onRejected || null,
resolve: resolve,
reject: reject
});
});
};

function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}

var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? callback.resolve : callback.reject;
cb(value);
return;
}
ret = cb(value);
callback.resolve(ret);
}

function resolve(newValue) {
if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
var then = newValue.then;
if (typeof then === 'function') {
then.call(newValue, resolve, reject);
return;
}
}
state = 'fulfilled';
value = newValue;
execute();
}

function reject(reason) {
state = 'rejected';
value = reason;
execute();
}

function execute() {
setTimeout(function () {
callbacks.forEach(function (callback) {
handle(callback);
});
}, 0);
}

fn(resolve, reject);
}

 

上述代码增加了新的

reject
方法,供异步操作失败时调用,同时抽出了
resolve
reject
共用的部分,形成
execute
方法。

错误冒泡是上述代码已经支持,且非常实用的一个特性。在

handle
中发现没有指定异步操作失败的回调时,会直接将
bridge promise
(
then
函数返回的
promise
,后同)设为
rejected
状态,如此达成执行后续失败回调的效果。这有利于简化串行
Promise
的失败处理成本,因为一组异步操作往往会对应一个实际功能,失败处理方法通常是一致的:

 

[code]//例6
getUserId()
.then(getUserJobById)
.then(function (job) {
// 处理job
}, function (error) {
// getUserId或者getUerJobById时出现的错误
console.log(error);
});

 

异常处理

 

细心的同学会想到:如果在执行成功回调、失败回调时代码出错怎么办?对于这类异常,可以使用

try-catch
捕获错误,并将
bridge promise
设为
rejected
状态。
handle
方法改造如下:

 

[code]function handle(callback) {
if (state === 'pending') {
callbacks.push(callback);
return;
}

var cb = state === 'fulfilled' ? callback.onFulfilled : callback.onRejected,
ret;
if (cb === null) {
cb = state === 'fulfilled' ? callback.resolve : callback.reject;
cb(value);
return;
}
try {
ret = cb(value);
callback.resolve(ret);
} catch (e) {
callback.reject(e);
}
}

 

如果在异步操作中,多次执行

resolve
或者
reject
会重复处理后续回调,可以通过内置一个标志位解决。

总结

 

刚开始看promise源码的时候总不能很好的理解then和resolve函数的运行机理,但是如果你静下心来,反过来根据执行promise时的逻辑来推演,就不难理解了。这里一定要注意的点是:promise里面的then函数仅仅是注册了后续需要执行的代码,真正的执行是在resolve方法里面执行的,理清了这层,再来分析源码会省力的多。

 

现在回顾下Promise的实现过程,其主要使用了设计模式中的观察者模式:

 

  1. 通过Promise.prototype.then和Promise.prototype.catch方法将观察者方法注册到被观察者Promise对象中,同时返回一个新的Promise对象,以便可以链式调用。
  2. 被观察者管理内部pending、fulfilled和rejected的状态转变,同时通过构造函数中传递的resolve和reject方法以主动触发状态转变和通知观察者。

http://www.cgpwyj.cn/
http://www.peacemind.com.cn/
http://www.tasknet.com.cn/
http://www.metroworld.com.cn/
http://www.cngodo.cn/
http://www.gzrdbp.cn/
http://www.dnapt.cn/
http://www.zgxxyp.cn/
http://www.sjjdvr.cn/
http://www.sujinkeji.cn/
http://www.zsjxbd.cn/
http://www.yesgas.cn/
http://www.quickpass.sh.cn/
http://www.jspcrm.cn/
http://www.yjdwpt.cn/
http://www.henanwulian.cn/
http://www.hhrshh.cn/
http://www.gpgold.cn/
http://www.jingzhuiyou.cn/
http://www.ownbar.cn/
http://www.shtarchao.net.cn/

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: