손 으로 Promise 를 실현 하 는 방법 을 알려 드릴 게 요.

20954 단어 Promise문법
머리말
많은 자 바스 크 립 트 의 초보 자 들 은 프로 미스 문법 을 익 혀 야 해탈 할 수 있다 는 지옥 지배 로 되 돌아 가 는 두려움 을 느 낀 적 이 있다.많은 언어 들 이 Promise 를 내 장 했 지만 자 바스 크 립 트 에서 이 를 진정 으로 빛 낸 것 은 jQuery 1.5 쌍$.ajax의 재 구성 으로 Promise 를 지 지 했 고 용법 도 jQuery 가 추앙 하 는 체인 호출 과 맞 아 떨 어 졌 다.그 후에 ES6 가 태 어 나 서 야 모두 가 전 국민 Promise 의 시대 에 들 어 섰 고 그 후에 ES8 은 async 문법 을 도입 하여 자 바스 크 립 트 의 비동기 적 인 문법 을 더욱 우아 하 게 만 들 었 다.
오늘 우 리 는 한 걸음 한 걸음 Promise 를 실현 할 것 입 니 다.만약 당신 이 Promise 를 사용 한 적 이 없다 면 먼저 Promise 문법 을 익히 고 본문 을 읽 는 것 을 권장 합 니 다.
구조 함수
기 존Promise/A+규범 에 서 는 promise 대상 이 어디에서 왔 는 지 규정 하지 않 았 으 며,jQuery 에 서 는 호출$.Deferred()을 통 해 promise 대상 을 얻 었 고,ES6 에 서 는 Promise 류 를 예화 하여 promise 대상 을 얻 었 다.여기 서 우 리 는 ES 의 문법 을 사용 하여 하나의 유형 을 구성 하고 실례 화 된 방식 으로 promise 대상 을 되 돌려 줍 니 다.Promise 가 이미 존재 하기 때문에 우 리 는 잠시 이 유형의 이름 을Deferred로 지 었 습 니 다.

class Deferred {
 constructor(callback) {
 const resolve = () => {
  // TODO
 }
 const reject = () => {
  // TODO
 }
 try {
  callback(resolve, reject)
 } catch (error) {
  reject(error)
 }
 }
}
구조 함수 가 콜백 을 받 아들 이 고 콜백 을 호출 할 때 resolve,reject 두 가지 방법 을 입력 해 야 합 니 다.
Promise 의 상태
Promise 는 모두 세 가지 상태 로 나 뉜 다.

  pending:대기 중 입 니 다.이것 은 Promise 의 초기 상태 입 니 다.
 fulfilled:끝 났 습 니 다.resolve 상 태 를 정상적으로 호출 합 니 다.
 
  rejected:거부 되 었 습 니 다.내부 에 오류 가 발생 하거나 reject 를 호출 한 후의 상태 입 니 다.

Promise 가 실행 되 는 동안[[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)
 }
 }
}
초기 브 라 우 저의 구현 중 fulfilled 상 태 는 resolved 로 Promise 규범 과 맞지 않 습 니 다.물론 지금 은 복구 되 었 습 니 다.

내부 결과
상 태 를 제외 하고 Promise 내부 에 resolve/reject 가 받 아들 인 값 을 임시 저장 하 는 결과[[PromiseResult]]가 있 습 니 다.


구조 함수 에 내부 결 과 를 계속 추가 합 니 다.

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)
 }
 }
}
저장 반전
Promise 를 사용 할 때 우 리 는 보통 promise 대상.then방법 을 호출 하여 promise 상태 가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 두 가지 방법 을 실현 해 야 한다.이 두 가지 방법 은 호출 될 때 promise 대상 의 상 태 를 바 꿀 것 이다.그리고 어떤 방법 이 호출 된 후에 다른 방법 은 호출 될 수 없다.

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)
 }
 }
}
호출 반전
상 태 를 수정 한 후 결 과 를 얻 은 promise 는 보통 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)
 }
 }
}
자 바스 크 립 트 이벤트 시스템 을 잘 아 는 학생 들 은promise.then방법 중의 리 셋 이 마이크로 퀘 스 트 대기 열 에 놓 인 다음 에 다른 단계 로 호출 된다 는 것 을 알 아야 한다.

따라서,우 리 는 리 셋 된 호출 을 비동기 대기 열 에 넣 어야 합 니 다.여기 서 우 리 는 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 방법
그 다음 에 우 리 는 then 방법 을 실현 해 야 한다.Promise 를 사용 한 학생 들 은 then 방법 이 체인 호출 을 계속 할 수 있다 는 것 을 알 고 있 을 것 이다.그래서 then 은 반드시 promise 대상 으로 돌아 가 야 한다.그러나Promise/A+규범 에 명확 한 규정 이 있 습 니 다.then 방법 은 this 로 직접 돌아 가 는 것 이 아니 라 새로운 promise 대상 으로 돌아 가 는 것 입 니 다.이 점 은 다음 코드 를 통 해 검증 할 수 있 습 니 다.
p1대상 과p2는 서로 다른 대상 이 고 then 방법 으로 되 돌아 오 는p2대상 도 Promise 의 인 스 턴 스 임 을 알 수 있다.
그 밖 에 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 을 호출 할 때 어떠한 인자 도 들 어 오지 않 으 면 규범 에 따라 현재 promise 의 값 은 다음 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리 턴 을 호출 할 때 그들의 반환 치 를 판단 해 야 한 다 는 것 이다.만약 반전 의 내부 가 promise 대상 으로 돌아간다 면 우 리 는 어떻게 처리 해 야 합 니까?혹은 순환 인용 이 나타 나 면 우 리 는 어떻게 처리 해 야 합 니까?
앞에서 우 리 는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
 }
}
반환 값 판단
우리 가 Promise 를 사용 할 때 는 then 방법 에서 새로운 Promise 를 되 돌려 주 고 새로운 Promise 가 완 성 된 내부 결 과 를 뒤의 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, value, resolve, reject)
  },
  function (reason) {
  reject(reason)
  }
 )
 return
 }
 //     promise   ,     
 resolve(value)
}
판단 순환 참조
현재 then 방법 리 셋 함수 리 턴 값 이 현재 then 방법 으로 생 성 된 새로운 promise 대상 이 라면 순환 참조 로 여 겨 집 니 다.구체 적 인 사례 는 다음 과 같 습 니 다.

then 방법 으로 되 돌아 오 는 새로운 promise 대상p1은 리 턴 에서 반환 값 으로 간주 되 며,이 때 이상 을 던 집 니 다.이전의 논리 에 따 르 면 코드 는 이 논리 에 계속 갇 혀 있 을 것 이기 때문이다.

그래서 우 리 는 사전에 예방 하고 제때에 잘못 을 던 져 야 한다.

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, value, resolve, reject)
  },
  function (reason) {
  reject(reason)
  }
 )
 return
 }
 //     promise   ,     
 resolve(value)
}
이제 then 에서 새로운 promise 대상 으로 돌아 가 보 자.

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 방법
catch 방법 은 사실 매우 간단 하 다.then 방법의 약자 에 해당 한다.

class Deferred {
 constructor(callback) {}
 then(onResolve, onReject) {}
 catch(onReject) {
 return this.then(null, onReject)
 }
}
정적 방법
resolve/reject
Promise 클래스 는 두 가지 정적 방법 을 제공 하여 상태 가 고정된 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 방법 은 하나의 promise 대상 의 배열 을 받 아들 이 고 배열 의 모든 promise 대상 의 상태 가fulfilled로 바 뀌 면 결 과 를 되 돌려 줍 니 다.그 결과 도 하나의 배열 이 고 배열 의 모든 값 은 promise 대상 의 내부 결과 에 대응 합 니 다.
우선,우 리 는 먼저 들 어 오 는 매개 변수 가 배열 인지 아 닌 지 를 판단 한 다음 에 결과 배열 과 새로운 promise 대상 을 구성 해 야 한다.

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
 }
}
그 다음 에 우 리 는 모든 promise 대상 의 resolve 를 차단 하고 매번 resolveremaining를 1 로 줄 여remaining를 0 으로 돌려 야 한 다 는 판단 을 해 야 한다.

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

위의 운행 결 과 는 기본적으로 우리 의 기대 에 부합된다.
race
race 방법 역시 promise 대상 의 배열 을 받 아들 이지 만 하나의 promise 가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)
})

위의 운행 결 과 는 기본적으로 우리 의 기대 에 부합된다.
총결산
간단 한 Promise 류 가 이미 실현 되 었 습 니 다.여 기 는 일부 세부 사항 을 생략 하고 전체 코드 는 github 에 접근 할 수 있 습 니 다.Promise 의 등장 은 후기의 async 문법 에 튼튼한 기반 을 다 져 주 었 다.다음 블 로 그 는 JavaScript 의 비동기 프로 그래 밍 사 에 대해 잘 이야기 할 수 있 고 실수 로 또 자신 에 게 구 덩이 를 파 주 었 다.
Promise 를 실현 하 는 방법 을 손 으로 가르쳐 주 는 이 글 은 여기까지 소개 되 었 습 니 다.더 많은 Promise 문법 내용 은 우리 의 이전 글 을 검색 하거나 아래 의 관련 글 을 계속 읽 어 보 세 요.앞으로 도 많은 응원 부 탁 드 리 겠 습 니 다!

좋은 웹페이지 즐겨찾기