Heim  >  Artikel  >  Web-Frontend  >  Eine kurze Analyse von Promise in es6 (mit Beispielen)

Eine kurze Analyse von Promise in es6 (mit Beispielen)

不言
不言nach vorne
2018-10-17 14:36:242240Durchsuche

Der Inhalt dieses Artikels ist eine kurze Analyse von Promise in es6 (mit Beispielen). Ich hoffe, dass er für Freunde in Not hilfreich ist.

Informationen zur grundlegenden Verwendung von Promise finden Sie in „Einführung in ECMAScript 6“ von Lehrer Ruan Yifeng.

Lass uns über etwas anderes reden.

Rückrufe

Wenn wir über Promise sprechen, beginnen wir normalerweise mit Rückrufen oder der Rückrufhölle. Was sind also die Nachteile der Verwendung von Rückrufen?

1. Callback-Verschachtelung

Mit Callbacks schreiben wir wahrscheinlich Geschäftscode in der folgenden Form:

doA( function(){
    doB();

    doC( function(){
        doD();
    } )

    doE();
} );

doF();

Natürlich ist dies A vereinfacht Nach einigem Nachdenken können wir feststellen, dass die Reihenfolge der Ausführung wie folgt lautet:

doA()
doF()
doB()
doC()
doE()
doD()

In tatsächlichen Projekten wird der Code jedoch chaotischer sein. Um das Problem zu beheben, müssen wir viele Unansehnliche umgehen Inhalte und ständige Sprünge zwischen Funktionen erschweren die Fehlerbehebung exponentiell.

Der Grund für dieses Problem liegt natürlich darin, dass diese verschachtelte Schreibmethode der linearen Denkweise der Menschen widerspricht, sodass wir mehr Energie aufwenden müssen, um über die tatsächliche Ausführungssequenz nachzudenken. Verschachtelung und Einrückung sind nur geringfügig Details, die die Aufmerksamkeit von diesem Denkprozess ablenken.

Natürlich ist es nicht das Schlimmste, gegen die lineare Denkweise zu verstoßen. Tatsächlich werden wir dem Code auch verschiedene logische Urteile hinzufügen, wie im obigen Beispiel muss doD() abgeschlossen werden doC() ist abgeschlossen. Was passiert, wenn die Ausführung von doC() fehlschlägt? Wollen wir doC() erneut versuchen? Oder direkt zu anderen Fehlerbehandlungsfunktionen wechseln? Wenn wir diese Beurteilungen in den Prozess einbeziehen, wird der Code schnell zu komplex, um ihn zu pflegen und zu aktualisieren.

2. Umkehrung der Kontrolle

Wenn wir Code normal schreiben, können wir natürlich unseren eigenen Code steuern, aber wenn wir Rückrufe verwenden, kann diese Rückruffunktion dann ausgeführt werden Hängt tatsächlich von der API ab, die den Rückruf verwendet, z. B.:

// 回调函数是否被执行取决于 buy 模块
import {buy} from './buy.js';

buy(itemData, function(res) {
    console.log(res)
});

Für die Abruf-API, die wir häufig verwenden, gibt es im Allgemeinen kein Problem, aber wenn wir eine API eines Drittanbieters verwenden Wolltuch?

Wenn Sie eine Drittanbieter-API aufrufen, führt die andere Partei dann aufgrund eines Fehlers die von Ihnen übergebene Rückruffunktion mehrmals aus?

Um solche Probleme zu vermeiden, können Sie Ihrer Rückruffunktion eine Beurteilung hinzufügen. Was aber, wenn die Rückruffunktion aufgrund eines Fehlers nicht ausgeführt wird?
Was ist, wenn diese Rückruffunktion manchmal synchron und manchmal asynchron ausgeführt wird?

Fassen wir diese Situationen zusammen:

  1. Die Rückruffunktion wird mehrmals ausgeführt

  2. Die Rückruffunktion wird nicht ausgeführt

  3. Die Rückruffunktion wird manchmal synchron und manchmal asynchron ausgeführt

In diesen Situationen müssen Sie möglicherweise einige Verarbeitungen in der Rückruffunktion durchführen und ausführen Die Rückruffunktion jedes Mal Wir müssen jedes Mal etwas verarbeiten, was eine Menge wiederholten Code mit sich bringt.

Callback-Hölle

Sehen wir uns zunächst ein einfaches Beispiel für die Callback-Hölle an.

Um nun die größte Datei in einem Verzeichnis zu finden, sollten die Verarbeitungsschritte wie folgt aussehen:

  1. Verwenden Sie fs.readdir, um die Dateiliste im Verzeichnis abzurufen;

  2. Durchsuchen Sie die Dateien und verwenden Sie

    , um Dateiinformationen zu erhaltenfs.stat

  3. Vergleichen Sie, um die größte Datei zu finden;

    mit dem größten Der Callback wird als Parameter mit dem Dateinamen der Datei aufgerufen.

  4. Der Code lautet:

    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)])
                    }
                })
            })
        })
    }
  5. Die Verwendungsmethode lautet:
// 查找当前目录最大的文件
findLargest('./', function(er, filename) {
    if (er) return console.error(er)
    console.log('largest file was:', filename)
});

Sie können den obigen Code in eine Datei wie

kopieren, und dann ausführen

Sie können den Namen der größten Datei ausdrucken.

Nachdem wir dieses Beispiel gelesen haben, sprechen wir über andere Probleme der Rückrufhölle: index.jsnode index.js

Schwierigkeit bei der Wiederverwendung

Nachdem die Reihenfolge der Rückrufe festgelegt wurde Außerdem ist es sehr schwierig, einige der Verbindungen wiederzuverwenden, da eine Bewegung den gesamten Körper betrifft. Wenn Sie beispielsweise den Code zum Lesen von Dateiinformationen

wiederverwenden möchten, muss der äußere Code nach der Extraktion geändert werden, da im Rückruf auf die äußeren Variablen verwiesen wird.

2. Die Stapelinformationen sind getrennt.fs.stat

Wir wissen, dass die JavaScript-Engine einen Ausführungskontextstapel verwaltet Wird auf den Stapel geschoben. Wenn die Ausführung der Funktion abgeschlossen ist, wird der Ausführungskontext aus dem Stapel entfernt. Wenn Funktion B in Funktion A aufgerufen wird, schiebt JavaScript zuerst den Ausführungskontext von Funktion A auf den Stapel und dann den Ausführungskontext von Funktion B auf den Stapel wird Der Ausführungskontext wird vom Stapel entfernt. Wenn Funktion A ausgeführt wird, wird der Ausführungskontext von Funktion A vom Stapel entfernt.

Der Vorteil davon ist, dass wir, wenn wir die Codeausführung unterbrechen, die vollständigen Stack-Informationen abrufen und alle gewünschten Informationen daraus erhalten können.

可是异步回调函数并非如此,比如执行 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 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。



Das obige ist der detaillierte Inhalt vonEine kurze Analyse von Promise in es6 (mit Beispielen). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Dieser Artikel ist reproduziert unter:segmentfault.com. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen