Шесть схем асинхронного программирования JS

JavaScript

предисловие

Мы знаем, что среда выполнения языка Javascript является «однопоточной». Это означает, что одновременно может выполняться только одна задача. Если задач несколько, они должны быть поставлены в очередь, предыдущая задача выполнена, а затем выполняется следующая задача.

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

Для решения этой проблемы язык Javascript делит режимы выполнения задач на два типа: синхронный и асинхронный. В этой статье в основном представлены несколько методов асинхронного программирования, и путем сравнения вы получите лучшее решение для асинхронного программирования!

Чтобы прочитать больше качественных статей, нажмитеБлог GitHub

1. Синхронный и асинхронный

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

Такое прерывистое выполнение называется асинхронным. Соответственно непрерывное выполнение называется синхронизацией

«Асинхронный режим» очень важен. На стороне браузера операции, которые занимают много времени, должны выполняться асинхронно, чтобы избежать зависания браузера.Лучший пример — операции Ajax. На стороне сервера «асинхронный режим» является даже единственным режимом, потому что среда выполнения является однопоточной, и если разрешить синхронное выполнение всех http-запросов, производительность сервера резко упадет и быстро перестанет отвечать. Далее мы представим шесть методов асинхронного программирования.

Во-вторых, функция обратного вызова (Callback)

Функция обратного вызова — это самый простой метод асинхронной работы. Следующий код является примером функции обратного вызова:

ajax(url, () => {
    // 处理逻辑
})

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

ajax(url, () => {
    // 处理逻辑
    ajax(url1, () => {
        // 处理逻辑
        ajax(url2, () => {
            // 处理逻辑
        })
    })
})

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

3. Мониторинг событий

В этом случае,Выполнение асинхронных задач зависит не от порядка кода, а от того, происходит событие или нет.

Ниже приведены две функции f1 и f2.Намерение программирования состоит в том, что f2 должна ждать, пока не будет выполнена f1, прежде чем она может быть выполнена. Во-первых, привяжите событие к f1 (здесь используется метод записи jQuery)

f1.on('done', f2);

Приведенная выше строка кода означает, что когда в f1 происходит событие done, выполняется f2. Затем перепишите f1:

function f1() {
  setTimeout(function () {
    // ...
    f1.trigger('done');
  }, 1000);
}

В приведенном выше коде f1.trigger('done') означает, что после завершения выполнения сразу же запускается событие done, тем самым начинается выполнение f2.

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

В-четвертых, опубликуйте и подпишитесь

Мы предполагаем, что существует «центр сигналов», и когда задача завершена, она «отправляет» сигнал в центр сигналов, а другие задачи могут «подписаться» на центр сигналов, чтобы знать, когда они могут начать выполнение. Это называется «шаблон публикации/подписки» (шаблон публикации-подписки), также известный как «шаблон наблюдателя» (шаблон наблюдателя).

Во-первых, f2 подписывает сигнал Done на сигнальный центр jQuery.

jQuery.subscribe('done', f2);

Тогда f1 переписывается следующим образом:

function f1() {
  setTimeout(function () {
    // ...
    jQuery.publish('done');
  }, 1000);
}

В приведенном выше коде jQuery.publish('done') означает, что после завершения выполнения f1 сигнал готовности передается в центр сигналов jQuery, тем самым запуская выполнение f2. После того, как f2 завершит выполнение, вы сможете отписаться (отписаться)

jQuery.unsubscribe('done', f2);

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

5. Обещание/A+

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

1. Три состояния обещания

  • Ожидание — исходное состояние экземпляра объекта Promise при его создании.
  • Выполнено---- можно понимать как успешное состояние
  • Отклонено ---- может быть понято как неисправное состояние

Это обещание никогда не может изменить состояние после перехода из состояния ожидания в другое состояние., например, когда состояние становится разрешенным, его нельзя снова изменить на Выполнено.

let p = new Promise((resolve, reject) => {
  reject('reject')
  resolve('success')//无效代码不会执行
})
p.then(
  value => {
    console.log(value)
  },
  reason => {
    console.log(reason)//reject
  }
)

Когда мы создаем промис, код внутри конструктора выполняется немедленно.

new Promise((resolve, reject) => {
  console.log('new Promise')
  resolve('success')
})
console.log('end')
// new Promise => end

2. Сцепленные вызовы промисов

  • Каждый вызов возвращает новый экземпляр Promise (поэтому его можно объединить в цепочку)
  • Если результат, возвращенный в then, является результатом, результат будет передан следующему успешному обратному вызову в then
  • Если есть исключение, а затем он перейдет к следующему, а затем обратный вызов неудачи
  • Если в этом случае используется return, возвращаемое значение будет обернуто Promise.resolve() (см. примеры 1, 2).
  • Никакие параметры не могут быть переданы в then, если нет, то они будут переданы следующему then (см. пример 3)
  • catch ловит неперехваченные исключения

Далее мы рассмотрим несколько примеров:

  // 例1
  Promise.resolve(1)
  .then(res => {
    console.log(res)
    return 2 //包装成 Promise.resolve(2)
  })
  .catch(err => 3)
  .then(res => console.log(res))
// 例2
Promise.resolve(1)
  .then(x => x + 1)
  .then(x => {
    throw new Error('My Error')
  })
  .catch(() => 1)
  .then(x => x + 1)
  .then(x => console.log(x)) //2
  .catch(console.error)
// 例3
let fs = require('fs')
function read(url) {
  return new Promise((resolve, reject) => {
    fs.readFile(url, 'utf8', (err, data) => {
      if (err) reject(err)
      resolve(data)
    })
  })
}
read('./name.txt')
  .then(function(data) {
    throw new Error() //then中出现异常,会走下一个then的失败回调
  }) //由于下一个then没有失败回调,就会继续往下找,如果都没有,就会被catch捕获到
  .then(function(data) {
    console.log('data')
  })
  .then()
  .then(null, function(err) {
    console.log('then', err)// then error
  })
  .catch(function(err) {
    console.log('error')
  })

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

ajax(url)
  .then(res => {
      console.log(res)
      return ajax(url1)
  }).then(res => {
      console.log(res)
      return ajax(url2)
  }).then(res => console.log(res))

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

6. Генераторы/выход

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

  • Синтаксически можно понять, что функция генератора представляет собой состояние состояния, которая инкапсулирует множественные внутренние состояния.
  • Помимо конечного автомата, функция Generator также является функцией генерации объекта обхода..
  • Функцию можно поставить на паузу, yield можно приостановить, можно запустить следующий метод, и каждый раз возвращается результат выражения после yield.
  • Само выражение yield не возвращает значение или всегда возвращает значение undefined.Следующий метод может принимать один параметр, который будет использоваться как возвращаемое значение предыдущего выражения yield..

Сначала рассмотрим пример:

function *foo(x) {
  let y = 2 * (yield (x + 1))
  let z = yield (y / 3)
  return (x + y + z)
}
let it = foo(5)
console.log(it.next())   // => {value: 6, done: false}
console.log(it.next(12)) // => {value: 8, done: false}
console.log(it.next(13)) // => {value: 42, done: true}

Результаты могут не совпадать с вашим воображением.Далее разбираем код построчно:

  • Во-первых, вызов функции Generator отличается от обычных функций, он возвращает итератор
  • При выполнении next в первый раз аргументы игнорируются, и функция приостанавливается на выходе (x + 1), поэтому она возвращает 5 + 1 = 6.
  • При выполнении next во второй раз в качестве возвращаемого значения предыдущего выражения yield будет использоваться входящий параметр 12. Если вы не передаете параметры, yield всегда будет возвращать значение undefined. В этот момент пусть y = 2 * 12, поэтому второй выход равен 2 * 12 / 3 = 8.
  • При следующем выполнении в третий раз входящий параметр 13 будет использоваться как возвращаемое значение предыдущего выражения yield, поэтому z = 13, x = 5, y = 24, что в сумме дает 42.

Давайте посмотрим на другой пример: есть три локальных файла, 1.txt, 2.txt и 3.txt, в каждом только по одному предложению, следующий запрос зависит от результата предыдущего запроса, и я хочу вызвать три файла в свою очередь через функцию Генератор

//1.txt文件
2.txt
//2.txt文件
3.txt
//3.txt文件
结束
let fs = require('fs')
function read(file) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, 'utf8', function(err, data) {
      if (err) reject(err)
      resolve(data)
    })
  })
}
function* r() {
  let r1 = yield read('./1.txt')
  let r2 = yield read(r1)
  let r3 = yield read(r2)
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
let it = r()
let { value, done } = it.next()
value.then(function(data) { // value是个promise
  console.log(data) //data=>2.txt
  let { value, done } = it.next(data)
  value.then(function(data) {
    console.log(data) //data=>3.txt
    let { value, done } = it.next(data)
    value.then(function(data) {
      console.log(data) //data=>结束
    })
  })
})
// 2.txt=>3.txt=>结束

Из приведенного выше примера мы видим ручную итерациюGeneratorФункция очень хлопотная, а логика реализации немного запутанная, но фактическая разработка в целом сотрудничает.coБиблиотека для использования.coЯвляется сборкой для Node.js и браузерным построителем инструментов управления процессами с помощью Promise, можно использовать более элегантный способ написания кода для неблокирующего.

УстановитьcoБиблиотеки просто:npm install co

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

function* r() {
  let r1 = yield read('./1.txt')
  let r2 = yield read(r1)
  let r3 = yield read(r2)
  console.log(r1)
  console.log(r2)
  console.log(r3)
}
let co = require('co')
co(r()).then(function(data) {
  console.log(data)
})
// 2.txt=>3.txt=>结束=>undefined

Мы можем решить проблему ада обратных вызовов с помощью функции Generator.Мы можем переписать предыдущий пример ада обратных вызовов в следующий код:

function *fetch() {
    yield ajax(url, () => {})
    yield ajax(url1, () => {})
    yield ajax(url2, () => {})
}
let it = fetch()
let result1 = it.next()
let result2 = it.next()
let result3 = it.next()

7. асинхронно/ожидание

1.async / ожидание Введение

С помощью async/await вы можете легко добиться того же, что и с генераторами и сопутствующими функциями.Он имеет следующие характеристики:

  • async/await реализован на основе Promise, его нельзя использовать для обычных callback-функций.
  • async/await, как и Promises, неблокирует.
  • async/await делает асинхронный код похожим на синхронный код, где и происходит его волшебство.

Если функция добавлена ​​async , то функция вернет Promise

async function async1() {
  return "1"
}
console.log(async1()) // -> Promise {<resolved>: "1"}

Функция-генератор вызывает по очереди три файла.Пример написан на языке async/await, который можно реализовать всего в нескольких словах.

let fs = require('fs')
function read(file) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, 'utf8', function(err, data) {
      if (err) reject(err)
      resolve(data)
    })
  })
}
async function readResult(params) {
  try {
    let p1 = await read(params, 'utf8')//await后面跟的是一个Promise实例
    let p2 = await read(p1, 'utf8')
    let p3 = await read(p2, 'utf8')
    console.log('p1', p1)
    console.log('p2', p2)
    console.log('p3', p3)
    return p3
  } catch (error) {
    console.log(error)
  }
}
readResult('1.txt').then( // async函数返回的也是个promise
  data => {
    console.log(data)
  },
  err => console.log(err)
)
// p1 2.txt
// p2 3.txt
// p3 结束
// 结束

2. Асинхронные/ожидающие одновременные запросы

Если вы запрашиваете два файла, это не имеет значения, вы можете передавать параллельные запросы

let fs = require('fs')
function read(file) {
  return new Promise(function(resolve, reject) {
    fs.readFile(file, 'utf8', function(err, data) {
      if (err) reject(err)
      resolve(data)
    })
  })
}
function readAll() {
  read1()
  read2()//这个函数同步执行
}
async function read1() {
  let r = await read('1.txt','utf8')
  console.log(r)
}
async function read2() {
  let r = await read('2.txt','utf8')
  console.log(r)
}
readAll() // 2.txt 3.txt

8. Резюме

1. Эволюционная история асинхронного программирования JS: обратный вызов -> обещание -> генератор -> асинхронный + ожидание

2. Реализация функции async/await состоит в том, чтобы обернуть функцию генератора и автоматического исполнителя в функцию.

3. Можно сказать, что async/await является идеальным асинхронным решением.

(1) Преимущества функции async/await по сравнению с Promise отражены в:

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

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

(2) Улучшение функции async/await по сравнению с функцией Generator отражено в следующих трех пунктах.:

  • Встроенный привод. Выполнение функции Generator должно полагаться на исполнителя, поэтому существует библиотека функций co, а у асинхронной функции есть собственный исполнитель. То есть,Выполнение асинхронной функции точно такое же, как и у обычной функции, только одна строка.

  • более широкая применимость. Согласно соглашению библиотеки функций, за командой yield может следовать только функция Thunk или объект Promise, в то время какЗа командой await асинхронной функции могут следовать объекты Promise и значения примитивных типов (числа, строки и логические значения, но это эквивалентно синхронной операции).

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

Справочная статья