Давайте поговорим об обещаниях в серии ES6.

внешний интерфейс GitHub JavaScript Promise
Давайте поговорим об обещаниях в серии ES6.

предисловие

Основное использование Обещания можно увидеть в книге Учителя Жуань Ифэна.Начало работы с ECMAScript 6.

Давай поговорим о чем-нибудь другом.

Перезвоните

Говоря об обещаниях, мы обычно начинаем с обратных вызовов или ада обратных вызовов, так каковы же недостатки использования обратных вызовов?

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файл, затем выполнитьnode index.jsОн распечатает имя самого большого файла.

Прочитав этот пример, давайте поговорим о других проблемах ада обратного вызова:

1. Сложно повторно использовать

После того, как порядок обратных вызовов определен, также сложно повторно использовать некоторые ссылки, и это повлияет на все тело.

Например, если вы хотитеfs.statКод для чтения информации о файле используется повторно, поскольку в обратном вызове есть ссылки на переменные внешнего уровня, а код внешнего уровня необходимо изменить после извлечения.

2. Информация о стеке отключена

Мы знаем, что движок JavaScript поддерживает стек контекста выполнения. Когда функция выполняется, он создает контекст выполнения функции и помещает его в стек. Когда функция выполняется, контекст выполнения извлекается из стека.

Если функция A вызывает функцию B, JavaScript сначала поместит в стек контекст выполнения функции A, а затем поместит в стек контекст выполнения функции B. После завершения выполнения функции A контекст выполнения функции A выскочил из стека.

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

Но это не относится к асинхронным функциям обратного вызова, таким как выполнениеfs.readdirКогда функция обратного вызова фактически добавляется в очередь задач, код продолжает выполняться до тех пор, пока основной поток не завершится, и завершенная задача будет выбрана из очереди задач и добавлена ​​в стек.В это время выполняется только эта в стеке.Контекст, если обратный вызов сообщает об ошибке, информация в стеке при вызове асинхронной операции не может быть получена, и непросто определить, где произошла ошибка.

Кроме того, поскольку он асинхронный, ошибки нельзя перехватывать напрямую с помощью операторов try catch.

(Обещания не решают эту проблему)

3. С помощью внешних переменных

Когда одновременно выполняются несколько асинхронных вычислений, таких как обход и чтение информации о файле здесь, поскольку нельзя ожидать порядка завершения, необходимо использовать переменные во внешней области, такие как количество, ошибка, статистика и т. д. здесь, что не только проблематично писать, но и игнорировать Если статус ошибки не записан, другие файлы будут прочитаны, что приведет к ненужным тратам. Кроме того, переменные на внешнем уровне также могут быть доступны и изменены другими функциями той же области действия, что легко может привести к неправильной работе.

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

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)
        })
    })
});

После использования промисов:

request(url)
.then(function(result) {
    return writeFileAsynv('1.txt', result)
})
.then(function(result) {
    return request(url2)
})
.catch(function(e){
    handleError(e)
});

А для примера чтения самого большого файла мы можем упростить его до:

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.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');

В этом примере с кэшем результат печати будет 1 2 3, а при отсутствии кэша результат печати будет 1 3 2.

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

Проще говоря, сосуществование синхронной и асинхронной программ не может гарантировать непротиворечивость логики программы.

Однако промисы решают эту проблему, давайте рассмотрим пример:

var promise = new Promise(function (resolve){
    resolve();
    console.log(1);
});
promise.then(function(){
    console.log(2);
});
console.log(3);

// 1 3 2

Даже если объект обещания сразу переходит в состояние разрешения, то есть функция разрешения вызывается синхронно, метод, указанный в функции then, все равно выполняется асинхронно.

В спецификации PromiseA+ также четко указано:

На практике убедитесь, что методы onFulfilled и onRejected выполняются асинхронно и должны выполняться в новом стеке выполнения после раунда цикла обработки событий, в котором был вызван метод then.

Обещание анти-шаблона

1. Вложение обещаний

// 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. Разорванная цепочка обещаний

// 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);
});

Если что-то ElseAsync выдает ошибку, ее невозможно поймать. Ты можешь написать:

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

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

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

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

Ограничения обещаний

1. Ошибки едят

В первую очередь надо понять, что за ошибка ест, значит ли это, что сообщение об ошибке не печатается?

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

throw new Error('error');
console.log(233333);

В этом случае из-за ошибки броска код блокируется и не будет напечатано 233333. Другой пример:

const promise = new Promise(null);
console.log(233333);

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

Еще один пример:

let promise = new Promise(() => {
    throw new Error('error')
});
console.log(2333333);

На этот раз он будет печатать нормально233333, указывая на то, что ошибка внутри промиса не повлияет на код вне промиса, и такая ситуация обычно называется «поеданием ошибки».

На самом деле, это не уникальное ограничение для промисов, то же самое и с try..catch, который также перехватит исключение и просто съест ошибку.

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

2. Одно значение

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

Серьезно, нет хорошего способа, предлагается использовать назначение деструктуризации ES6:

Promise.all([Promise.resolve(1), Promise.resolve(2)])
.then(([x, y]) => {
    console.log(x, y);
});

3. Невозможно отменить

Как только обещание создано, оно будет выполнено немедленно и не может быть отменено на полпути.

4. Невозможно узнать статус ожидания

В состоянии ожидания невозможно узнать, какой этап выполняется в данный момент (только что начался или вот-вот завершится).

Ссылаться на

  1. «Вы не знаете JavaScript, средний объем»
  2. N вариантов использования промисов
  3. Мини-книга «JavaScript Promises»
  4. Обещания/спецификация A+
  5. Как используются промисы
  6. Promise Anti-patterns
  7. Вопрос интервью о приложении Promise

Es6 серия

Адрес каталога серии ES6:GitHub.com/ в настоящее время имеет бриз…

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

Если есть какие-либо ошибки или неточности, пожалуйста, поправьте меня, большое спасибо. Если вам нравится или у вас есть вдохновение, добро пожаловать в звезду, что также является поощрением для автора.