ホームページ  >  記事  >  ウェブフロントエンド  >  es6 の Promise の簡単な分析 (例付き)

es6 の Promise の簡単な分析 (例付き)

不言
不言転載
2018-10-17 14:36:242269ブラウズ

この記事では、es6 における Promise の簡単な分析を紹介します (例を示します)。必要な方は参考にしていただければ幸いです。

Promise の基本的な使用方法については、Ruan Yifeng 教師による「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)
});

通常、よく使用する取得 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 ファイルを作成し、nodeindex.js を実行して最大のファイルの名前を出力します。

この例を読んだ後、コールバック地獄の他の問題について話しましょう:

1. 再利用の難しさ

コールバックの順序が決定された後また、1 つの動作がボディ全体に影響を与えるため、一部のリンクを再利用することも非常に困難です。

たとえば、fs.stat でファイル情報を読み取るコードを再利用したい場合、外側の層の変数がコールバックで参照されるため、外側の層の変数を抽出する必要があります。レイヤーのコードが変更されます。

2. スタック情報が切断されています

関数が実行されると、JavaScript エンジンが実行コンテキスト スタックを維持することがわかっています。スタックにプッシュされると、関数の実行が完了すると、実行コンテキストがスタックからポップアウトされます。

関数 A で関数 B が呼び出された場合、JavaScript はまず関数 A の実行コンテキストをスタックにプッシュし、次に関数 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);
        });
    };
}

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



以上がes6 の Promise の簡単な分析 (例付き)の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。