promise与setTimeout的执行顺序问题

有一次在面试题中有做到promise与setTimeout的执行顺序,当时有点懵,执行顺序还是弄错了一点点,这里记录下1.输出setTimeout(function() { console.log(111)}, 0);setTimeout(function() { console.log(333)}, 1000);new Promise(function(resolve){ console.log(444); resolve(); console.log(555);}).then(function(){ console.log(666);});console.log(777);async function test1() { console.log(“test1”); await test2(); console.log(“test1 last”);}async function test2() { console.log(“test2”);}test1();输出结果2.个人理解首先执行同步代码,然后以事件轮询的方式执行异步代码promise中的异步体现在.then()和.catch()中而promise中的function里的是同步代码上面的代码是先执行promise里的同步代码,然后执行脚本里本身的同步代码async无论方法是同步还是异步都可以用async关键字来进行标识因为用async标识只是显示表明在该方法内,可能会用到await关键字使其变为异步方法,而且将该异步方法进行了明确的划分,只有用了await关键字时才是异步操作,其余一并为同步操作同 Generator 函数一样,async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句await 命令后面的 Promise 对象,运行结果可能是 rejected,所以最好把 await 命令放在 try…catch 代码块中3.其他在网上还找到了一些资料参考了这篇文章的一些内容 参考文章setImmediate(function(){ console.log(1);},0);setTimeout(function(){ console.log(2);},0);new Promise(function(resolve){ console.log(3); resolve(); console.log(4);}).then(function(){ console.log(5);});console.log(6);process.nextTick(function(){ console.log(7);});console.log(8);输出结果: 3 4 6 8 7 5 2 1macro-task: script (整体代码),setTimeout, setInterval, setImmediate, I/O, UI rendering. micro-task: process.nextTick, Promise(原生),Object.observe,MutationObserver第一步. script整体代码被执行,执行过程为创建setImmediate macro-task创建setTimeout macro-task创建micro-task Promise.then 的回调,并执行script console.log(3); resolve(); console.log(4); 此时输出3和4,虽然resolve调用了,执行了但是整体代码还没执行完,无法进入Promise.then 流程。console.log(6)输出6process.nextTick 创建micro-taskconsole.log(8) 输出8 第一个过程过后,已经输出了3 4 6 8第二步. 由于其他micro-task 的 优先级高于macro-task。此时micro-task 中有两个任务按照优先级process.nextTick 高于 Promise,所以先输出7,再输出5第三步,micro-task 任务列表已经执行完毕,家下来执行macro-task. 由于setTimeout的优先级高于setIImmediate,所以先输出2,再输出1。优先级: promise.Trick()>promise的回调>setTimeout>setImmediate正在努力学习中,若对你的学习有帮助,留下你的印记呗(点个赞咯^_^)往期好文推荐:判断iOS和Android及PC端纯css实现瀑布流(multi-column多列及flex布局)实现多行文字及单行的省略号微信小程序之购物车和父子组件传值及calc的注意事项 ...

November 20, 2018 · 1 min · jiezi

util.promisify 的那些事儿

util.promisify是在node.js 8.x版本中新增的一个工具,用于将老式的Error first callback转换为Promise对象,让老项目改造变得更为轻松。 在官方推出这个工具之前,民间已经有很多类似的工具了,比如es6-promisify、thenify、bluebird.promisify。 以及很多其他优秀的工具,都是实现了这样的功能,帮助我们在处理老项目的时候,不必费神将各种代码使用Promise再重新实现一遍。工具实现的大致思路首先要解释一下这种工具大致的实现思路,因为在Node中异步回调有一个约定:Error first,也就是说回调函数中的第一个参数一定要是Error对象,其余参数才是正确时的数据。 知道了这样的规律以后,工具就很好实现了,在匹配到第一个参数有值的情况下,触发reject,其余情况触发resolve,一个简单的示例代码:function util (func) { return (…arg) => new Promise((resolve, reject) => { func(…arg, (err, arg) => { if (err) reject(err) else resolve(arg) }) })}调用工具函数返回一个匿名函数,匿名函数接收原函数的参数。匿名函数被调用后根据这些参数来调用真实的函数,同时拼接一个用来处理结果的callback。检测到err有值,触发reject,其他情况触发resolveresolve 只能传入一个参数,所以callback中没有必要使用…arg获取所有的返回值常规的使用方式拿一个官方文档中的示例const { promisify } = require(‘util’)const fs = require(‘fs’)const statAsync = promisify(fs.stat)statAsync(’.’).then(stats => { // 拿到了正确的数据}, err => { // 出现了异常})以及因为是Promise,我们可以使用await来进一步简化代码:const { promisify } = require(‘util’)const fs = require(‘fs’)const statAsync = promisify(fs.stat)// 假设在 async 函数中try { const stats = await statAsync(’.’) // 拿到正确结果} catch (e) { // 出现异常}用法与其他工具并没有太大的区别,我们可以很轻易的将回调转换为Promise,然后应用于新的项目中。自定义的 Promise 化有那么一些场景,是不能够直接使用promisify来进行转换的,有大概这么两种情况:没有遵循Error first callback约定的回调函数返回多个参数的回调函数首先是第一个,如果没有遵循我们的约定,很可能导致reject的误判,得不到正确的反馈。 而第二项呢,则是因为Promise.resolve只能接收一个参数,多余的参数会被忽略。 所以为了实现正确的结果,我们可能需要手动实现对应的Promise函数,但是自己实现了以后并不能够确保使用方不会针对你的函数调用promisify。 所以,util.promisify还提供了一个Symbol类型的key,util.promisify.custom。 Symbol类型的大家应该都有了解,是一个唯一的值,这里是util.prosimify用来指定自定义的Promise化的结果的,使用方式如下:const { promisify } = require(‘util’)// 比如我们有一个对象,提供了一个返回多个参数的回调版本的函数const obj = { getData (callback) { callback(null, ‘Niko’, 18) // 返回两个参数,姓名和年龄 }}// 这时使用promisify肯定是不行的// 因为Promise.resolve只接收一个参数,所以我们只会得到 Nikopromisify(obj.getData)().then(console.log) // Niko// 所以我们需要使用 promisify.custom 来自定义处理方式obj.getData[promisify.custom] = async () => ({ name: ‘Niko’, age: 18 })// 当然了,这是一个曲线救国的方式,无论如何 Promise 不会返回多个参数过来的promisify(obj.getData)().then(console.log) // { name: ‘Niko’, age: 18 }关于Promise为什么不能resolve多个值,我有一个大胆的想法,一个没有经过考证,强行解释的理由:如果能resolve多个值,你让async函数怎么return(当个乐子看这句话就好,不要当真) 不过应该确实跟return有关,因为Promise是可以链式调用的,每个Promise中执行then以后都会将其返回值作为一个新的Promise对象resolve的值,在JavaScript中并没有办法return多个参数,所以即便第一个Promise可以返回多个参数,只要经过return的处理就会丢失 在使用上就是很简单的针对可能会被调用promisify的函数上添加promisify.custom对应的处理即可。 当后续代码调用promisify时就会进行判断:如果目标函数存在promisify.custom属性,则会判断其类型:如果不是一个可执行的函数,抛出异常如果是可执行的函数,则直接返回其对应的函数如果目标函数不存在对应的属性,按照Error first callback的约定生成对应的处理函数然后返回添加了这个custom属性以后,就不用再担心使用方针对你的函数调用promisify了。 而且可以验证,赋值给custom的函数与promisify返回的函数地址是一处:obj.getData[promisify.custom] = async () => ({ name: ‘Niko’, age: 18 })// 上边的赋值为 async 函数也可以改为普通函数,只要保证这个普通函数会返回 Promise 实例即可// 这两种方式与上边的 async 都是完全相等的obj.getData[promisify.custom] = () => Promise.resolve({ name: ‘Niko’, age: 18 })obj.getData[promisify.custom] = () => new Promise(resolve({ name: ‘Niko’, age: 18 }))console.log(obj.getData[promisify.custom] === promisify(obj.getData)) // true一些内置的 custom 处理在一些内置包中,也能够找到promisify.custom的踪迹,比如说最常用的child_process.exec就内置了promisify.custom的处理:const { exec } = require(‘child_process’)const { promisify } = require(‘util’)console.log(typeof exec[promisify.custom]) // function因为就像前边示例中所提到的曲线救国的方案,官方的做法也是将函数签名中的参数名作为key,将其所有参数存放到一个Object对象中进行返回,比如child_process.exec的返回值抛开error以外会包含两个,stdout和stderr,一个是命令执行后的正确输出,一个是命令执行后的错误输出:promisify(exec)(’ls’).then(console.log)// -> { stdout: ‘XXX’, stderr: ’’ }或者我们故意输入一些错误的命令,当然了,这个只能在catch模块下才能够捕捉到,一般命令正常执行stderr都会是一个空字符串:promisify(exec)(’lss’).then(console.log, console.error)// -> { …, stdout: ‘’, stderr: ’lss: command not found’ }包括像setTimeout、setImmediate也都实现了对应的promisify.custom。 之前为了实现sleep的操作,还手动使用Promise封装了setTimeout:const sleep = promisify(setTimeout)console.log(new Date())await sleep(1000)console.log(new Date())内置的 promisify 转换后函数如果你的Node版本使用10.x以上的,还可以从很多内置的模块中找到类似.promises的子模块,这里边包含了该模块中常用的回调函数的Promise版本(都是async函数),无需再手动进行promisify转换了。 而且我本人觉得这是一个很好的指引方向,因为之前的工具实现,有的选择直接覆盖原有函数,有的则是在原有函数名后边增加Async进行区分,官方的这种在模块中单独引入一个子模块,在里边实现Promise版本的函数,其实这个在使用上是很方便的,就拿fs模块进行举例:// 之前引入一些 fs 相关的 API 是这样做的const { readFile, stat } = require(‘fs’)// 而现在可以很简单的改为const { readFile, stat } = require(‘fs’).promises// 或者const { promises: { readFile, stat } } = require(‘fs’)后边要做的就是将调用promisify相关的代码删掉即可,对于其他使用API的代码来讲,这个改动是无感知的。 所以如果你的node版本够高的话,可以在使用内置模块之前先去翻看文档,有没有对应的promises支持,如果有实现的话,就可以直接使用。promisify 的一些注意事项一定要符合Error first callback的约定不能返回多个参数注意进行转换的函数是否包含this的引用前两个问题,使用前边提到的promisify.custom都可以解决掉。 但是第三项可能会在某些情况下被我们所忽视,这并不是promisify独有的问题,就一个很简单的例子:const obj = { name: ‘Niko’, getName () { return this.name }}obj.getName() // Nikoconst func = obj.getNamefunc() // undefined类似的,如果我们在进行Promise转换的时候,也是类似这样的操作,那么可能会导致生成后的函数this指向出现问题。 修复这样的问题有两种途径:使用箭头函数,也是推荐的做法在调用promisify之前使用bind绑定对应的this不过这样的问题也是建立在promisify转换后的函数被赋值给其他变量的情况下会发生。 如果是类似这样的代码,那么完全不必担心this指向的问题:const obj = { name: ‘Niko’, getName (callback) { callback(null, this.name) }}// 这样的操作是不需要担心 this 指向问题的obj.XXX = promisify(obj.getName)// 如果赋值给了其他变量,那么这里就需要注意 this 的指向了const func = promisify(obj.getName) // 错误的 this小结个人认为Promise作为当代javaScript异步编程中最核心的一部分,了解如何将老旧代码转换为Promise是一件很有意思的事儿。 而我去了解官方的这个工具,原因是在搜索Redis相关的Promise版本时看到了这个readme:This package is no longer maintained. node_redis now includes support for promises in core, so this is no longer needed.然后跳到了node_redis里边的实现方案,里边提到了util.promisify,遂抓过来研究了一下,感觉还挺有意思,总结了下分享给大家。参考资料util.promisifychild_process.execfs.promises ...

October 18, 2018 · 2 min · jiezi

ES6 系列之我们来聊聊 Promise

前言Promise 的基本使用可以看阮一峰老师的 《ECMAScript 6 入门》。我们来聊点其他的。回调说起 Promise,我们一般都会从回调或者回调地狱说起,那么使用回调到底会导致哪些不好的地方呢?1. 回调嵌套使用回调,我们很有可能会将业务代码写成如下这种形式:doA( function(){ doB(); doC( function(){ doD(); } ) doE();} );doF();当然这是一种简化的形式,经过一番简单的思考,我们可以判断出执行的顺序为:doA()doF()doB()doC()doE()doD()然而在实际的项目中,代码会更加杂乱,为了排查问题,我们需要绕过很多碍眼的内容,不断的在函数间进行跳转,使得排查问题的难度也在成倍增加。当然之所以导致这个问题,其实是因为这种嵌套的书写方式跟人线性的思考方式相违和,以至于我们要多花一些精力去思考真正的执行顺序,嵌套和缩进只是这个思考过程中转移注意力的细枝末节而已。当然了,与人线性的思考方式相违和,还不是最糟糕的,实际上,我们还会在代码中加入各种各样的逻辑判断,就比如在上面这个例子中,doD() 必须在 doC() 完成后才能完成,万一 doC() 执行失败了呢?我们是要重试 doC() 吗?还是直接转到其他错误处理函数中?当我们将这些判断都加入到这个流程中,很快代码就会变得非常复杂,以至于无法维护和更新。2. 控制反转正常书写代码的时候,我们理所当然可以控制自己的代码,然而当我们使用回调的时候,这个回调函数是否能接着执行,其实取决于使用回调的那个 API,就比如:// 回调函数是否被执行取决于 buy 模块import {buy} from ‘./buy.js’;buy(itemData, function(res) { console.log(res)});对于我们经常会使用的 fetch 这种 API,一般是没有什么问题的,但是如果我们使用的是第三方的 API 呢?当你调用了第三方的 API,对方是否会因为某个错误导致你传入的回调函数执行了多次呢?为了避免出现这样的问题,你可以在自己的回调函数中加入判断,可是万一又因为某个错误这个回调函数没有执行呢?万一这个回调函数有时同步执行有时异步执行呢?我们总结一下这些情况:回调函数执行多次回调函数没有执行回调函数有时同步执行有时异步执行对于这些情况,你可能都要在回调函数中做些处理,并且每次执行回调函数的时候都要做些处理,这就带来了很多重复的代码。回调地狱我们先看一个简单的回调地狱的示例。现在要找出一个目录中最大的文件,处理步骤应该是:用 fs.readdir 获取目录中的文件列表;循环遍历文件,使用 fs.stat 获取文件信息比较找出最大文件;以最大文件的文件名为参数调用回调。代码为:var fs = require(‘fs’);var path = require(‘path’);function findLargest(dir, cb) { // 读取目录下的所有文件 fs.readdir(dir, function(er, files) { if (er) return cb(er); var counter = files.length; var errored = false; var stats = []; files.forEach(function(file, index) { // 读取文件信息 fs.stat(path.join(dir, file), function(er, stat) { if (errored) return; if (er) { errored = true; return cb(er); } stats[index] = stat; // 事先算好有多少个文件,读完 1 个文件信息,计数减 1,当为 0 时,说明读取完毕,此时执行最终的比较操作 if (–counter == 0) { var largest = stats .filter(function(stat) { return stat.isFile() }) .reduce(function(prev, next) { if (prev.size > next.size) return prev return next }) cb(null, files[stats.indexOf(largest)]) } }) }) })}使用方式为:// 查找当前目录最大的文件findLargest(’./’, function(er, filename) { if (er) return console.error(er) console.log(’largest file was:’, filename)});你可以将以上代码复制到一个比如 index.js 文件,然后执行 node index.js 就可以打印出最大的文件的名称。看完这个例子,我们再来聊聊回调地狱的其他问题:1.难以复用回调的顺序确定下来之后,想对其中的某些环节进行复用也很困难,牵一发而动全身。举个例子,如果你想对 fs.stat 读取文件信息这段代码复用,因为回调中引用了外层的变量,提取出来后还需要对外层的代码进行修改。2.堆栈信息被断开我们知道,JavaScript 引擎维护了一个执行上下文栈,当函数执行的时候,会创建该函数的执行上下文压入栈中,当函数执行完毕后,会将该执行上下文出栈。如果 A 函数中调用了 B 函数,JavaScript 会先将 A 函数的执行上下文压入栈中,再将 B 函数的执行上下文压入栈中,当 B 函数执行完毕,将 B 函数执行上下文出栈,当 A 函数执行完毕后,将 A 函数执行上下文出栈。这样的好处在于,我们如果中断代码执行,可以检索完整的堆栈信息,从中获取任何我们想获取的信息。可是异步回调函数并非如此,比如执行 fs.readdir 的时候,其实是将回调函数加入任务队列中,代码继续执行,直至主线程完成后,才会从任务队列中选择已经完成的任务,并将其加入栈中,此时栈中只有这一个执行上下文,如果回调报错,也无法获取调用该异步操作时的栈中的信息,不容易判定哪里出现了错误。此外,因为是异步的缘故,使用 try catch 语句也无法直接捕获错误。(不过 Promise 并没有解决这个问题)3.借助外层变量当多个异步计算同时进行,比如这里遍历读取文件信息,由于无法预期完成顺序,必须借助外层作用域的变量,比如这里的 count、errored、stats 等,不仅写起来麻烦,而且如果你忽略了文件读取错误时的情况,不记录错误状态,就会接着读取其他文件,造成无谓的浪费。此外外层的变量,也可能被其它同一作用域的函数访问并且修改,容易造成误操作。之所以单独讲讲回调地狱,其实是想说嵌套和缩进只是回调地狱的一个梗而已,它导致的问题远非嵌套导致的可读性降低而已。PromisePromise 使得以上绝大部分的问题都得到了解决。1. 嵌套问题举个例子:request(url, function(err, res, body) { if (err) handleError(err); fs.writeFile(‘1.txt’, body, function(err) { request(url2, function(err, res, body) { if (err) handleError(err) }) })});使用 Promise 后:request(url).then(function(result) { return writeFileAsynv(‘1.txt’, result)}).then(function(result) { return request(url2)}).catch(function(e){ handleError(e)});而对于读取最大文件的那个例子,我们使用 promise 可以简化为:var fs = require(‘fs’);var path = require(‘path’);var readDir = function(dir) { return new Promise(function(resolve, reject) { fs.readdir(dir, function(err, files) { if (err) reject(err); resolve(files) }) })}var stat = function(path) { return new Promise(function(resolve, reject) { fs.stat(path, function(err, stat) { if (err) reject(err) resolve(stat) }) })}function findLargest(dir) { return readDir(dir) .then(function(files) { let promises = files.map(file => stat(path.join(dir, file))) return Promise.all(promises).then(function(stats) { return { stats, files } }) }) .then(data => { let largest = data.stats .filter(function(stat) { return stat.isFile() }) .reduce((prev, next) => { if (prev.size > next.size) return prev return next }) return data.files[data.stats.indexOf(largest)] })}2. 控制反转再反转前面我们讲到使用第三方回调 API 的时候,可能会遇到如下问题:回调函数执行多次回调函数没有执行回调函数有时同步执行有时异步执行对于第一个问题,Promise 只能 resolve 一次,剩下的调用都会被忽略。对于第二个问题,我们可以使用 Promise.race 函数来解决:function timeoutPromise(delay) { return new Promise( function(resolve,reject){ setTimeout( function(){ reject( “Timeout!” ); }, delay ); } );}Promise.race( [ foo(), timeoutPromise( 3000 )] ).then(function(){}, function(err){});对于第三个问题,为什么有的时候会同步执行有的时候回异步执行呢?我们来看个例子:var cache = {…};function downloadFile(url) { if(cache.has(url)) { // 如果存在cache,这里为同步调用 return Promise.resolve(cache.get(url)); } return fetch(url).then(file => cache.set(url, file)); // 这里为异步调用}console.log(‘1’);getValue.then(() => console.log(‘2’));console.log(‘3’);在这个例子中,有 cahce 的情况下,打印结果为 1 2 3,在没有 cache 的时候,打印结果为 1 3 2。然而如果将这种同步和异步混用的代码作为内部实现,只暴露接口给外部调用,调用方由于无法判断是到底是异步还是同步状态,影响程序的可维护性和可测试性。简单来说就是同步和异步共存的情况无法保证程序逻辑的一致性。然而 Promise 解决了这个问题,我们来看个例子:var promise = new Promise(function (resolve){ resolve(); console.log(1);});promise.then(function(){ console.log(2);});console.log(3);// 1 3 2即使 promise 对象立刻进入 resolved 状态,即同步调用 resolve 函数,then 函数中指定的方法依然是异步进行的。PromiseA+ 规范也有明确的规定:实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。Promise 反模式1.Promise 嵌套// badloadSomething().then(function(something) { loadAnotherthing().then(function(another) { DoSomethingOnThem(something, another); });});// goodPromise.all([loadSomething(), loadAnotherthing()]).then(function ([something, another]) { DoSomethingOnThem(…[something, another]);});2.断开的 Promise 链// badfunction anAsyncCall() { var promise = doSomethingAsync(); promise.then(function() { somethingComplicated(); }); return promise;}// goodfunction anAsyncCall() { var promise = doSomethingAsync(); return promise.then(function() { somethingComplicated() });}3.混乱的集合// badfunction workMyCollection(arr) { var resultArr = []; function _recursive(idx) { if (idx >= resultArr.length) return resultArr; return doSomethingAsync(arr[idx]).then(function(res) { resultArr.push(res); return _recursive(idx + 1); }); } return _recursive(0);}你可以写成:function workMyCollection(arr) { return Promise.all(arr.map(function(item) { return doSomethingAsync(item); }));}如果你非要以队列的形式执行,你可以写成:function workMyCollection(arr) { return arr.reduce(function(promise, item) { return promise.then(function(result) { return doSomethingAsyncWithResult(item, result); }); }, Promise.resolve());}4.catch// badsomethingAync.then(function() { return somethingElseAsync();}, function(err) { handleMyError(err);});如果 somethingElseAsync 抛出错误,是无法被捕获的。你可以写成:// goodsomethingAsync.then(function() { return somethingElseAsync()}).then(null, function(err) { handleMyError(err);});// goodsomethingAsync().then(function() { return somethingElseAsync();}).catch(function(err) { handleMyError(err);});红绿灯问题题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)三个亮灯函数已经存在:function red(){ console.log(‘red’);}function green(){ console.log(‘green’);}function yellow(){ console.log(‘yellow’);}利用 then 和递归实现:function red(){ console.log(‘red’);}function green(){ console.log(‘green’);}function yellow(){ console.log(‘yellow’);}var light = function(timmer, cb){ return new Promise(function(resolve, reject) { setTimeout(function() { cb(); resolve(); }, timmer); });};var step = function() { Promise.resolve().then(function(){ return light(3000, red); }).then(function(){ return light(2000, green); }).then(function(){ return light(1000, yellow); }).then(function(){ step(); });}step();promisify有的时候,我们需要将 callback 语法的 API 改造成 Promise 语法,为此我们需要一个 promisify 的方法。因为 callback 语法传参比较明确,最后一个参数传入回调函数,回调函数的第一个参数是一个错误信息,如果没有错误,就是 null,所以我们可以直接写出一个简单的 promisify 方法:function promisify(original) { return function (…args) { return new Promise((resolve, reject) => { args.push(function callback(err, …values) { if (err) { return reject(err); } return resolve(…values) }); original.call(this, …args); }); };}完整的可以参考 es6-promisifPromise 的局限性1. 错误被吃掉首先我们要理解,什么是错误被吃掉,是指错误信息不被打印吗?并不是,举个例子:throw new Error(’error’);console.log(233333);在这种情况下,因为 throw error 的缘故,代码被阻断执行,并不会打印 233333,再举个例子:const promise = new Promise(null);console.log(233333);以上代码依然会被阻断执行,这是因为如果通过无效的方式使用 Promise,并且出现了一个错误阻碍了正常 Promise 的构造,结果会得到一个立刻跑出的异常,而不是一个被拒绝的 Promise。然而再举个例子:let promise = new Promise(() => { throw new Error(’error’)});console.log(2333333);这次会正常的打印 233333,说明 Promise 内部的错误不会影响到 Promise 外部的代码,而这种情况我们就通常称为 “吃掉错误”。其实这并不是 Promise 独有的局限性,try..catch 也是这样,同样会捕获一个异常并简单的吃掉错误。而正是因为错误被吃掉,Promise 链中的错误很容易被忽略掉,这也是为什么会一般推荐在 Promise 链的最后添加一个 catch 函数,因为对于一个没有错误处理函数的 Promise 链,任何错误都会在链中被传播下去,直到你注册了错误处理函数。2. 单一值Promise 只能有一个完成值或一个拒绝原因,然而在真实使用的时候,往往需要传递多个值,一般做法都是构造一个对象或数组,然后再传递,then 中获得这个值后,又会进行取值赋值的操作,每次封装和解封都无疑让代码变得笨重。说真的,并没有什么好的方法,建议是使用 ES6 的解构赋值:Promise.all([Promise.resolve(1), Promise.resolve(2)]).then(([x, y]) => { console.log(x, y);});3. 无法取消Promise 一旦新建它就会立即执行,无法中途取消。4. 无法得知 pending 状态当处于 pending 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。参考《你不知道的 JavaScript 中卷》Promise 的 N 种用法JavaScript Promise 迷你书Promises/A+规范 Promise 如何使用Promise Anti-patterns一道关于Promise应用的面试题ES6 系列ES6 系列目录地址:https://github.com/mqyqingfeng/BlogES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。 ...

October 17, 2018 · 4 min · jiezi

从0到1实现Promise

前言Promise大家一定都不陌生了,JavaScript异步流程从最初的Callback,到Promise,到Generator,再到目前使用最多的Async/Await(如果对于这些不熟悉的可以参考我另一篇文章《JavaScript异步编程》),这不仅仅是技术实现的发展,更是思想上对于如何控制异步的递进。Promise作为后续方案的基础,是重中之重,也是面试时候最常被问到的。今天我们就一起从0到1实现一个基于A+规范的Promise,过程中也会对Promise的异常处理,以及是否可手动终止做一些讨论,最后会对我们实现的Promise做单元测试。完整的代码已经上传到github,想直接看代码的可以点这里。虽然已经有很多带你实现Promise类的文章了,但每个人理解的程度不一样,也许不同的文章可以带给你不同的思考呢,那我们就开始吧。正文1. 基础框架new Promise()时接收一个executor函数作为参数,该函数会立即执行,函数中有两个参数,它们也是函数,分别是resolve和reject,函数同步执行一定要放在try…catch中,否则无法进行错误捕获。MyPromise.jsfunction MyPromise(executor) { function resolve(value) { } function reject(reason) { } try { executor(resolve, reject); } catch (reason) { reject(reason); }}module.exports = MyPromise;resolve()接收Promise成功值value,reject接收Promise失败原因reason。test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { resolve(123);})2. 添加状态机目前实现存在的问题:Promise是一个状态机的机制,初始状态为 pending,成功状态为 fulfilled,失败状态为 rejected。只能从 pending -> fulfilled,或者从 pending -> rejected,并且状态一旦转变,就永远不会再变了。所以,我们需要为Promise添加一个状态流转的机制。MyPromise.jsconst PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function MyPromise(executor) { let self = this; self.state = PENDING; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; } } try { executor(resolve, reject); } catch (reason) { reject(reason); }}module.exports = MyPromise;test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { resolve(123);});promise.then(function(value) { console.log(‘value’, value);}, function(reason) { console.log(‘reason’, reason);})3. 添加then方法Promise拥有一个then方法,接收两个函数 onFulfilled 和 onRejected,分别作为Promise成功和失败的回调。所以,在then方法中我们需要对状态state进行判断,如果是fulfilled,则执行onFulfilled(value)方法,如果是rejected,则执行onRejected(reason)方法。由于成功值value和失败原因reason是由用户在executor中通过resolve(value) 和 reject(reason)传入的,所以我们需要有一个全局的value和reason供后续方法获取。MyPromise.jsconst PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function MyPromise(executor) { let self = this; self.state = PENDING; self.value = null; self.reason = null; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; self.value = value; } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; self.reason = reason; } } try { executor(resolve, reject); } catch (reason) { reject(reason); }}MyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; if (self.state === FULFILLED) { onFuifilled(self.value); } if (self.state === REJECTED) { onRejected(self.reason); }};module.exports = MyPromise;4. 实现异步调用resolve目前实现存在的问题:同步调用resolve()没有问题,但如果是异步调用,比如放到setTimeout中,因为目前的代码在调用then()方法时,state仍是pending状态,当timer到时候调用resolve()把state修改为fulfilled状态,但是onFulfilled()函数已经没有时机调用了。针对上述问题,进行如下修改:MyPromise.jsconst PENDING = ‘pending’;const FULFILLED = ‘fulfilled’;const REJECTED = ‘rejected’;function MyPromise(executor) { let self = this; self.state = PENDING; self.value = null; self.reason = null; self.onFulfilledCallbacks = []; self.onRejectedCallbacks = []; function resolve(value) { if (self.state === PENDING) { self.state = FULFILLED; self.value = value; self.onFulfilledCallbacks.forEach(function(fulfilledCallback) { fulfilledCallback(); }); } } function reject(reason) { if (self.state === PENDING) { self.state = REJECTED; self.reason = reason; self.onRejectedCallbacks.forEach(function(rejectedCallback) { rejectedCallback(); }); } } try { executor(resolve, reject); } catch (reason) { reject(reason); }}MyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; if (self.state === PENDING) { self.onFulfilledCallbacks.push(() => { onFuifilled(self.value); }); self.onRejectedCallbacks.push(() => { onRejected(self.reason); }); } if (self.state === FULFILLED) { onFuifilled(self.value); } if (self.state === REJECTED) { onRejected(self.reason); }};module.exports = MyPromise;我们添加了两个回调函数数组onFulfilledCallbacks和onRejectedCallbacks,用来存储then()方法中传入的成功和失败回调。然后,当用户调用resolve()或reject()的时候,修改state状态,并从相应的回调数组中依次取出回调函数执行。同时,通过这种方式我们也实现了可以注册多个then()函数,并且在成功或者失败时按照注册顺序依次执行。test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { setTimeout(function() { resolve(123); }, 1000);});promise.then(function(value) { console.log(‘value1’, value);}, function(reason) { console.log(‘reason1’, reason);});promise.then(function(value) { console.log(‘value2’, value);}, function(reason) { console.log(‘reason2’, reason);});5. then返回的仍是Promise读过PromiseA+规范的同学肯定知道,then()方法返回的仍是一个Promise,并且返回Promise的resolve的值是上一个Promise的onFulfilled()函数或onRejected()函数的返回值。如果在上一个Promise的then()方法回调函数的执行过程中发生了错误,那么会将其捕获到,并作为返回的Promise的onRejected函数的参数传入。比如:let promise = new Promise((resolve, reject) => { resolve(123);});promise.then((value) => { console.log(‘value1’, value); return 456;}).then((value) => { console.log(‘value2’, value);});let promise = new Promise((resolve, reject) => { resolve(123);});打印结果为:value1 123 value2 456let promise = new Promise((resolve, reject) => { resolve(123);});promise.then((value) => { console.log(‘value1’, value); a.b = 2; // 这里存在语法错误 return 456;}).then((value) => { console.log(‘value2’, value);}, (reason) => { console.log(‘reason2’, reason);});打印结果为:value1 123 reason2 ReferenceError: a is not defined可以看到,then()方法回调函数如果发生错误,会被捕获到,那么then()返回的Promise会自动变为onRejected,执行onRejected()回调函数。let promise = new Promise((resolve, reject) => { reject(123);});promise.then((value) => { console.log(‘value1’, value); return 456;}, (reason) => { console.log(‘reason1’, reason); return 456;}).then((value) => { console.log(‘value2’, value);}, (reason) => { console.log(‘reason2’, reason);});打印结果为:reason1 123 value2 456好啦,接下来我们就去实现then()方法依然返回一个Promise。MyPromise.jsMyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; let promise2 = null; promise2 = new MyPromise((resolve, reject) => { if (self.state === PENDING) { self.onFulfilledCallbacks.push(() => { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch(reason) { reject(reason); } }); self.onRejectedCallbacks.push(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch(reason) { reject(reason); } }); } if (self.state === FULFILLED) { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } } if (self.state === REJECTED) { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } } }); return promise2;};可以看到,我们新增了一个promise2作为then()方法的返回值。通过let x = onFuifilled(self.value) 或者 let x = onRejected(self.reason)拿到then()方法回调函数的返回值,然后调用self.resolvePromise(promise2, x, resolve, reject),将新增的promise2、x、promise2的resolve和reject传入到resolvePromise()中。所以,下面我们重点看一下resolvePromise()方法。MyPromise.jsMyPromise.prototype.resolvePromise = function(promise2, x, resolve, reject) { let self = this; let called = false; // called 防止多次调用 if (promise2 === x) { return reject(new TypeError(‘循环引用’)); } if (x !== null && (Object.prototype.toString.call(x) === ‘[object Object]’ || Object.prototype.toString.call(x) === ‘[object Function]’)) { // x是对象或者函数 try { let then = x.then; if (typeof then === ‘function’) { then.call(x, (y) => { // 别人的Promise的then方法可能设置了getter等,使用called防止多次调用then方法 if (called) return ; called = true; // 成功值y有可能还是promise或者是具有then方法等,再次resolvePromise,直到成功值为基本类型或者非thenable self.resolvePromise(promise2, y, resolve, reject); }, (reason) => { if (called) return ; called = true; reject(reason); }); } else { if (called) return ; called = true; resolve(x); } } catch (reason) { if (called) return ; called = true; reject(reason); } } else { // x是普通值,直接resolve resolve(x); }};resolvePromise()是用来解析then()回调函数中返回的仍是一个Promise,这个Promise有可能是我们自己的,有可能是别的库实现的,也有可能是一个具有then()方法的对象,所以这里靠resolvePromise()来实现统一处理。下面是翻译自PromiseA+规范关于resolvePromise()的要求:Promise 解决过程Promise 解决过程是一个抽象的操作,其需输入一个 promise 和一个值,我们表示为 [[Resolve]](promise, x),如果 x 有 then 方法且看上去像一个 Promise ,解决程序即尝试使 promise 接受 x 的状态;否则其用 x 的值来执行 promise 。这种 thenable 的特性使得 Promise 的实现更具有通用性:只要其暴露出一个遵循 Promise/A+ 协议的 then 方法即可;这同时也使遵循 Promise/A+ 规范的实现可以与那些不太规范但可用的实现能良好共存。运行 [[Resolve]](promise, x) 需遵循以下步骤:x 与 promise 相等如果 promise 和 x 指向同一对象,以 TypeError 为据因拒绝执行 promisex 为 Promise如果 x 为 Promise ,则使 promise 接受 x 的状态:- 如果 x 处于等待态, promise 需保持为等待态直至 x 被执行或拒绝- 如果 x 处于执行态,用相同的值执行 promise- 如果 x 处于拒绝态,用相同的据因拒绝 promisex 为对象或函数如果 x 为对象或者函数:- 把 x.then 赋值给 then- 如果取 x.then 的值时抛出错误 e ,则以 e 为据因拒绝 promise- 如果 then 是函数,将 x 作为函数的作用域 this 调用之。传递两个回调函数作为参数,第一个参数叫做 resolvePromise ,第二个参数叫做 rejectPromise: - 如果 resolvePromise 以值 y 为参数被调用,则运行 [[Resolve]](promise, y) - 如果 rejectPromise 以据因 r 为参数被调用,则以据因 r 拒绝 promise - 如果 resolvePromise 和 rejectPromise 均被调用,或者被同一参数调用了多次,则优先采用首次调用并忽略剩下的调用 - 如果调用 then 方法抛出了异常 e: - 如果 resolvePromise 或 rejectPromise 已经被调用,则忽略之 - 否则以 e 为据因拒绝 promise - 如果 then 不是函数,以 x 为参数执行 promise- 如果 x 不为对象或者函数,以 x 为参数执行 promise如果一个 promise 被一个循环的 thenable 链中的对象解决,而 [[Resolve]](promise, thenable) 的递归性质又使得其被再次调用,根据上述的算法将会陷入无限递归之中。算法虽不强制要求,但也鼓励施者检测这样的递归是否存在,若检测到存在则以一个可识别的 TypeError 为据因来拒绝 promise。参考上述规范,结合代码中的注释,相信大家可以理解resolvePromise()的作用了。测试:test.jslet MyPromise = require(’./MyPromise.js’);let promise = new MyPromise(function(resolve, reject) { setTimeout(function() { resolve(123); }, 1000);});promise.then((value) => { console.log(‘value1’, value); return new MyPromise((resolve, reject) => { resolve(456); }).then((value) => { return new MyPromise((resolve, reject) => { resolve(789); }) });}, (reason) => { console.log(‘reason1’, reason);}).then((value) => { console.log(‘value2’, value);}, (reason) => { console.log(‘reason2’, reason);});打印结果:value1 123 value2 7896. 让then()方法的回调函数总是异步调用官方Promise实现的回调函数总是异步调用的:console.log(‘start’);let promise = new Promise((resolve, reject) => { console.log(‘step-’); resolve(123);});promise.then((value) => { console.log(‘step–’); console.log(‘value’, value);});console.log(’end’);打印结果:start step- end step– value1 123Promise属于微任务,这里我们为了方便用宏任务setTiemout来代替实现异步,具体关于宏任务、微任务以及Event Loop可以参考我的另一篇文章带你彻底弄懂Event Loop。MyPromise.jsMyPromise.prototype.then = function(onFuifilled, onRejected) { let self = this; let promise2 = null; promise2 = new MyPromise((resolve, reject) => { if (self.state === PENDING) { self.onFulfilledCallbacks.push(() => { setTimeout(() => { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); }); self.onRejectedCallbacks.push(() => { setTimeout(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); }); } if (self.state === FULFILLED) { setTimeout(() => { try { let x = onFuifilled(self.value); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); } if (self.state === REJECTED) { setTimeout(() => { try { let x = onRejected(self.reason); self.resolvePromise(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }, 0); } }); return promise2;};测试:test.jslet MyPromise = require(’./MyPromise.js’);console.log(‘start’);let promise = new MyPromise((resolve, reject) => { console.log(‘step-’); setTimeout(() => { resolve(123); }, 1000);});promise.then((value) => { console.log(‘step–’); console.log(‘value’, value);});console.log(’end’);打印结果:start step- end step– value1 123经过以上步骤,一个最基本的Promise就已经实现完了,下面我们会实现一些不在PromiseA+规范的扩展方法。7. 实现catch()方法then()方法的onFulfilled和onRejected回调函数都不是必传项,如果不传,那么我们就无法接收reject(reason)中的错误,这时我们可以通过链式调用catch()方法用来接收错误。举例:let promise = new Promise((resolve, reject) => { reject(‘has error’);});promise.then((value) => { console.log(‘value’, value);}).catch((reason) => { console.log(‘reason’, reason);});打印结果:reason has error不仅如此,catch()可以作为Promise链式调用的最后一步,前面Promise发生的错误会冒泡到最后一个catch()中,从而捕获异常。举例:let promise = new Promise((resolve, reject) => { resolve(123);});promise.then((value) => { console.log(‘value’, value); return new Promise((resolve, reject) => { reject(‘has error1’); });}).then((value) => { console.log(‘value’, value); return new Promise((resolve, reject) => { reject(‘has error2’); });}).catch((reason) => { console.log(‘reason’, reason);});打印结果:reason has error reason has error1那么catch()方法到底是如何实现的呢?答案就是在Promise的实现中,onFulfilled和onRejected函数是有默认值的:MyPromise.jsMyPromise.prototype.then = function(onFuifilled, onRejected) { onFuifilled = typeof onFuifilled === ‘function’ ? onFuifilled : value => {return value;}; onRejected = typeof onRejected === ‘function’ ? onRejected : reason => {throw reason};};MyPromise.prototype.catch = function(onRejected) { return this.then(null, onRejected);};可以看到,onRejected的默认值是把错误reason通过throw抛出去。由于我们对于同步代码的执行都是在try…catch中的,所以如果Promise发生了错误,如果没传onRejected,默认的函数会把错误reason抛出,然后会被promise2捕捉到,作为reject(reason)决议。catch()实现就是调用this.then(null, onRejected),由于promise2被reject,所以会执行onRejected回调,于是就捕捉到了第一个promise的错误。总结来说,then()方法中不传onRejected回调,Promise内部会默认帮你写一个函数作为回调,作用就是throw抛出reject或者try…catch到的错误,然后错误reason会被promise2作为reject(reason)进行决议,于是会被下一个then()方法的onRejected回调函数调用,而catch只是写了一个特殊的then(null, onRejected)而已。所以,我们在写Promise的链式调用的时候,在then()中可以不传onRejected回调,只需要在链式调用的最末尾加一个catch()就可以了,这样在该链条中的Promise发生的错误都会被最后的catch捕获到。举例1:let promise = new Promise((resolve, reject) => { reject(123);});promise.then((value) => { // 注意,不会走这里,因为第一个promise是被reject的 console.log(‘value1’, value); return new Promise((resolve, reject) => { reject(‘has error1’); });}).then((value) => { console.log(‘value2’, value); return new Promise((resolve, reject) => { reject(‘has error2’); });}, (reason) => { // 注意,这个then有onRejected回调 console.log(‘reason2’, reason);}).catch((reason) => { // 错误在上一个then就被捕获了,所以不会走到这里 console.log(‘reason3’, reason);});打印结果:reason2 123举例2:let promise = new Promise((resolve, reject) => { reject(123);});promise.then((value) => { console.log(‘value1’, value); return new Promise((resolve, reject) => { reject(‘has error1’); });}).then((value) => { console.log(‘value2’, value); return new Promise((resolve, reject) => { reject(‘has error2’); });}).catch((reason) => { // 由于链条中的then都没有onRejected回调,所以会一直被冒泡到最后的catch这里 console.log(‘reason3’, reason);});catch和then一样都是返回一个新的Promise。有的同学可能会有疑问,如果catch中的回调执行也发生错误该怎么办呢,这个我们后续在Promise异常处理中再做讨论。打印结果:reason3 1238. 实现finally方法finally是某些库对Promise实现的一个扩展方法,无论是resolve还是reject,都会走finally方法。MyPromise.jsMyPromise.prototype.finally = function(fn) { return this.then(value => { fn(); return value; }, reason => { fn(); throw reason; });};9. 实现done方法done方法作为Promise链式调用的最后一步,用来向全局抛出没有被Promise内部捕获的错误,并且不再返回一个Promise。一般用来结束一个Promise链。MyPromise.jsMyPromise.prototype.done = function() { this.catch(reason => { console.log(‘done’, reason); throw reason; });};10. 实现Promise.all方法Promise.all()接收一个包含多个Promise的数组,当所有Promise均为fulfilled状态时,返回一个结果数组,数组中结果的顺序和传入的Promise顺序一一对应。如果有一个Promise为rejected状态,则整个Promise.all为rejected。MyPromise.jsMyPromise.all = function(promiseArr) { return new MyPromise((resolve, reject) => { let result = []; promiseArr.forEach((promise, index) => { promise.then((value) => { result[index] = value; if (result.length === promiseArr.length) { resolve(result); } }, reject); }); });};test.jslet MyPromise = require(’./MyPromise.js’);let promise1 = new MyPromise((resolve, reject) => { console.log(‘aaaa’); setTimeout(() => { resolve(1111); console.log(1111); }, 1000);});let promise2 = new MyPromise((resolve, reject) => { console.log(‘bbbb’); setTimeout(() => { reject(2222); console.log(2222); }, 2000);});let promise3 = new MyPromise((resolve, reject) => { console.log(‘cccc’); setTimeout(() => { resolve(3333); console.log(3333); }, 3000);});Promise.all([promise1, promise2, promise3]).then((value) => { console.log(‘all value’, value);}, (reason) => { console.log(‘all reason’, reason);})打印结果:aaaa bbbb cccc 1111 2222 all reason 2222 333311. 实现Promise.reace方法Promise.race()接收一个包含多个Promise的数组,当有一个Promise为fulfilled状态时,整个大的Promise为onfulfilled,并执行onFulfilled回调函数。如果有一个Promise为rejected状态,则整个Promise.race为rejected。MyPromise.jsMyPromise.race = function(promiseArr) { return new MyPromise((resolve, reject) => { promiseArr.forEach(promise => { promise.then((value) => { resolve(value); }, reject); }); });};test.jslet MyPromise = require(’./MyPromise.js’);let promise1 = new MyPromise((resolve, reject) => { console.log(‘aaaa’); setTimeout(() => { resolve(1111); console.log(1111); }, 1000);});let promise2 = new MyPromise((resolve, reject) => { console.log(‘bbbb’); setTimeout(() => { reject(2222); console.log(2222); }, 2000);});let promise3 = new MyPromise((resolve, reject) => { console.log(‘cccc’); setTimeout(() => { resolve(3333); console.log(3333); }, 3000);});Promise.all([promise1, promise2, promise3]).then((value) => { console.log(‘all value’, value);}, (reason) => { console.log(‘all reason’, reason);})打印结果:aaaa bbbb cccc 1111 all reason 1111 2222 333312. 实现Promise.resolve方法Promise.resolve用来生成一个fulfilled完成态的Promise,一般放在整个Promise链的开头,用来开始一个Promise链。MyPromise.jsMyPromise.resolve = function(value) { let promise; promise = new MyPromise((resolve, reject) => { this.prototype.resolvePromise(promise, value, resolve, reject); }); return promise;};test.jslet MyPromise = require(’./MyPromise.js’);MyPromise.resolve(1111).then((value) => { console.log(‘value1’, value); return new MyPromise((resolve, reject) => { resolve(2222); })}).then((value) => { console.log(‘value2’, value);})打印结果:value1 1111 value2 2222由于传入的value有可能是普通值,有可能是thenable,也有可能是另一个Promise,所以调用resolvePromise进行解析。12. 实现Promise.reject方法Promise.reject用来生成一个rejected失败态的Promise。MyPromise.jsMyPromise.reject = function(reason) { return new MyPromise((resolve, reject) => { reject(reason); });};test.jslet MyPromise = require(’./MyPromise.js’);MyPromise.reject(1111).then((value) => { console.log(‘value1’, value); return new MyPromise((resolve, reject) => { resolve(2222); })}).then((value) => { console.log(‘value2’, value);}).catch(reason => { console.log(‘reason’, reason);});打印结果:reason 111113. 实现Promise.deferred方法Promise.deferred可以用来延迟执行resolve和reject。MyPromise.jsMyPromise.deferred = function() { let dfd = {}; dfd.promies = new MyPromise((resolve, reject) => { dfd.resolve = resolve; dfd.rfeject = reject; }); return dfd;};这样,你就可以在外部通过调用dfd.resolve()和dfd.reject()来决议该Promise。13. 如何停止一个Promise链假设这样一个场景,我们有一个很长的Promise链式调用,这些Promise是依次依赖的关系,如果链条中的某个Promise出错了,就不需要再向下执行了,默认情况下,我们是无法实现这个需求的,因为Promise无论是then还是catch都会返回一个Promise,都会继续向下执行then或catch。举例:new Promise(function(resolve, reject) { resolve(1111)}).then(function(value) { // “ERROR!!!”}).catch() .then() .then() .catch() .then()有没有办法让这个链式调用在ERROR!!!的后面就停掉,完全不去执行链式调用后面所有回调函数呢?我们自己封装一个Promise.stop方法。MyPromise.jsMyPromise.stop = function() { return new Promise(function() {});};stop中返回一个永远不执行resolve或者reject的Promise,那么这个Promise永远处于pending状态,所以永远也不会向下执行then或catch了。这样我们就停止了一个Promise链。new MyPromise(function(resolve, reject) { resolve(1111)}).then(function(value) { // “ERROR!!!” MyPromise.stop();}).catch() .then() .then() .catch() .then()但是这样会有一个缺点,就是链式调用后面的所有回调函数都无法被垃圾回收器回收。14. 如何解决Promise链上返回的最后一个Promise出现错误看如下例子:new Promise(function(resolve) { resolve(42)}).then(function(value) { a.b = 2;});这里a不存在,所以给a.b赋值是一个语法错误,onFulfilled回调函数是包在try…catch中执行的,错误会被catch到,但是由于后面没有then或catch了,这个错误无法被处理,就会被Promise吃掉,没有任何异常,这就是常说的Promise有可能会吃掉错误。那么我们怎么处理这种情况呢?方法一就是我们前面已经实现过的done()。new Promise(function(resolve) { resolve(42)}).then(function(value) { a.b = 2;}).done();done()方法相当于一个catch,但是却不再返回Promise了,注意done()方法中不能出现语法错误,否则又无法捕获了。方法二普通错误监听window的error事件可以实现捕获window.addEventListener(’error’, error => { console.log(error); // 不会触发});Promise没有被onRejected()处理的错误需要监听unhandledrejection事件window.addEventListener(‘unhandledrejection’, error => { console.log(‘unhandledrejection’, error); // 可以触发,而且还可以直接拿到 promise 对象});14. 单元测试结束相关单元测试以及完整代码可以到我的github查看,如果对你有帮助的话,就来个star吧~参考文档PromiseA+规范 ...

September 29, 2018 · 8 min · jiezi