Научите, как реализовать Promise

Node.js JavaScript
Научите, как реализовать Promise

предисловие

Многие новички в JavaScript чувствовали страх перед адом обратных вызовов, и только когда они освоили синтаксис Promise, они почувствовали облегчение. Хотя многие языки уже имеют встроенные промисы, именно jQuery 1.5 действительно продвигает их в JavaScript.$.ajaxРефакторинг поддерживает Promise, а использование также совпадает с цепочкой вызовов, которую рекомендует jQuery. Позже родился ES6, и все начали вступать в эпоху универсальных промисов, а позже в ES8 появился асинхронный синтаксис, который сделал асинхронное написание JavaScript более элегантным.

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

Конструктор

в существующихPromise/A+Технические характеристикине указывает, откуда берется объект обещания, в jQuery, вызывая$.Deferred()Получить объект обещания В ES6 объект обещания получается путем создания экземпляра класса Promise. Здесь мы используем синтаксис ES для создания класса и возвращаем объект промиса путем создания экземпляра.Поскольку промис уже существует, мы временно назовем этот классDeferred.

class Deferred {
  constructor(callback) {
    const resolve = () => {
      // TODO
    }
    const reject = () => {
      // TODO
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }
}

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

Состояние обещаний

Обещания делятся на три состояния:

状态

  • pending: Ожидание, это начальное состояние Promise;pending
  • 🙆‍♂️fulfilled: все закончилось, состояние вызова разрешается нормально;fulfilled
  • 🙅‍♂️rejected: Отклонено, есть внутренняя ошибка или состояние после вызова reject;rejected

Мы видим, что промисы имеют состояние во время выполнения, хранящееся в[[PromiseState]]середина. Далее мы добавляем состояние в Deferred.

//基础变量的定义
const STATUS = {
  PENDING: 'PENDING',
  FULFILLED: 'FULFILLED',
  REJECTED: 'REJECTED'
}

class Deferred {
  constructor(callback) {
    this.status = STATUS.PENDING

    const resolve = () => {
      // TODO
    }
    const reject = () => {
      // TODO
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      // 出现异常直接进行 reject
      reject(error)
    }
  }
}

Здесь есть еще одна интересная вещь: состояние выполнения в ранней реализации браузера разрешено, что явно не соответствует спецификации Promise. Конечно, сейчас это исправлено.

Chrome Bug

Внутренние результаты

Помимо состояния внутри промиса есть результат[[PromiseResult]], используемый для временного хранения значений, принятых resolve/reject .

resolve result

reject result

Идем дальше и добавляем внутренний результат в конструктор.

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    const resolve = value => {
      this.value = value
      // TODO
    }
    const reject = reason => {
      this.value = reason
      // TODO
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      // 出现异常直接进行 reject
      reject(error)
    }
  }
}

сохранить обратный вызов

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

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    this.rejectQueue = []
    this.resolveQueue = []

    const resolve = value => {
      this.value = value
      // TODO
    }
    const reject = reason => {
      this.value = reason
      // TODO
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      // 出现异常直接进行 reject
      reject(error)
    }
  }
}

resolveа такжеreject

Изменить статус

Далее нам нужно реализовать методы resolve и reject, которые при вызове изменят состояние объекта обещания. И после вызова любого метода другой метод не может быть вызван.

new Promise((resolve, reject) => {
	setTimeout(() => {
    resolve('🙆‍♂️')
  }, 500)
  setTimeout(() => {
    reject('🙅‍♂️')
  }, 800)
}).then(
  () => {
    console.log('fulfilled')
  },
  () => {
    console.log('rejected')
  }
)

运行结果

В этот момент консоль просто выведетfulfilled, не появляетсяrejected.

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    this.rejectQueue = []
    this.resolveQueue = []

    let called // 用于判断状态是否被修改
    const resolve = value => {
			if (called) return
      called = true
      this.value = value
      // 修改状态
      this.status = STATUS.FULFILLED
    }
    const reject = reason => {
			if (called) return
      called = true
      this.value = reason
      // 修改状态
      this.status = STATUS.REJECTED
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      // 出现异常直接进行 reject
      reject(error)
    }
  }
}

Обратный звонок

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

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    this.rejectQueue = []
    this.resolveQueue = []

    let called // 用于判断状态是否被修改
    const resolve = value => {
			if (called) return
      called = true
      this.value = value
      // 修改状态
      this.status = STATUS.FULFILLED
      // 调用回调
      for (const fn of this.resolveQueue) {
        fn(this.value)
      }
    }
    const reject = reason => {
			if (called) return
      called = true
      this.value = reason
      // 修改状态
      this.status = STATUS.REJECTED
      // 调用回调
      for (const fn of this.rejectQueue) {
        fn(this.value)
      }
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      // 出现异常直接进行 reject
      reject(error)
    }
  }
}

Студенты, знакомые с системой событий JavaScript, должны знать,promise.thenОбратный вызов в методе будет помещен в очередь микрозадач, а затем вызван асинхронно.

MDN文档

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

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    this.rejectQueue = []
    this.resolveQueue = []

    let called // 用于判断状态是否被修改
    const resolve = value => {
			if (called) return
      called = true
      // 异步调用
      setTimeout(() => {
      	this.value = value
        // 修改状态
        this.status = STATUS.FULFILLED
        // 调用回调
        for (const fn of this.resolveQueue) {
          fn(this.value)
        }
      })
    }
    const reject = reason => {
			if (called) return
      called = true
      // 异步调用
      setTimeout(() =>{
        this.value = reason
        // 修改状态
        this.status = STATUS.REJECTED
        // 调用回调
        for (const fn of this.rejectQueue) {
          fn(this.value)
        }
      })
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      // 出现异常直接进行 reject
      reject(error)
    }
  }
}

затем метод

Затем нам нужно реализовать метод then.Учащиеся, которые использовали Promise, должны знать, что метод then может продолжать вызываться в цепочке, поэтому then должен возвращать объект обещания. Но когдаPromise/A+В спецификации четко оговорено, что метод then возвращает новый объект промиса, а не возвращает это напрямую, что мы можем проверить с помощью следующего кода.

then的结果

можно увидетьp1объект иp2Это два разных объекта, а то, что затем способ возвращаетсяp2Объекты также являются экземплярами промисов.

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

class Deferred {
  then(onResolve, onReject) {
    if (this.status === STATUS.PENDING) {
      // 将回调放入队列中
      const rejectQueue = this.rejectQueue
      const resolveQueue = this.resolveQueue
      return new Deferred((resolve, reject) => {
        // 暂存到成功回调等待调用
        resolveQueue.push(function (innerValue) {
          try {
            const value = onResolve(innerValue)
            // 改变当前 promise 的状态
            resolve(value)
          } catch (error) {
            reject(error)
          }
        })
        // 暂存到失败回调等待调用
        rejectQueue.push(function (innerValue) {
          try {
            const value = onReject(innerValue)
            // 改变当前 promise 的状态
            resolve(value)
          } catch (error) {
            reject(error)
          }
        })
      })
    } else {
      const innerValue = this.value
      const isFulfilled = this.status === STATUS.FULFILLED
      return new Deferred((resolve, reject) => {
        try {
          const value = isFulfilled
            ? onResolve(innerValue) // 成功状态调用 onResolve
            : onReject(innerValue) // 失败状态调用 onReject
          resolve(value) // 返回结果给后面的 then
        } catch (error) {
          reject(error)
        }
      })
    }
  }
}

Теперь, когда наша логика в основном работает, давайте попробуем запустить фрагмент кода:

new Deferred(resolve => {
  setTimeout(() => {
    resolve(1)
  }, 3000)
}).then(val1 => {
  console.log('val1', val1)
  return val1 * 2
}).then(val2 => {
  console.log('val2', val2)
  return val2
})

Через 3 секунды консоль показывает следующие результаты:

试运行

Как видите, это в основном соответствует нашим ожиданиям.

проникновение стоимости

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

new Deferred(resolve => {
  resolve(1)
})
  .then()
  .then()
  .then(val => {
    console.log(val)
  })

值穿透

Я не вижу никакого вывода в консоли, но переключаюсь на Promise, чтобы увидеть правильный результат.

值穿透

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

const isFunction = fn => typeof fn === 'function'

class Deferred {
  then(onResolve, onReject) {
    // 解决值穿透
    onReject = isFunction(onReject) ? onReject : reason => { throw reason }
    onResolve = isFunction(onResolve) ? onResolve : value => { return value }
    if (this.status === STATUS.PENDING) {
      // ...
    } else {
      // ...
    }
  }
}

值穿透

Теперь мы можем получить правильный результат.

шаг далеко

Теперь мы всего в одном шаге от идеальной реализации метода then, что мы и передаем при вызове метода then.onResolve/onRejectПри обратном вызове вам также необходимо оценить их возвращаемое значение. Что нам делать, если внутри обратного вызова возвращается объект обещания? Или если есть циклическая ссылка, что нам делать?

Мы получаемonResolve/onRejectПосле возвращаемого значения он вызывается напрямуюresolveилиresolve, и теперь нам нужно немного обработать их возвращаемые значения.

then(onResolve, onReject) {
  // 解决值穿透代码已经省略
  if (this.status === STATUS.PENDING) {
    // 将回调放入队列中
    const rejectQueue = this.rejectQueue
    const resolveQueue = this.resolveQueue
    const promise = new Deferred((resolve, reject) => {
      // 暂存到成功回调等待调用
      resolveQueue.push(function (innerValue) {
        try {
          const value = onResolve(innerValue)
-         resolve(value)
+         doThenFunc(promise, value, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
      // 暂存到失败回调等待调用
      rejectQueue.push(function (innerValue) {
        try {
          const value = onReject(innerValue)
-         resolve(value)
+         doThenFunc(promise, value, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
    })
    return promise
  } else {
    const innerValue = this.value
    const isFulfilled = this.status === STATUS.FULFILLED
    const promise = new Deferred((resolve, reject) => {
      try {
        const value = isFulfilled
        ? onResolve(innerValue) // 成功状态调用 onResolve
        : onReject(innerValue) // 失败状态调用 onReject
-       resolve(value)
+       doThenFunc(promise, value, resolve, reject)
      } catch (error) {
        reject(error)
      }
    })
    return promise
  }
}

возвращаемое оценочное суждение

Когда мы используем промис, мы часто возвращаем новый промис в методе then, а затем передаем внутренний результат нового промиса в последующий метод then.

fetch('server/login')
	.then(user => {
  	// 返回新的 promise 对象
  	return fetch(`server/order/${user.id}`)
	})
	.then(order => {
  	console.log(order)
	})
function doThenFunc(promise, value, resolve, reject) {
  // 如果 value 是 promise 对象
  if (value instanceof Deferred) {
    // 调用 then 方法,等待结果
    value.then(
      function (val) {
      	doThenFunc(promise, val, resolve, reject)
    	},
      function (reason) {
        reject(reason)
      }
    )
    return
  }
  // 如果非 promise 对象,则直接返回
  resolve(value)
}

Проверка циклических ссылок

Если возвращаемое значение функции обратного вызова текущего метода then представляет собой новый объект обещания, сгенерированный текущим методом then, он считается циклической ссылкой. Конкретные случаи следующие:

循环引用

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

循环引用

Поэтому нам нужно заранее предупреждать и вовремя выбрасывать ошибки.

function doThenFunc(promise, value, resolve, reject) {
  // 循环引用
  if (promise === value) {
    reject(
    	new TypeError('Chaining cycle detected for promise')
    )
    return
  }
  // 如果 value 是 promise 对象
  if (value instanceof Deferred) {
    // 调用 then 方法,等待结果
    value.then(
      function (val) {
      	doThenFunc(promise, val, resolve, reject)
    	},
      function (reason) {
        reject(reason)
      }
    )
    return
  }
  // 如果非 promise 对象,则直接返回
  resolve(value)
}

Теперь давайте попробуем вернуть новый объект обещания в then .

const delayDouble = (num, time) => new Deferred((resolve) => {
  console.log(new Date())
  setTimeout(() => {
    resolve(2 * num)
  }, time)
})

new Deferred(resolve => {
  setTimeout(() => {
    resolve(1)
  }, 2000)
})
  .then(val => {
    console.log(new Date(), val)
    return delayDouble(val, 2000)
  })
  .then(val => {
    console.log(new Date(), val)
  })

运行结果

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

Уловить метод

Метод catch на самом деле очень прост и эквивалентен сокращению метода then.

class Deferred {
  constructor(callback) {}
  then(onResolve, onReject) {}
  catch(onReject) {
    return this.then(null, onReject)
  }
}

статический метод

resolve/reject

Класс Promise также предоставляет два статических метода, которые напрямую возвращают объект обещания, состояние которого было исправлено.

class Deferred {
  constructor(callback) {}
  then(onResolve, onReject) {}
  catch(onReject) {}
  
  static resolve(value) {
    return new Deferred((resolve, reject) => {
      resolve(value)
    })
  }

  static reject(reason) {
    return new Deferred((resolve, reject) => {
      reject(reason)
    })
  }
}

all

Метод all принимает массив объектов обещаний и ожидает, пока состояние всех объектов обещаний в массиве не станетfulfilled, а затем вернуть результат, который также является массивом, и каждое значение массива соответствует внутреннему результату объекта обещания.

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

class Deferred {
  static all(promises) {
    // 非数组参数,抛出异常
    if (!Array.isArray(promises)) {
      return Deferred.reject(new TypeError('args must be an array'))
    }

		// 用于存储每个 promise 对象的结果
    const result = []
    const length = promises.length
    // 如果 remaining 归零,表示所有 promise 对象已经 fulfilled
    let remaining = length 
    const promise = new Deferred(function (resolve, reject) {
      // TODO
    })
		return promise
  }
}

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

class Deferred {
  static all(promises) {
    // 非数组参数,抛出异常
    if (!Array.isArray(promises)) {
      return Deferred.reject(new TypeError('args must be an array'))
    }

    const result = [] // 用于存储每个 promise 对象的结果
    const length = promises.length

    let remaining = length
    const promise = new Deferred(function (resolve, reject) {
      // 如果数组为空,则返回空结果
      if (promises.length === 0) return resolve(result)

      function done(index, value) {
        doThenFunc(
          promise,
          value,
          (val) => {
            // resolve 的结果放入 result 中
            result[index] = val
            if (--remaining === 0) {
              // 如果所有的 promise 都已经返回结果
              // 然后运行后面的逻辑
              resolve(result)
            }
          },
          reject
        )
      }
      // 放入异步队列
      setTimeout(() => {
        for (let i = 0; i < length; i++) {
          done(i, promises[i])
        }
      })
    })
		return promise
  }
}

Ниже мы используем следующий код, чтобы определить, правильна ли логика. Как и ожидалось, после выполнения кода через 3 секунды консоль выводит массив[2, 4, 6].

const delayDouble = (num, time) => new Deferred((resolve) => {
  setTimeout(() => {
    resolve(2 * num)
  }, time)
})

console.log(new Date())
Deferred.all([
  delayDouble(1, 1000),
  delayDouble(2, 2000),
  delayDouble(3, 3000)
]).then((results) => {
  console.log(new Date(), results)
})

all

Приведенные выше результаты в основном соответствуют нашим ожиданиям.

race

Метод гонки также принимает массив объектов обещаний, но ему нужно только одно обещание, которое становитсяfulfilledСтатус вернет результат.

class Deferred {
  static race(promises) {
    if (!Array.isArray(promises)) {
      return Deferred.reject(new TypeError('args must be an array'))
    }

    const length = promises.length
    const promise = new Deferred(function (resolve, reject) {
      if (promises.length === 0) return resolve([])

      function done(value) {
        doThenFunc(promise, value, resolve, reject)
      }

      // 放入异步队列
      setTimeout(() => {
        for (let i = 0; i < length; i++) {
          done(promises[i])
        }
      })
    })
    return promise
  }
}

Давайте изменим предыдущий случай проверки метода all на race. Как и ожидалось, после запуска кода через 1 секунду консоль выводит 2.

const delayDouble = (num, time) => new Deferred((resolve) => {
  setTimeout(() => {
    resolve(2 * num)
  }, time)
})

console.log(new Date())
Deferred.race([
  delayDouble(1, 1000),
  delayDouble(2, 2000),
  delayDouble(3, 3000)
]).then((results) => {
  console.log(new Date(), results)
})

race

Приведенные выше результаты в основном соответствуют нашим ожиданиям.

Суммировать

Упрощенная версия класса Promise уже реализована, некоторые детали здесь опущены, и доступен полный кодgithub. Появление Promise заложило прочный фундамент для более позднего асинхронного синтаксиса.В следующем блоге можно рассказать об истории асинхронного программирования в JavaScript, а для себя случайно вырыть яму. . .