Maison > Article > interface Web > Une brève analyse de Promise dans es6 (avec des exemples)
Le contenu de cet article concerne une brève analyse de Promise dans es6 (avec des exemples). Il a une certaine valeur de référence. Les amis dans le besoin peuvent s'y référer.
Pour l'utilisation de base de Promise, vous pouvez lire "Introduction à ECMAScript 6" par le professeur Ruan Yifeng.
Parlons d’autre chose.
Rappels
Quand on parle de Promise, on commence généralement par les rappels ou l'enfer des rappels. Alors, quels sont les inconvénients de l'utilisation des rappels ?
1. Imbrication de rappels
En utilisant les rappels, nous sommes susceptibles d'écrire du code commercial sous la forme suivante :
doA( function(){ doB(); doC( function(){ doD(); } ) doE(); } ); doF();
Bien sûr, c'est une forme simplifiée. Après quelques réflexions simples, nous pouvons juger que l'ordre d'exécution est :
doA() doF() doB() doC() doE() doD()
Cependant, dans les projets réels, le code sera plus compliqué. Afin de résoudre le problème, nous le faisons. Il est nécessaire d'éviter de nombreux contenus inesthétiques et de passer constamment d'une fonction à l'autre, ce qui rend le dépannage exponentiellement plus difficile.
Bien sûr, la raison de ce problème est que cette méthode d'écriture imbriquée est contraire à la façon de penser linéaire des gens, de sorte que nous devons consacrer plus d'énergie à réfléchir à la séquence d'exécution réelle, l'imbrication et l'indentation ne sont que mineures. des détails qui détournent l’attention de ce processus de réflexion.
Bien sûr, aller à l'encontre de la façon de penser linéaire n'est pas le pire. En fait, nous ajouterons également divers jugements logiques au code, comme dans l'exemple ci-dessus, doD() doit être complété après. doC() est terminé. Que se passe-t-il si l'exécution de doC() échoue ? Voulons-nous réessayer doC() ? Ou accéder directement à d’autres fonctions de gestion des erreurs ? Lorsque nous ajoutons ces jugements au processus, le code devient rapidement trop complexe à maintenir et à mettre à jour.
2. Inversion de contrôle
Lors de l'écriture du code normalement, nous pouvons bien sûr contrôler notre propre code, mais lorsque nous utilisons des rappels, ce rappel peut-il fonctionner ensuite l'exécution cela dépend en fait de l'API qui utilise le callback, par exemple :
// 回调函数是否被执行取决于 buy 模块 import {buy} from './buy.js'; buy(itemData, function(res) { console.log(res) });
Pour l'API fetch que nous utilisons souvent, il n'y a généralement pas de problème, mais si nous utilisons un tiers, qu'en est-il de l'API ?
Lorsque vous appelez une API tierce, l'autre partie exécutera-t-elle la fonction de rappel que vous avez transmise plusieurs fois en raison d'une erreur ?
Afin d'éviter de tels problèmes, vous pouvez ajouter du jugement à votre fonction de rappel. Mais que se passe-t-il si la fonction de rappel n'est pas exécutée en raison d'une erreur ?
Et si cette fonction de rappel était parfois exécutée de manière synchrone et parfois de manière asynchrone ?
Résumons ces situations :
La fonction de rappel est exécutée plusieurs fois
La fonction de rappel n'est pas exécutée
La fonction de rappel est parfois exécutée de manière synchrone et parfois de manière asynchrone
Pour ces situations, vous devrez peut-être effectuer un traitement dans la fonction de rappel et exécuter la fonction de rappel à chaque fois Nous devons effectuer un traitement à chaque fois, ce qui entraîne beaucoup de code répété.
Callback Hell
Regardons d'abord un exemple simple d'enfer de rappel.
Maintenant, pour trouver le fichier le plus volumineux dans un répertoire, les étapes de traitement doivent être :
Utilisez fs.readdir
pour obtenir la liste des fichiers dans le répertoire
Parcourez les fichiers et utilisez fs.stat
pour obtenir des informations sur les fichiers
Comparez pour trouver le fichier le plus volumineux
avec le plus grand Le rappel est appelé en paramètre avec le nom du fichier.
Le code est :
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)]) } }) }) }) }
La méthode d'utilisation est :
// 查找当前目录最大的文件 findLargest('./', function(er, filename) { if (er) return console.error(er) console.log('largest file was:', filename) });
Vous pouvez copier le code ci-dessus dans un fichier tel que index.js
, puis exécutez node index.js
pour imprimer le nom du fichier le plus volumineux.
Après avoir lu cet exemple, parlons d'autres problèmes de l'enfer des rappels :
1 Difficulté à réutiliser
Une fois l'ordre des rappels déterminé. , il est également très difficile de réutiliser certains liens, car un seul mouvement affecte tout le corps.
Par exemple, si vous souhaitez réutiliser le code pour lire les informations du fichier fs.stat
, car les variables externes sont référencées dans le rappel, le code externe doit être modifié après l'extraction.
2. Les informations de la pile sont déconnectées
Nous savons que le moteur JavaScript maintient une pile de contexte d'exécution Lorsqu'une fonction est exécutée, le contexte d'exécution de la fonction le sera. être créé. Poussé sur la pile, lorsque la fonction termine son exécution, le contexte d'exécution sera retiré de la pile.
Si la fonction B est appelée dans la fonction A, JavaScript poussera d'abord le contexte d'exécution de la fonction A sur la pile, puis poussera le contexte d'exécution de la fonction B sur la pile. Lorsque la fonction B termine son exécution, la fonction B. will Le contexte d'exécution est extrait de la pile. Lorsque la fonction A est exécutée, le contexte d'exécution de la fonction A est extrait de la pile.
L'avantage de ceci est que si nous interrompons l'exécution du code, nous pouvons récupérer les informations complètes de la pile et en obtenir toutes les informations que nous souhaitons.
可是异步回调函数并非如此,比如执行 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 的时候,可能会遇到如下问题:
回调函数执行多次
回调函数没有执行
回调函数有时同步执行有时异步执行
对于第一个问题,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 状态时,无法得知目前进展到哪一个阶段(刚刚开始还是即将完成)。
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!