Как получить доступ к предыдущим результатам обещания в цепочке. then ()?


Я изменил свой код на обещания, и построил замечательный длинный плоская цепь обещания, состоящую из нескольких .then() обратные вызовы. В конце концов я хочу вернуть некоторое составное значение, и мне нужно получить доступ к нескольким промежуточные результаты обещания. Однако значения разрешения из середины последовательности не находятся в области действия в последнем обратном вызове, как мне получить к ним доступ?

function getExample() {
    return promiseA(…).then(function(resultA) {
        // Some processing
        return promiseB(…);
    }).then(function(resultB) {
        // More processing
        return // How do I gain access to resultA here?
    });
}
15 523

15 ответов:

разорвать цепь

когда вам нужно получить доступ к промежуточным значениям в вашей цепочке, вы должны разделить свою цепочку на те отдельные части, которые вам нужны. Вместо того чтобы присоединять один обратный вызов и каким - то образом пытаться использовать его параметр несколько раз, присоедините несколько обратных вызовов к одному и тому же обещанию-везде, где вам нужно значение результата. Не забывай, а promise просто представляет (прокси) будущее значение! Рядом с получением одного обещания от другого в линейном цепочка, используйте комбинаторы обещаний, которые даны вам вашей библиотекой, чтобы построить значение результата.

это приведет к очень простой поток управления, четкой композицией функций и, следовательно, легко модуляризацией.

function getExample() {
    var a = promiseA(…);
    var b = a.then(function(resultA) {
        // some processing
        return promiseB(…);
    });
    return Promise.all([a, b]).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

вместо деструктурирования параметра в обратном вызове после Promise.all это стало доступно только с ES6, в ES5 then вызов будет заменен отличным вспомогательным методом, который был предоставлен многими библиотеками обещаний ( Q,птица,, когда,...):.spread(function(resultA, resultB) { ….

Bluebird также имеет специальный join функции заменить Promise.all+spread комбинация с более простой (и более эффективной) конструкцией:

…
return Promise.join(a, b, function(resultA, resultB) { … });

ECMAScript Harmony

конечно, эта проблема была признана и дизайнеров языке. Они сделали много работы и предложение асинхронных функций наконец-то сделал это в

ECMAScript 8

вам не нужен ни один then вызов или функция обратного вызова больше, как и в асинхронной функции (которая возвращает обещание при вызове) вы можете просто ждать обещаний, чтобы решить непосредственно. Она также имеет произвольное структуры управления, такие как условия, циклы и предложения try-catch, но для удобства нам они здесь не нужны:

async function getExample() {
    var resultA = await promiseA(…);
    // some processing
    var resultB = await promiseB(…);
    // more processing
    return // something using both resultA and resultB
}

ECMAScript 6

пока мы ждали ES8,мы уже использовали очень похожий синтаксис. ES6 пришел с функции генератора, которые позволяют разбить исполнение на части при произвольном размещении yield ключевые слова. Эти срезы могут выполняться друг за другом, независимо, даже асинхронно - и это только то, что мы делаем, когда хотим дождаться разрешения обещания, прежде чем выполнить следующий шаг.

есть выделенные библиотеки (например co или задач.js), но также многие библиотеки обещаний имеют вспомогательные функции (Q,птица,, когда, ...), что делать это асинхронное пошаговое выполнение для вас, когда вы даете им функцию генератора, что дает обещания.

var getExample = Promise.coroutine(function* () {
//               ^^^^^^^^^^^^^^^^^ Bluebird syntax
    var resultA = yield promiseA(…);
    // some processing
    var resultB = yield promiseB(…);
    // more processing
    return // something using both resultA and resultB
});

это сделал работа в узле.js начиная с версии 4.0, также несколько браузеров (или их выпуски dev) поддерживали синтаксис генератора относительно рано.

ECMAScript 5

однако, если вы хотите / должны быть обратно совместимы вы не можете использовать те, без транспилера. Как функции генератора, так и асинхронные функции поддерживаются текущим инструментарием, см., например, документацию Babel on генераторы и асинхронные функции.

и затем, есть также много других compile-to-JS languages которые предназначены для облегчения асинхронного программирования. Они обычно используют синтаксис, подобный await (например,Iced CoffeeScript), но есть и другие, которые имеют Haskell-like do-нотации (например,LatteJs,монадическом,PureScript или LispyScript).

синхронно проверки

назначение обещаний для более поздних необходимых значений переменным, а затем получение их значения с помощью синхронной проверки. В примере используется сине .value() метод, но многие библиотеки предоставляют подобный метод.

function getExample() {
    var a = promiseA(…);

    return a.then(function() {
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // a is guaranteed to be fulfilled here so we can just retrieve its
        // value synchronously
        var aValue = a.value();
    });
}

Это может быть использовано для любого количества значений, как вам нравится:

function getExample() {
    var a = promiseA(…);

    var b = a.then(function() {
        return promiseB(…)
    });

    var c = b.then(function() {
        return promiseC(…);
    });

    var d = c.then(function() {
        return promiseD(…);
    });

    return d.then(function() {
        return a.value() + b.value() + c.value() + d.value();
    });
}

вложенности (и) закрытие

использование замыканий для поддержания области переменных (в нашем случае параметров функции обратного вызова success) является естественным решением JavaScript. С обещаниями мы можем произвольно гнездо и придавить.then() вызовы - они семантически эквивалентны, за исключением объема внутренней.

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(function(resultB) {
            // more processing
            return // something using both resultA and resultB;
        });
    });
}

конечно, это строительство пирамиды отступа. Если отступ становится слишком большим, вы все еще можете применить старые инструменты для противодействия пирамида судьбы: модульность, использование дополнительных именованных функций и сглаживание цепочки обещаний, как только вам больше не нужна переменная.
Теоретически вы всегда можете избежать более двух уровней вложенности (сделав все замыкания явными), на практике используйте столько, сколько разумно.

function getExample() {
    // preprocessing
    return promiseA(…).then(makeAhandler(…));
}
function makeAhandler(…)
    return function(resultA) {
        // some processing
        return promiseB(…).then(makeBhandler(resultA, …));
    };
}
function makeBhandler(resultA, …) {
    return function(resultB) {
        // more processing
        return // anything that uses the variables in scope
    };
}

вы также можете использовать вспомогательные функции для такого рода частичное применение, как _.partial от подчеркивание/лодашь или уроженца .bind() метод, для дальнейшего уменьшения отступа:

function getExample() {
    // preprocessing
    return promiseA(…).then(handlerA);
}
function handlerA(resultA) {
    // some processing
    return promiseB(…).then(handlerB.bind(null, resultA));
}
function handlerB(resultA, resultB) {
    // more processing
    return // anything that uses resultA and resultB
}

явный сквозной

подобно вложению обратных вызовов, этот метод основан на замыканиях. Тем не менее, цепочка остается плоской - вместо того, чтобы передавать только последний результат, для каждого шага передается некоторый объект состояния. Эти объекты состояния накапливают результаты предыдущих действий, передавая все значения, которые понадобятся позже снова плюс результат текущей задачи.

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(b => [resultA, b]); // function(b) { return [resultA, b] }
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

вот эта маленькая стрелка b => [resultA, b] - это функция, которая закрывает resultA, и передает массив обоих результатов на следующий шаг. Который использует синтаксис деструктурирования параметров, чтобы снова разбить его на отдельные переменные.

прежде чем деструктурирование стало доступно с ES6, отличный вспомогательный метод называется .spread() было предоставлено многими библиотеками обещаний (Q,птица,, когда,...). Он принимает функцию с несколькими параметрами-по одному для каждого элемента массива - для использования в качестве .spread(function(resultA, resultB) { ….

Of конечно, это закрытие, необходимое здесь, может быть дополнительно упрощено некоторыми вспомогательными функциями, например

function addTo(x) {
    // imagine complex `arguments` fiddling or anything that helps usability
    // but you get the idea with this simple one:
    return res => [x, res];
}

…
return promiseB(…).then(addTo(resultA));

кроме того, вы можете использовать Promise.all чтобы создать обещание для массива:

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return Promise.all([resultA, promiseB(…)]); // resultA will implicitly be wrapped
                                                    // as if passed to Promise.resolve()
    }).then(function([resultA, resultB]) {
        // more processing
        return // something using both resultA and resultB
    });
}

и вы можете использовать не только массивы, но и произвольно сложные объекты. Например, с _.extend или Object.assign в другой вспомогательной функции:

function augment(obj, name) {
    return function (res) { var r = Object.assign({}, obj); r[name] = res; return r; };
}

function getExample() {
    return promiseA(…).then(function(resultA) {
        // some processing
        return promiseB(…).then(augment({resultA}, "resultB"));
    }).then(function(obj) {
        // more processing
        return // something using both obj.resultA and obj.resultB
    });
}

в то время как этот шаблон гарантирует плоскую цепь и явное состояние объекты могут улучшить четкость, это станет утомительным для длинной цепи. Особенно когда состояние нужно только спорадически, его все равно приходится проходить через каждый шаг. С этим фиксированным интерфейсом одиночные обратные вызовы в цепочке довольно тесно связаны и негибки для изменения. Это делает факторинг отдельных шагов сложнее, и обратные вызовы не могут быть предоставлены непосредственно из других модулей - они всегда должны быть завернуты в шаблонный код, который заботится о состоянии. Абстрактные вспомогательные функции как и выше, можно немного облегчить боль, но она всегда будет присутствовать.

изменяемое контекстное состояние

тривиальное (но неэлегантное и скорее errorprone) решение состоит в том, чтобы просто использовать переменные более высокой области (к которым имеют доступ все обратные вызовы в цепочке) и записывать в них значения результатов, когда вы их получаете:

function getExample() {
    var resultA;
    return promiseA(…).then(function(_resultA) {
        resultA = _resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both resultA and resultB
    });
}

вместо многих переменных можно также использовать (изначально пустой) объект, на котором результаты хранятся в динамически создаваемых объектов.

это решение имеет несколько недостатки:

  • изменчивое состояние уродливо и глобальные переменные-это зло.
  • этот шаблон не работает через границы функций, модулирование функций сложнее, поскольку их объявления не должны выходить из общей области
  • область действия переменных не препятствует доступу к ним до их инициализации. Это особенно вероятно для сложных конструкций обещания (петли, ветвления, исключения), где условия гонки могут произойти. Пройдя прямо заявить, а декларативная конструкция что обещает поощрять, сил более чистый стиль кодирования, который может предотвратить это.
  • необходимо правильно выбрать область для этих общих переменных. Он должен быть локальным для выполняемой функции, чтобы предотвратить условия гонки между несколькими параллельными вызовами, как это было бы в случае, если, например, состояние было сохранено на экземпляре.

библиотека "Синяя птица" призывает использование объекта, который передается вместе, используя их bind() метод чтобы назначить объект контекста для цепочки обещаний. Он будет доступен из каждой функции обратного вызова через иначе непригодный this ключевое слово. Хотя свойства объекта более склонны к незамеченным опечаткам, чем переменные, шаблон довольно умен:

function getExample() {
    return promiseA(…)
    .bind({}) // Bluebird only!
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }).bind(); // don't forget to unbind the object if you don't want the
               // caller to access it
}

этот подход можно легко смоделировать в библиотеках promise, которые не поддерживают .связывать (хотя и в несколько более многословный способ и не может быть использован в выражении):

function getExample() {
    var ctx = {};
    return promiseA(…)
    .then(function(resultA) {
        this.resultA = resultA;
        // some processing
        return promiseB(…);
    }.bind(ctx)).then(function(resultB) {
        // more processing
        return // something using both this.resultA and resultB
    }.bind(ctx));
}

узел 7.4 теперь поддерживает асинхронные / ожидающие вызовы с флагом гармонии.

попробуйте это:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

и запустите файл с:

node --harmony-async-await getExample.js

просто, как может быть!

другой ответ, используя babel-node версия

используя async - await

npm install -g babel@5.6.14

example.js:

async function getExample(){

  let response = await returnPromise();

  let response2 = await returnPromise2();

  console.log(response, response2)

}

getExample()

затем запустите babel-node example.js и вуаля!

менее резкий спин на "изменяемом контекстном состоянии"

использование объекта локальной области для сбора промежуточных результатов в цепочке обещаний является разумным подходом к поставленному вами вопросу. Рассмотрим следующий фрагмент:

function getExample(){
    //locally scoped
    const results = {};
    return promiseA(...).then(function(resultA){
        results.a = resultA;
        return promiseB(...);
    }).then(function(resultB){
        results.b = resultB;
        return promiseC(...);
    }).then(function(resultC){
        //Resolve with composite of all promises
        return Promise.resolve(results.a + results.b + resultC);
    }).catch(function(error){
        return Promise.reject(error);
    });
}
  • глобальные переменные плохи, поэтому это решение использует локальную переменную, которая не причиняет вреда. Он доступен только в пределах функции.
  • изменчивое состояние уродливо, но это не мутирует состояние в уродливые манеры. Уродливое изменчивое состояние традиционно относится к изменению состояния аргументов функции или глобальных переменных, но этот подход просто изменяет состояние переменной локальной области, которая существует с единственной целью агрегирования результатов обещания...переменная, которая умрет простой смертью, как только обещание разрешится.
  • промежуточные обещания не препятствуют доступу к состоянию объекта результатов, но это не вводит какой-то страшный сценарий, когда один из обещания в цепочке будут идти изгоев и саботировать ваши результаты. Ответственность за установку значений на каждом этапе обещания ограничивается этой функцией, и общий результат будет либо правильным, либо incorrect...it не будет какой-то ошибки, которая появится через несколько лет в производстве (если вы не намерены это делать!)
  • это не вводит сценарий состояния гонки, который возник бы из параллельного вызова, потому что новый экземпляр переменной результатов создается для каждого вызов функции getExample.

другой ответ, используя последовательный исполнитель nsynjs:

function getExample(){

  var response1 = returnPromise1().data;

  // promise1 is resolved at this point, '.data' has the result from resolve(result)

  var response2 = returnPromise2().data;

  // promise2 is resolved at this point, '.data' has the result from resolve(result)

  console.log(response, response2);

}

nynjs.run(getExample,{},function(){
    console.log('all done');
})

обновление: добавлен рабочий пример

function synchronousCode() {
     var urls=[
         "https://ajax.googleapis.com/ajax/libs/jquery/1.7.0/jquery.min.js",
         "https://ajax.googleapis.com/ajax/libs/jquery/1.8.0/jquery.min.js",
         "https://ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"
     ];
     for(var i=0; i<urls.length; i++) {
         var len=window.fetch(urls[i]).data.text().data.length;
         //             ^                   ^
         //             |                   +- 2-nd promise result
         //             |                      assigned to 'data'
         //             |
         //             +-- 1-st promise result assigned to 'data'
         //
         console.log('URL #'+i+' : '+urls[i]+", length: "+len);
     }
}

nsynjs.run(synchronousCode,{},function(){
    console.log('all done');
})
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

в эти дни я также встречаю некоторые вопросы, как вы. Наконец, я нахожу хорошее решение с вопросом, это просто и хорошо читать. Я надеюсь, что это может помочь вам.

по данным how-to-chain-javascript-promises

ок, давайте посмотрим на код:

const firstPromise = () => {
    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('first promise is completed');
            resolve({data: '123'});
        }, 2000);
    });
};

const secondPromise = (someStuff) => {
    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('second promise is completed');
            resolve({newData: `${someStuff.data} some more data`});
        }, 2000);
    });
};

const thirdPromise = (someStuff) => {
    const promise = new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log('third promise is completed');
            resolve({result: someStuff});
        }, 2000);
    });
};

firstPromise()
    .then(seondPromise)
    .then(thirdPromise)
    .then(data => {
        console.log(data);
    });

Я не собираюсь использовать этот шаблон в мой собственный код, так как я не большой поклонник использования глобальных переменных. Однако в крайнем случае это сработает.

пользователь-это многообещающая модель Мангуста.

var globalVar = '';

User.findAsync({}).then(function(users){
  globalVar = users;
}).then(function(){
  console.log(globalVar);
});

при использовании bluebird, вы можете использовать .bind метод совместного использования переменных в цепочке обещаний:

somethingAsync().bind({})
.spread(function (aValue, bValue) {
    this.aValue = aValue;
    this.bValue = bValue;
    return somethingElseAsync(aValue, bValue);
})
.then(function (cValue) {
    return this.aValue + this.bValue + cValue;
});

пожалуйста, проверьте эту ссылку для получения дополнительной информации:

http://bluebirdjs.com/docs/api/promise.bind.html

function getExample() {
    var retA, retB;
    return promiseA(…).then(function(resultA) {
        retA = resultA;
        // Some processing
        return promiseB(…);
    }).then(function(resultB) {
        // More processing
        //retA is value of promiseA
        return // How do I gain access to resultA here?
    });
}

простой способ: D

Я думаю, что вы можете использовать хэш RSVP.

что-то вроде как ниже :

    const mainPromise = () => {
        const promise1 = new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('first promise is completed');
                resolve({data: '123'});
            }, 2000);
        });

        const promise2 = new Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('second promise is completed');
                resolve({data: '456'});
            }, 2000);
        });

        return new RSVP.hash({
              prom1: promise1,
              prom2: promise2
          });

    };


   mainPromise()
    .then(data => {
        console.log(data.prom1);
        console.log(data.prom2);
    });