手把手教你使用ts 一步一步的去完成一个Promise
栏目: JavaScript · 发布时间: 5年前
内容简介:笔者公司前端小组,线上出现了因为Promise用错了而导致的问题。 本文的出发点不仅是了解Promise,主要目的是跟着特性去写一个Promise。 所以前提是你已经掌握了Promise的基本特性,如果没有接触过的话,请点击学习内部三种状态分别为pending、fullfiled、rejected,初始化状态为pending。状态变化可以从pending转化fullfiled或rejected。无其他转化方式。解决ajax回调地狱。
前奏
笔者公司前端小组,线上出现了因为Promise用错了而导致的问题。 本文的出发点不仅是了解Promise,主要目的是跟着特性去写一个Promise。 所以前提是你已经掌握了Promise的基本特性,如果没有接触过的话,请点击学习
正文
熟悉es6的Promise特性。
1.特性概览
-
链式调用。
-
内部三种状态。
内部三种状态分别为pending、fullfiled、rejected,初始化状态为pending。状态变化可以从pending转化fullfiled或rejected。无其他转化方式。
2.出现的背景
解决ajax回调地狱。
ajax回调地狱,本质是希望不可控制的异步变成同步形式。如:
ajax1({...,success(res1){ ... }}) ajax2({...,params:{res1},success(res2){ // error no res1 ... }}) 复制代码
当我们的ajax2需要用到ajax1的时候,我们不得不使用嵌套式:
ajax1({...,success(res1){ ajax2({...,params:{res1},success(res2){ /// doing something }}) }}) 复制代码
这种写法的最大问题就是当嵌套层数很多当时候,代码会变得难以维护。
那么 how is Promise的写法 ajax改造:
p1 = function() { return new Promise(function(resolve){ ajax1({...,success(res1){ resolve(res1) }}) }) } p2 = function(){ return new Promise(function(resolve){ ajax2({...,params:{res1},success(res2){ /// doing something }}) }) } 复制代码
那么最终的写法则变成
p1().then(res=>{ return p2() }).then(res2=>{ // doing something }) 复制代码
3 具体特性。
- 1.了解特性首推官方的Promise A+,点击
- 2.然后是用法,阮一峰大大的es6 Promise特别详细。点击
这里根据Promise A+ 把接下来的几个定义函数做在约定。
1.new Promise().then(),传递的函数分别叫 onFulfilled、onRejected
开始手写
1.Promise是个函数
所以我们最先开始的就是传递一个函数
function Promise(executor) { if( !isFunc(executor) ){ throw 'Promise2 传递的参数不为functon!!!'; } } 复制代码
2.Promise 初始状态。
promise有三种状态,可以用ts的枚举
enum pStatus { pending = 'pending', fulled = 'fullfilled', rejected = 'rejected' } 复制代码
然后我们需要定义一些属性。
function Promise() { if( !isFunc(executor) ){ throw 'Promise2 传递的参数不为functon!!!'; } this.status = pStatus.pending; // 默认状态 this.resovlecbs = []; // 回调的resolve函数 主要来自于Promise.prototype.then this.rejectcbs = []; // 回调的reject函数 主要来自于Promise.prototype.then this.value; // 记录的resolve值 this.error; // 记录的reject值 } 复制代码
我们知道Promise传递的函数,是直接会在主线程的执行的,所以我们需要直接执行它。
function Promise() { if( !isFunc(executor) ){ throw 'Promise2 传递的参数不为functon!!!'; } this.status = pStatus.pending; // 默认状态 this.resovlecbs = []; // 回调的resolve函数 主要来自于Promise.prototype.then this.rejectcbs = []; // 回调的reject函数 主要来自于Promise.prototype.then this.value; // 记录的resolve值 this.error; // 记录的reject值 try { executor(resolve,reject); // 传递的函数的执行。 } catch (error) { reject(error); // 捕获的异常会直接执行reject。 } } 复制代码
3.开始Promise的链式结构,达到异步结果变同步,解决回调地狱。
首先需要说明的是链式的结构的原理是不断返回新的Promise,也就是说then的结果是
Promise.prototype.then = function() { return new Promise(function(resolve,reject){ xxx... }) } 复制代码
首先我们需要明确,Promise具体特性是什么?
1.then 传入的值分别是 resolve的回调和 reject状态回调。
2.传递值,将上一个then的值一直往下传。
3.符合同层先来先到,异层必定上层先执行的策略。
为了了解第三个特性的详细意思之前,让我们看一个例子:
var p1 = new Promise(function(resolve,reject){ resolve('p1') }); var p2 = new Promise(function(resolve,reject){ resolve('p2') }); p1.then(()=>{ console.log('p11') }).then(()=>{ console.log('p12') }) p2.then(()=>{ console.log('p21') }).then(()=>{ console.log('p22') }) 复制代码
相信大家都知道顺序为 p11 => p21 => p12 => p22。原因的话涉及到宏微任务的特性,请参考这篇文章,点击学习。 想必大家已经明白第三点了。
那么如何实现?
分步骤:
1) 传递给Promise的函数,完成后我们才会执行then传递的函数。也就是
new Promise(function(resolve,reject){ resolve('xxx') // (1)只有执行完这个才会执行后面then的 onFulfilled函数 }).then(function(){ ...xxx // (2)这是第二步 }) 复制代码
所以then传递onFulfilled函数和onRejected函数都是在resolve中执行的。所以then其实只是去保存then传递的函数而已,而保存的地方则是Promise主函数内部的resolvecbs和rejectcbs这两个数组。
我觉得可能会有人问为什么会是数组?
因为你可能会这么写:
var p = new Promise(...); p.then(function(){ ... },...); p.then(function(){ ... },...); p.then(function(){ ... },...); 复制代码
这种非链路,其实都是把onFullfilled,保存到Promise内部,所以需要数组。
然后就是Promise内部到resolve函数和reject函数。这两个函数会做为 用户传入的函数的参数传入。 本质内部就是去遍历执行reslovecbs的函数项。并且改变状态,还有就是将传入的值记录下来,这些值会传给onFullfilled,并由onFullfilled决定是否要继续传递下去。也即是:
then(function onFullfilled(value){ // value 来自于resolve传递的参数。 return value // return 则表示续传 下一个then是否能拿到。 }) 复制代码
我们添加下,大致如下:
function Promise() { if( !isFunc(executor) ){ throw 'Promise2 传递的参数不为functon!!!'; } this.status = pStatus.pending; // 默认状态 this.resovlecbs = []; // 回调的resolve函数 主要来自于Promise.prototype.then this.rejectcbs = []; // 回调的reject函数 主要来自于Promise.prototype.then this.value; // 记录的resolve值 this.error; // 记录的reject值 const resolve = (value:object)=>{ // resolve做的三件事 this.value = value; // 记录值 then 的 onFullfilled会用它 this.resovlecbs.forEach((item:Function)=>{ item(value); // 这个就是 onFullfilled函数,会用上面的value }) this.status = pStatus.fulled; // 把状态改变为 fullfilled } // ... reject同理 try { executor(resolve,reject); } catch (error) { reject(error); } } 复制代码
resolve中执行的是resolveCbs数组存放的函数。而这些函数是来自于then推送的。 但是值得注意的是,函数除了执行then传递的onFullfiled函数和onRejected函数,还要将这两个返回的值,传递下去,所以要执行下一个Promise的resolve
,因为resolve的第一个特性就是记录值。 所以then是这样的。
Promise.prototype.then = function (onFullfilled:Function=noop,onRejected:Function=noop) { let scope = this; return new Promise(function(resolve = noop,reject = noop){ scope.resovlecbs.push((value)=>{ handlerRes(onFullfilled,value,resolve); }) scope.rejectcbs.push((error)=>{ handlerRes(onRejected,error,reject); }) }); } export function handlerRes(handler,message,next){ let res if(isFunc(handler)){ res = handler(message); } next(res); // 执行下一个函数的resolve } 复制代码
可以看到这里是把 then传递的函数onFullfilled和onRejected分别推入 实例的 resovlecbs 数组和 rejectcbs()达到resolve和onFullfilled的同步执行的效果。
且不仅是onRresolved被执行,同时被执行的还有下一个Promise的 resolve。
这样已经实现了then 链的顺序执行了。
对于构造函数new Promise(),的几个步骤是 创建一个空对象,并将Promise内部执行的所有属性都挂载到这个对象上。也就是this的所有属性。
传递都效果图如下:
但是上面的写法会有两个问题:
-
1.无法达到前面说的
3.符合同层先来先到,异层必定上层先执行的策略。
,这种效果,正式event loop的队列。 所以我们可以使用微任务或宏任务。 这里是了简化代码结构使用setTimeout来模拟,如果感兴趣可以去了解下这个npm库asap
, 点击这里 -
2.目前我们的then函数的写法是直接把函数推入到resolvecbs数组,等待resolve去执行,但是这种方式不hack,如果我们先执行了resolve后,我们在执行then。比如:
var p1 = new Promise(function(resolve,reject){ resolve('p1') }); p1.then(()=>{ console.log('p11') }). 复制代码
这时候我们会先执行resolve, 完成了resolvecbs的遍历执行,然后才去通过then,对resolvecbs进行搜集。name后面搜集的函数就永远不会执行了。所以我们必须判断状态。
hack写法:
Promise.prototype.then = function (onFullfilled:Function=noop,onRejected:Function=noop) { let scope = this; return new Promise(function(resolve = noop,reject = noop){ if(scope.status === pStatus.pending) { // pending则等待执行 scope.resovlecbs.push((value)=>{ handlerRes(onFullfilled,value,resolve); }) scope.rejectcbs.push((error)=>{ handlerRes(onRejected,error,reject); }) } else if(scope.status===pStatus.fulled) { // fullfilled则直接执行 handlerRes(onFullfilled,scope.value,resolve); } else { // rejectd 直接执行 handlerRes(onRejected,scope.error,reject); } }); } 复制代码
Promise是微任务,这里为了方便, 对Promise本身添加宏任务间隔。reject同理。
function Promise(executor:any) { if( !isFunc(executor) ){ throw 'Promise2 传递的参数不为functon!!!'; } this.status = pStatus.pending; this.resovlecbs = []; this.rejectcbs = []; this.value; this.error; const resolve = (value:object)=>{ this.value = value; setTimeout(()=>{ this.resovlecbs.forEach((item:Function)=>{ item(value); }) this.status = pStatus.fulled; },0) } const reject = (error:Error)=>{ this.error = error; setTimeout(()=>{ this.status = pStatus.rejected; if(this.rejectcbs.length ===0){ throw this.error; } else { this.rejectcbs.forEach((item:Function)=>{ item(error); }) } },0) // if(this.rejectcbs.length === 0 ) throw error; } try { executor(resolve,reject); } catch (error) { reject(error); } } 复制代码
然而这依然并非是最终版本,因为这无法解决,多次resolve会重复执行 resolvecbs的问题。 所以resolve函数的内容必须旨在pending的状态下才执行。 比如有人会这么做:
new Promise(function(resolve){ reslove('ddd') resolve('ttt') }).then(value=>{ console.log(value) }) 复制代码
为了只打印一个值,我们必须要在resolve函数做个判断,只有pending的时候会
function Promise(executor:any) { if( !isFunc(executor) ){ throw 'Promise2 传递的参数不为functon!!!'; } this.status = pStatus.pending; this.resovlecbs = []; this.rejectcbs = []; this.value; this.error; const resolve = (value:object)=>{ setTimeout(()=>{ if(this.status===pStatus.pending){ // 避免重复执行。 this.value = value; this.resovlecbs.forEach((item:Function)=>{ item(value); }) this.status = pStatus.fulled; // 状态改变 } },0) } const reject = (error:Error)=>{ setTimeout(()=>{ // why if(this.status===pStatus.pending){ // 添加了判断 避免重复执行 this.error = error; this.status = pStatus.rejected; //状态改变 if(this.rejectcbs.length ===0){ throw this.error; } else { this.rejectcbs.forEach((item:Function)=>{ item(error); }) } } },0) // if(this.rejectcbs.length === 0 ) throw error; } try { executor(resolve,reject); } catch (error) { reject(error); } } 复制代码
4.Promise的catch和finally函数。
catch函数和finally函数其实是语法糖,我们完全可以用then替代的。读者大大们思考下。。
下面给出代码:
Promise.prototype.catch = function(catchcb:Function) { return this.then(undefined, catchcb); // 本质是then } Promise.prototype.finally = function (callback) { return this.then((value)=>{ // 本质是then callback(); return value; },callback); } 复制代码
所以下面这种写法
p.then(onResolve,onReject).catch(onCatch).finally(onFinal); 复制代码
其实是等于
p.then(onResolve,onReject).then(undefined,onCatch).then(onFinal,onFinal); 复制代码
5.Promise.resolve。
阮一峰给出了这个函数的四种处理方式。
- 1.传递的是Promise,
- 2.传递的是thenable的对象 如 { then:function(){} }
- 3.传递是非thenbale的值
- 4.什么也没传。
需要注意的是Promise.resolve,传递出来的一定是promise。 笔者的写法是
Promise.resolve = function(handler){ if( isObject(handler) && 'constructor' in handler && handler.constructor=== this) { // handler 是 Promise return handler; } else if (isObject(handler) && isFunc(handler.then) ){ // thenable return new this(handler.then.bind(handler)); } else { // 非thenable return new this(function(resolve){ resolve(handler); }) } } 复制代码
可以看到如果是:
情况1,则直接原封不动的返回。
情况2则返回一个Promise,且把对象的then函数,作为参数传递进入Promise。
情况3 直接把handler resolve掉。
6.Promise.reject.
Promise.reject可不像resolve这么麻烦。完全把传递的值直接传递出来。
Promise.reject = function() { const args = Array.prototype.slice.call(arguments); return new this((resolve, reject) => reject(args.shift())); } 复制代码
6.Promise.all。
首先是用法:
const promises = [2, 3, 5, 7, 11, 13].map(function (id) { return new Promise(function(resolve,reject){ setTimeout(()=>{ resolve(id); },id); }) }); Promise.all(promises).then(function (posts) { console.log(posts); }) 复制代码
首先传入的是数组。其次就是所有的数组,其次就是数组存在的Promise全部都执行完后才会进入all的 then函数中。 所以需要一个标记记录实时记录所有已经完成的promise。
然后就是传入的数组 可能有promise也有可能传递的并非是Promise,所以需要hack。 区分是否存在then函数。
Promise.all = function(arr) { if( !isArray(arr) ){ throw 'all函数 传递的参数不为Array!!!'; } let args = Array.prototype.slice.call(arr); let resArr = Array.call(null,Array(arr.length)).map(()=>null); // 记录所有的结果 let handlerNum = 0; // 处理标记 return new this((resolve,reject)=>{ for(let i = 0;i<args.length;i++){ let ifunc = args[i]; if(ifunc && isFunc(ifunc.then) ) { //是否存在then函数。 ifunc.then(value=>{ resArr[i] = value; handlerNum ++; // 标记添加 if(handlerNum>=arr.length){ // 彻底完成 resolve(resArr) // 完成后的数组 } },error=>{ reject(error); }); } else { // 非thenable resArr[i] = ifunc; handlerNum ++; // 标记添加 if(handlerNum>=arr.length){ // 彻底完成 resolve(resArr) // 完成后的数组 } } } }); } 复制代码
7. Promise.race。
直接上代码吧,大致就是跑的最快的会作为结果传回
Promise2.race = function(arr) { if( !isArray(arr) ){ throw 'race函数 传递的参数不为Array!!!'; } let args = Array.prototype.slice.call(arr); let hasResolve = false; return new this((resolve,reject)=>{ for(let i = 0;i<args.length;i++){ let ifunc = args[i]; if(ifunc && isFunc(ifunc.then) ) { ifunc.then(value=>{ !hasResolve && resolve(value) },error=>{ !hasResolve && reject(error); }); } else { hasResolve = true; !hasResolve && resolve(ifunc) } } }) } 复制代码
分析同事的问题。
源代码大致如下
let test = function() { return new Promise((resolve,reject)=>{ reject(new Error('test')) }) } Promise.resolve('new').then(res=>{ test().then(res2=>{ ... }) }).catch(err=>{ // use err console.log(err) }) 复制代码
遇到的问题是,最后的catch里面拿不到err。
文中我们已经说过,catch只是then的语法糖,而then的值的传递,是靠onFullfilled的return 和 onRejected的return 传递了。问题这是在then里面缺少了return。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:- 使用geohash完成地理距离计算
- 使用装饰器完成用户身份验证
- 使用 Reactor 完成类似 Flink 的操作
- 使用REST规范从未完成的5件事
- 使用 Reactor 完成类似的 Flink 的操作
- 使用 Bootstrap Token 完成 TLS Bootstrapping
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
jQuery基础教程 (第4版)
[美] Jonathan Chaffer、[美] Karl Swedberg / 李松峰 / 人民邮电出版社 / 2013-10 / 59.00
本书由jQuery API网站维护者亲自撰写,第一版自2008上市以来,一版再版,累计重印14次,是国内首屈一指的jQuery经典著作! 作为最新升级版,本书涵盖jQuery 1.10.x和jQuery 2.0.x。本书前6章以通俗易懂的方式讲解了jQuery的核心组件,包括jQuery的选择符、事件、动画、DOM操作、Ajax支持等。第7章和第8章介绍了jQuery UI、jQuery M......一起来看看 《jQuery基础教程 (第4版)》 这本书的介绍吧!
HTML 压缩/解压工具
在线压缩/解压 HTML 代码
MD5 加密
MD5 加密工具