Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ES6 系列之我们来聊聊 Promise #98

Open
mqyqingfeng opened this issue Oct 16, 2018 · 34 comments
Open

ES6 系列之我们来聊聊 Promise #98

mqyqingfeng opened this issue Oct 16, 2018 · 34 comments

Comments

@mqyqingfeng
Copy link
Owner

前言

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,对方是否会因为某个错误导致你传入的回调函数执行了多次呢?

为了避免出现这样的问题,你可以在自己的回调函数中加入判断,可是万一又因为某个错误这个回调函数没有执行呢?
万一这个回调函数有时同步执行有时异步执行呢?

我们总结一下这些情况:

  1. 回调函数执行多次
  2. 回调函数没有执行
  3. 回调函数有时同步执行有时异步执行

对于这些情况,你可能都要在回调函数中做些处理,并且每次执行回调函数的时候都要做些处理,这就带来了很多重复的代码。

回调地狱

我们先看一个简单的回调地狱的示例。

现在要找出一个目录中最大的文件,处理步骤应该是:

  1. fs.readdir 获取目录中的文件列表;
  2. 循环遍历文件,使用 fs.stat 获取文件信息
  3. 比较找出最大文件;
  4. 以最大文件的文件名为参数调用回调。

代码为:

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 等,不仅写起来麻烦,而且如果你忽略了文件读取错误时的情况,不记录错误状态,就会接着读取其他文件,造成无谓的浪费。此外外层的变量,也可能被其它同一作用域的函数访问并且修改,容易造成误操作。

之所以单独讲讲回调地狱,其实是想说嵌套和缩进只是回调地狱的一个梗而已,它导致的问题远非嵌套导致的可读性降低而已。

Promise

Promise 使得以上绝大部分的问题都得到了解决。

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 的时候,可能会遇到如下问题:

  1. 回调函数执行多次
  2. 回调函数没有执行
  3. 回调函数有时同步执行有时异步执行

对于第一个问题,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 嵌套

// bad
loadSomething().then(function(something) {
    loadAnotherthing().then(function(another) {
        DoSomethingOnThem(something, another);
    });
});
// good
Promise.all([loadSomething(), loadAnotherthing()])
.then(function ([something, another]) {
    DoSomethingOnThem(...[something, another]);
});

2.断开的 Promise 链

// bad
function anAsyncCall() {
    var promise = doSomethingAsync();
    promise.then(function() {
        somethingComplicated();
    });

    return promise;
}
// good
function anAsyncCall() {
    var promise = doSomethingAsync();
    return promise.then(function() {
        somethingComplicated()
    });
}

3.混乱的集合

// bad
function 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

// bad
somethingAync.then(function() {
    return somethingElseAsync();
}, function(err) {
    handleMyError(err);
});

如果 somethingElseAsync 抛出错误,是无法被捕获的。你可以写成:

// good
somethingAsync
.then(function() {
    return somethingElseAsync()
})
.then(null, function(err) {
    handleMyError(err);
});
// good
somethingAsync()
.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-promisif

Promise 的局限性

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 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。

参考

  1. 《你不知道的 JavaScript 中卷》
  2. Promise 的 N 种用法
  3. JavaScript Promise 迷你书
  4. Promises/A+规范
  5. Promise 如何使用
  6. Promise Anti-patterns
  7. 一道关于Promise应用的面试题

ES6 系列

ES6 系列目录地址:https://github.com/mqyqingfeng/Blog

ES6 系列预计写二十篇左右,旨在加深 ES6 部分知识点的理解,重点讲解块级作用域、标签模板、箭头函数、Symbol、Set、Map 以及 Promise 的模拟实现、模块加载方案、异步处理等内容。

如果有错误或者不严谨的地方,请务必给予指正,十分感谢。如果喜欢或者有所启发,欢迎 star,对作者也是一种鼓励。

@cobish
Copy link

cobish commented Oct 18, 2018

大大终于更新啦,可喜可贺,消灭零评论。

@Jackie1210
Copy link

《你不知道的 JavaScript 中卷》这里有英文版,也可以看~

@nanzihan
Copy link

doA()
doF()
doB()
doC()
doE()
doD()
这里的执行顺序是有问题的吧?

@nanzihan
Copy link

不过如果doA 是异步的话当我没说

@inottn
Copy link

inottn commented Dec 12, 2018

@nanzihan

使用回调,我们很有可能会将业务代码写成如下这种形式:
...

doA() 里传的是回调函数。

@hellobabyting
Copy link

这个primitify代码看的似懂非懂的,想尝试一下怎么用,但是不会用,能否有个例子让观摩学习一下

@jusss
Copy link

jusss commented Jan 20, 2019

如果不使用setTimeout, 使用next和Promise能自动执行generator直至结束吗?而且还不阻塞线程也就是说是异步的

@littlewin-wang
Copy link

@hellobabyting

var fs = require('fs')

// callback usage
fs.readdir('./', function (err, files) {
  ...
})

// promisify
let promisifyReadDir = promisify(fs.readdir)
promisifyReadDir('./').then(files => {
  console.log(files)
})

@woshiqiang1
Copy link

有几个笔误:“会”,函数名
to be fixed

@yh284914425
Copy link

image
博主还是改一下吧

@GarvenZhang
Copy link

红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次

这里是 红3 绿1 黄2 红3 绿1 黄2 红3....

难道没人发现这里不对吗?包括一道关于Promise应用的面试题

不应该是这个顺序吗?

var step = function() {
    Promise.resolve().then(function(){
        return light(3000, red);
    }).then(function(){
        return light(1000, green);
    }).then(function(){
        return light(2000, yellow);
    }).then(function(){
        step();
    });
}

@zooeyking
Copy link

红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次

这里是 红3 绿1 黄2 红3 绿1 黄2 红3....

难道没人发现这里不对吗?包括一道关于Promise应用的面试题

不应该是这个顺序吗?

var step = function() {
    Promise.resolve().then(function(){
        return light(3000, red);
    }).then(function(){
        return light(1000, green);
    }).then(function(){
        return light(2000, yellow);
    }).then(function(){
        step();
    });
}

这里没什么不妥啊, 只是跟你的那篇文章有些许差异而已,你step()里接收了一个立即resolve()的promise 跟Promise.resolve() 效果一样的

@sammok
Copy link

sammok commented Mar 8, 2020

@hellobabyting

这个primitify代码看的似懂非懂的,想尝试一下怎么用,但是不会用,能否有个例子让观摩学习一下

用法大概是这样:
callback 方式: stat(path, (err, res) => .....)
promise 方式: promisify(stat)(path).then(res => console.log(res))

function promisify(original) {
    //  promisify(stat) 这一步 return 下一行
    return function (...args) {
        //  将 original 函数接管,比如调用 promisify(stat)(path) 则 return 下一行的 promise
        return new Promise((resolve, reject) => {
            // 将 arguments 里面新增一个 original 的 callback,用来改变 promise 的状态
            args.push(function callback(err, ...values) {
                if (err) {
                    return reject(err);
                }
                return resolve(...values)
            });
           //  执行原函数(args 已经新增了 callback 了)
            original.call(this, ...args);
        });
    };
}

如有错误,请指正

@coderlxf
Copy link

coderlxf commented May 4, 2020

这个primitify代码看的似懂非懂的,想尝试一下怎么用,但是不会用,能否有个例子让观摩学习一下

这样会好理解一点

function promisify (original) {
  return function () {
    return new Promise((resolve, reject) => {
      original(...arguments, (err, ...values) => {
        if (err) return reject(err);
        return resolve(...values);
      });
    });
  };
} 

@zhw2590582
Copy link

@coderlxf 你这写法更容易理解,但剩余参数values不是单一值时,就无法resolve多个值了,所以还是可以改造一下

function promisify(original, manyArgs = false) {
  return function () {
    return new Promise((resolve, reject) => {
      original(...arguments, (err, ...values) => {
        if (err) return reject(err);
        return resolve(manyArgs ? values : values[0]);
      });
    });
  };
}

@jdwdw
Copy link

jdwdw commented Jun 28, 2020

无法取消的问题,是不是可以使用Promise.race 实现的promise-abort来解决?(虽然还是会执行) https://www.npmjs.com/package/promise-abortable
实现类似于:

function fetchWithAbort(fetchPromise) {
  let abort = null
  const abortPromise = new Promise((resolve, reject) => {
    abort = () => {
      reject('abort')
    }
  })
  let promiseWithAbort = Promise.race([fetchPromise, abortPromise])
  promiseWithAbort.abort = abort
  return promiseWithAbort
}

@keep-run
Copy link

想到一个问题,红绿灯的问题中,如果想实现中途操作一下,退出递归,该怎么实现呢。

@xsfxtsxxr
Copy link

红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次
这里是 红3 绿1 黄2 红3 绿1 黄2 红3....
难道没人发现这里不对吗?包括一道关于Promise应用的面试题
不应该是这个顺序吗?

var step = function() {
    Promise.resolve().then(function(){
        return light(3000, red);
    }).then(function(){
        return light(1000, green);
    }).then(function(){
        return light(2000, yellow);
    }).then(function(){
        step();
    });
}

这里没什么不妥啊, 只是跟你的那篇文章有些许差异而已,你step()里接收了一个立即resolve()的promise 跟Promise.resolve() 效果一样的

红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次
这里是 红3 绿1 黄2 红3 绿1 黄2 红3....
难道没人发现这里不对吗?包括一道关于Promise应用的面试题
不应该是这个顺序吗?

var step = function() {
    Promise.resolve().then(function(){
        return light(3000, red);
    }).then(function(){
        return light(1000, green);
    }).then(function(){
        return light(2000, yellow);
    }).then(function(){
        step();
    });
}

这里没什么不妥啊, 只是跟你的那篇文章有些许差异而已,你step()里接收了一个立即resolve()的promise 跟Promise.resolve() 效果一样的

请教一下,绿灯一秒亮一次,那它后面不应该是1000吗,为什么green 是2000,yellow反而是1000?

@xiaoyucoding
Copy link

感谢分享,一点小瑕疵 writeFileAsynv => writeFileAsync 拼写错误。

@monsterjacob24
Copy link

@keep-run 可以step里最后的then判断是否要执行step,比如外部设定执行次数,执行step之前累加

@xsfxtsxxr
Copy link

又看了一遍,发现题目描述是错误的,而且各个平台这个题目都是这样照搬过来的,“红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次” 应该改为 “红灯亮一次亮三秒,绿灯亮一次亮一秒,黄灯亮一次亮2秒”

打印一下调用函数时候的秒数:

function red () {
  console.log('red: ', new Date().getSeconds())
  console.log('red')
}
function green () {
  console.log('green: ', new Date().getSeconds())
  console.log('green')
}
function yellow () {
  console.log('yellow: ', new Date().getSeconds())
  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()
// 打印结果 
// red:  14
// red
// green:  16
// green
// yellow:  17
// yellow
// red:  20
// red
// green:  22
// green
// yellow:  23
// yellow
// red:  26
// red
// green:  28
// green
// yellow:  29
// yellow

如果不对,欢迎打脸😶

@zhanghang2017
Copy link

zhanghang2017 commented Mar 2, 2021

这个primitify代码看的似懂非懂的,想尝试一下怎么用,但是不会用,能否有个例子让观摩学习一下

一般用于将一些第三方的 api的回调函数方式转化成 promise的写法,比如常见的 微信api

wx.showModal({
  title: '提示',
  content: '这是一个模态弹窗',
  success (res) {
    if (res.confirm) {
      console.log('用户点击确定')
    } else if (res.cancel) {
      console.log('用户点击取消')
    }
  }
})

这种方式 就会有回调地狱的现象,换成下面的方式:

let promiseify = function (originFn) {
  return function () {
    let args = [...arguments][0]
    return new Promise((resolve, rejcet) => {
      args.success = resolve
      args.fail = rejcet
      originFn.call(this, args)
    })
  }
}
let myMhowModal  = promiseify(wx.showModal)
myMhowModal ({ title: '提示',
  content: '这是一个模态弹窗',}).then(....)

@lovyliu
Copy link

lovyliu commented Mar 10, 2021

又看了一遍,发现题目描述是错误的,而且各个平台这个题目都是这样照搬过来的,“红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次” 应该改为 “红灯亮一次亮三秒,绿灯亮一次亮一秒,黄灯亮一次亮2秒”

打印一下调用函数时候的秒数:

function red () {
  console.log('red: ', new Date().getSeconds())
  console.log('red')
}
function green () {
  console.log('green: ', new Date().getSeconds())
  console.log('green')
}
function yellow () {
  console.log('yellow: ', new Date().getSeconds())
  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()
// 打印结果 
// red:  14
// red
// green:  16
// green
// yellow:  17
// yellow
// red:  20
// red
// green:  22
// green
// yellow:  23
// yellow
// red:  26
// red
// green:  28
// green
// yellow:  29
// yellow

如果不对,欢迎打脸😶

题目没错,不是亮一次亮几秒。
你说的是“红灯亮一次亮三秒,绿灯亮一次亮一秒,黄灯亮一次亮2秒”。如果按灯亮几秒算的话,以你提供的秒数为例:14秒红灯亮,16秒绿灯亮,17秒黄灯亮,20秒红灯亮。结果是红灯亮了2秒,绿灯亮了1秒,黄灯亮了3秒。
我的理解是这个题里的灯几秒钟亮一次无所谓,重点是1⃣️使用promise的链式调用来保证灯按顺序亮,2⃣️使用递归来重复亮灯。

@DerrySongSN
Copy link

刚看了阮一峰的Promise
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');

这个地方如果 return Promise.resolve(cache.get(url)); 那他并不会 打印 1,2,3 因为 Promise.resolve立马执行也是在当前 event loop 的结束前执行

@yinsang
Copy link

yinsang commented Jan 5, 2022

同步就没有必要用地狱回调和Promise了,这里讲的回调默认指的是异步了。

@yinsang
Copy link

yinsang commented Jan 6, 2022

function workMyCollection(arr) {
    return arr.reduce(function(promise, item) {
        return promise.then(function(result) {
            return doSomethingAsyncWithResult(item, result);
        });
    }, Promise.resolve());
}

队列这里是容易引起误解的。需要其实传入[函数1, 函数2],而不是Promise实例 [ new Promise(), new Promise()]。
arr需要包含一系列异步函数而不是直接包含Promise实例,如果直接包含实例,Promise实例会数组在初始化时就开始执行,而不会等到第一个Promise执行完再执行第二个。

return doSomethingAsyncWithResult(item, result);

这一行其实是指讲上一个result作为参数传递给下一个promise 函数

return item(result)

这个问题在【Promise 嵌套】一节也存在,bad里面表达的先后执行队列与Promise.all的同时执行差异是很大的。

@yinsang
Copy link

yinsang commented Jan 6, 2022

还可以思考下如果按照队列执行,又想拿到所有的Promise实例结果怎么实现?
下面给出一个简单的版本,希望大家斧正。


function loadSomething(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log('reject loadSomething')
      resolve('loadSomethingDone')
  },2000)
  })
}
function loadAnotherthing(){
  return new Promise((resolve,reject)=>{
    setTimeout(()=>{
      console.log('reject loadAnotherthing')
      resolve('loadAnotherthingDone')
  },1000)
  })
}
function promiseOrder(promises){
  return new Promise((resolve, reject) => {
    const result = []
    let promiseIndex = 0;
    const runner = ()=>{
      if(promiseIndex === promises.length){
        resolve(result);
        return;
      }
       promises[promiseIndex++]().then((res)=>{
        result.push(res)
        runner()
      }).catch(e=>{
        reject(e)
      })
    }
   runner()
  })
}
promiseOrder([loadSomething, loadAnotherthing]).then(results=>{
  console.log(results,'allResult')
})

@YBFJ
Copy link

YBFJ commented Jul 20, 2022

刚看了阮一峰的Promise 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');

这个地方如果 return Promise.resolve(cache.get(url)); 那他并不会 打印 1,2,3 因为 Promise.resolve立马执行也是在当前 event loop 的结束前执行

我这边跑出来也是,132,因为console.log(2),放在.then里面,
image

@JessicaFang
Copy link

个人理解,Promise 反模式中有关于2.断开的 Promise 链两个例子应该是不相等的, 如下面

// false
let p1 = function anAsyncCall() {
  var promise = Promise.resolve(1);
  promise.then(function() {
      Promise.reject(1)
  });
  return promise;
}
// good
let p2 = function anAsyncCall() {
  var promise =  Promise.resolve(1);
  return promise.then(function() {
    Promise.reject(1)
  });
}
p1() === p2 ()// false

如果在then里面加上return会更明显

// bad
let p1 = function anAsyncCall() {
  var promise = Promise.resolve(1);
  promise.then(function() {
      return Promise.reject(1)
  });
  return promise;
}
// good
let p2 = function anAsyncCall() {
  var promise =  Promise.resolve(1);
  return promise.then(function() {
     return Promise.reject(1)
  });
}
p1();  // Promise {<fulfilled>: 1}
p2();  // Promise {<rejected>: 1}

原因: bad里面的返回的是Promise.resolve这个promise, good里面返回的是promise.then这个promise, 这点对照参考源码比较清晰理解

@Erica-WX
Copy link

Erica-WX commented Feb 6, 2023

请问为什么有cache的情况下打印是1,2,3呢?感觉应该还是1,3,2呀
image

@justorez
Copy link

justorez commented Feb 7, 2023

红绿灯问题,绿灯和黄灯的时间写错了吧。

题目:红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次;如何让三个灯不断交替重复亮灯?(用 Promse 实现)

const now = () => new Date().toLocaleTimeString()

function red(){
    console.log(now(), 'red');
}
function green(){
    console.log(now(), 'green');
}
function yellow(){
    console.log(now(), 'yellow');
}

var light = function(timmer, cb){
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            cb();
            resolve();
        }, timmer);
    });
};

var step = function() {
    console.log(now())
    Promise.resolve().then(function(){
        return light(3000, red);
    }).then(function(){
        return light(1000, green);
    }).then(function(){
        return light(2000, yellow);
    })
}

step();
15:09:07
15:09:10 red
15:09:11 green
15:09:13 yellow

@qyjandroid
Copy link

function doA(callback){
console.log("doA")
callback();
}
function doB(){
console.log("doB")
}
function doC(callback){
console.log("doC")
callback();
}
function doD(){
console.log("doD")
}
function doE(){
console.log("doE")
}

function doF(){
console.log("doF")
}

doA(function(){
doB();
doC(function(){
doD();
});
doE();
})

doF();

简单代码来看,顺序和你说的不一致!
doA
doB
doC
doD
doE
doF

@fengjrrz
Copy link

fengjrrz commented Feb 4, 2024

请问为什么有cache的情况下打印是1,2,3呢?感觉应该还是1,3,2呀 image

我跑下来,也是1,3,2。其实downloadFile返回的也是promise,再then,还是异步的呀。。作者应该是粗心了

@init-center
Copy link

又看了一遍,发现题目描述是错误的,而且各个平台这个题目都是这样照搬过来的,“红灯三秒亮一次,绿灯一秒亮一次,黄灯2秒亮一次” 应该改为 “红灯亮一次亮三秒,绿灯亮一次亮一秒,黄灯亮一次亮2秒”
打印一下调用函数时候的秒数:

function red () {
  console.log('red: ', new Date().getSeconds())
  console.log('red')
}
function green () {
  console.log('green: ', new Date().getSeconds())
  console.log('green')
}
function yellow () {
  console.log('yellow: ', new Date().getSeconds())
  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()
// 打印结果 
// red:  14
// red
// green:  16
// green
// yellow:  17
// yellow
// red:  20
// red
// green:  22
// green
// yellow:  23
// yellow
// red:  26
// red
// green:  28
// green
// yellow:  29
// yellow

如果不对,欢迎打脸😶

题目没错,不是亮一次亮几秒。 你说的是“红灯亮一次亮三秒,绿灯亮一次亮一秒,黄灯亮一次亮2秒”。如果按灯亮几秒算的话,以你提供的秒数为例:14秒红灯亮,16秒绿灯亮,17秒黄灯亮,20秒红灯亮。结果是红灯亮了2秒,绿灯亮了1秒,黄灯亮了3秒。 我的理解是这个题里的灯几秒钟亮一次无所谓,重点是1⃣️使用promise的链式调用来保证灯按顺序亮,2⃣️使用递归来重复亮灯。

题目描述确实是错误的,我个人测试情况如下:
red 2024-04-12T10:46:58.217Z
green 2024-04-12T10:47:01.225Z
yellow 2024-04-12T10:47:03.227Z
red 2024-04-12T10:47:04.228Z
green 2024-04-12T10:47:07.230Z
yellow 2024-04-12T10:47:09.231Z
red 2024-04-12T10:47:10.233Z

红灯亮三秒,然后绿灯亮两秒,黄灯亮一秒。并不是隔几秒亮一次,同一种颜色的灯都是隔六秒亮一次。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests