// customPromise.js
var CustomPromise = (function getCustomPromise() {
/** 프로미스 상태 */
var PromiseState = {
pending: 'pending',
fulfilled: 'fulfilled',
rejected: 'rejected',
}
/**
* 프로미스 상태 이벤트 핸들러 생성자
* 프로미스의 상태를 관리하고, 상태관련 eventListener 생성과 dispatchEvent를 담당
*/
function PromiseStateEvent() {
this.state = PromiseState.pending
Object.defineProperty(this, 'eventTarget', {
value: undefined,
writable: true,
configurable: false,
enumerable: false
})
}
/**
* 현재 상태가 pending인지 확인
* @returns {boolean}
*/
PromiseStateEvent.prototype.isPending = function() {
return this.state === PromiseState.pending
}
/**
* 이벤트 타겟이 있는지 확인
* (다음 프로미스가 있는지 확인하는 용도)
* @returns {boolean}
*/
PromiseStateEvent.prototype.haveTarget = function() {
return this.eventTarget !== undefined
}
/**
* 프로미스 확정 시(fulfilled, rejected) 콜백함수 실행 eventListener 생성
* dispatchSettleEvent에서 인자로 넘어올 detail.result을 콜백함수에 넣어서 실행
* @param {string} state
* @param {function} callback
*/
PromiseStateEvent.prototype.addSettleEventListener = function(state, callback) {
if(!this.haveTarget()) this.eventTarget = new CustomEventTarget()
this.eventTarget.addEventListener(state, function(e) {
if(typeof callback === 'function') {
callback(e.detail.result)
} else {
throw new TypeError('PromiseStateEvent.addSettleEventListener parameter ' + callback + ' is not a function')
}
})
}
/**
* 프로미스 확정 시(fulfilled, rejected) 호출함수
* 상태를 바꾸고, 확정 이벤트에 result를 담아서 보낸다.
* @param {string} state
* @param {any} result
*/
PromiseStateEvent.prototype.dispatchSettleEvent = function(state, result) {
this.state = state
var options = {
detail: {
result: result
}
}
if(this.haveTarget()) this.eventTarget.dispatchEvent(new MyCustomEvent(state, options))
}
////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* IE에서 비동기를 처리하기 위해 Promise의 기능을 간단하게 구현한 객체생성자
* @param {function} executor
*/
function CustomPromise(executor) {
PromiseStateEvent.call(this)
this.result = undefined
// 함수가 아닌 매개변수(executor)가 들어왔을 때 TypeError를 던진다.
if(typeof executor !== 'function') {
throw new TypeError('CustomPromise resolver ' + executor + ' is not a function')
}
var resolvers = getResolvers()
var resolveFunc = resolvers.resolveFunc
var rejectFunc = resolvers.rejectFunc
var errorFunc = resolvers.errorFunc
var curPromise = this
// 실행순서를 가장 뒤로 보낸다.
setTimeout(function() {
try {
executor(resolveFunc.bind(curPromise), rejectFunc.bind(curPromise))
} catch(err) {
errorFunc.call(curPromise, err)
}
}, 0)
}
// PromiseStateEvent 프로토타입 상속
CustomPromise.prototype = Object.create(PromiseStateEvent.prototype)
/**
* 프로미스가 이행되었는지를 판단하는 isResolved 변수를 포함한, resolve와 reject 함수를 반환하는 함수
* @returns {object} { resolve, reject }
*/
function getResolvers() {
var isResolved = false // 프로미스 이행상태
var isRejected = false // 프로미스 거부상태
/**
* 프로미스 콜백함수가 성공 시 호출함수
* @param {any} result
*/
function resolveFunc(result) {
if(isResolved || isRejected) return
isResolved = true
// 매개변수로 프로미스가 들어오면 들어온 프로미스가 확정된 값을 result에 담는다.
if(result instanceof CustomPromise) {
var resolvers = getResolvers()
var resolveFuncInner = resolvers.resolveFunc
var rejectFuncInner = resolvers.rejectFunc
result.then(resolveFuncInner.bind(this), rejectFuncInner.bind(this))
return
}
settle.call(this, PromiseState.fulfilled, result)
}
/**
* 프로미스 콜백함수가 실패 시 호출함수
* @param {any} result
*/
function rejectFunc(result) {
if(isResolved || isRejected) return
isRejected = true
// 이상하지만 rejectFunc에 매개변수로 프로미스가 들어오면 resolveFunc과 달리 그대로 result에 넘겨준다. 2024-01-26
// if(result instanceof CustomPromise) {
// // 프로미스 분기
// return
// }
settle.call(this, PromiseState.rejected, result)
// 다음 프로미스가 없는 경우 에러
if(!this.haveTarget()) {
_throwError(result)
}
}
function errorFunc(err) {
if(isRejected && !this.haveTarget()) _throwError(err) // 프로미스가 거부되었으나 다음 프로미스가 없는 경우
if(isResolved || isRejected) return // 이미 이행 혹은 거부된 경우
isRejected = true
settle.call(this, PromiseState.rejected, err)
if(!this.haveTarget()) {
_throwError(err)
}
}
return { resolveFunc, rejectFunc, errorFunc }
}
/**
* 프로미스를 확정짓는 함수
* @param {string} state // PromiseState
* @param {any} result
*/
function settle(state, result) {
this.result = result
this.dispatchSettleEvent(state, result)
}
/**
* throw error
* @param {any} err
*/
function _throwError(err) {
if(err instanceof Error) {
throw err
} else {
throw new Error(err)
}
}
/**
* 프로미스 확정 후 실행 시킬 이행, 거부 콜백함수 설정
* 새로운 프로미스 인스턴스를 생성하고,
* 이행, 거부 콜백함수를 이벤트 리스너에 저장 후 프로미스 반환
* @param {function} onFulfilled
* @param {function} onRejected
*/
CustomPromise.prototype.then = function(onFulfilled, onRejected) {
// 다음 실행될 프로미스
var nextPromise = new CustomPromise(function() {})
// 현재 프로미스의 이행을 처리할 onFulfilled 콜백함수를 이벤트 리스너에 저장
this.addSettleEventListener(PromiseState.fulfilled, function(value) {
runOnSettled.call(nextPromise, PromiseState.fulfilled, onFulfilled, value)
})
// 현재 프로미스의 거부를 처리할 onRejected 콜백함수를 이벤트 리스너에 저장
this.addSettleEventListener(PromiseState.rejected, function(reason) {
runOnSettled.call(nextPromise, PromiseState.rejected, onRejected, reason)
})
// 이미 프로미스가 확정된 상태라면 바로 확정이벤트를 보낸다.
if(!this.isPending()) {
this.dispatchSettleEvent(this.state, this.result)
}
return nextPromise
}
/**
* 프로미스 확정 후 실행 시킬 거부 콜백함수 설정
* 새로운 프로미스 인스턴스를 생성하고,
* 거부 콜백함수를 이벤트 리스너에 저장 후 프로미스 반환
* 위 기능을 then(undefined, onRejected)로 대체
* @param {function} onFulfilled
* @param {function} onRejected
*/
CustomPromise.prototype.catch = function(onRejected) {
return this.then(undefined, onRejected)
}
/**
* 이전 CustomPromise의 비동기처리가 성공하고 넘어온 값을 현재 CustomPromise가 처리하는 로직
* onFulfilled가 함수라면 먼저 실행하고 그 결과값으로 _resolve를 실행, 함수가 아니라면 바로 _resolve를 실행
* onFulfilled의 실행 결과가 CustomPromise라면 그것을 처리한 후 _resolve또는 _reject를 실행
* 처리하는 중 error가 발생하면 _reject를 실행
* @param {string} state
* @param {function} onFulfilled
* @param {any} result
*/
function runOnSettled(state, onSettled, result) {
var resolvers = getResolvers()
var resolveFunc = resolvers.resolveFunc
var rejectFunc = resolvers.rejectFunc
var errorFunc = resolvers.errorFunc
if(typeof onSettled === 'function') {
var nextResult
try {
nextResult = onSettled(result)
} catch(err) {
errorFunc.call(this, err) // onSettled error 발생 시 _error로 값을 넘겨줌
}
// 만약 반환된 값이 CustomPromise라면 그 처리가 완료된 후 현재의 CustomPromise가 처리되도록 함
if(nextResult instanceof CustomPromise) {
nextResult.then(resolveFunc.bind(this), rejectFunc.bind(this))
} else {
resolveFunc.call(this, nextResult)
}
} else {
if(state === PromiseState.fulfilled) resolveFunc.call(this, result)
else rejectFunc.call(this, result)
}
}
return CustomPromise
})()
// myCustomEvent.js
/**
* 커스텀 이벤트 생성자
* @param {string} event
* @param {object} params
* @returns {object} customEvent
*/
var MyCustomEvent = (function setCustomEventConstructor() {
/**
* window.CustomEvent의 constructor를 지원하지 않을 때(IE) CustomEvent() 폴리필을 구현.
* MDN CustomEvent() Polyfill 참고.
* https://developer.mozilla.org/ko/docs/Web/API/CustomEvent/CustomEvent
*/
if (typeof window.CustomEvent !== "function") {
function MyCustomEvent ( event, params ) {
params = params || { bubbles: false, cancelable: false, detail: undefined };
var evt = document.createEvent( 'CustomEvent' );
evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail );
return evt;
}
MyCustomEvent.prototype = window.Event.prototype;
return MyCustomEvent
}
return window.CustomEvent
})()
// customEventTarget.js
/**
* 이벤트 타겟 생성자
* @returns {object} eventTarget
*/
var CustomEventTarget = (function setEventTargetConstructor() {
/**
* window.EventTarget의 constructor를 지원하지 않을 때(IE) EventTarget 폴리필을 구현.
* MDN EventTarget 구현 예제 참고.
* https://developer.mozilla.org/ko/docs/Web/API/EventTarget
*/
if(typeof window.EventTarget !== "function") {
var CustomEventTarget = function() {
this.listeners = {};
};
CustomEventTarget.prototype.listeners = null;
CustomEventTarget.prototype.addEventListener = function(type, callback) {
if (!(type in this.listeners)) {
this.listeners[type] = [];
}
this.listeners[type].push(callback);
};
CustomEventTarget.prototype.removeEventListener = function(type, callback) {
if (!(type in this.listeners)) {
return;
}
var stack = this.listeners[type];
for (var i = 0, l = stack.length; i < l; i++) {
if (stack[i] === callback){
stack.splice(i, 1);
return;
}
}
};
CustomEventTarget.prototype.dispatchEvent = function(event) {
if (!(event.type in this.listeners)) {
return true;
}
var stack = this.listeners[event.type].slice();
for (var i = 0, l = stack.length; i < l; i++) {
stack[i].call(this, event);
}
return !event.defaultPrevented;
};
return CustomEventTarget
}
return window.EventTarget
})()
예전에 IE환경에서 동작하는 Promise를 만들어야하는 일이 있었는데, 그때 기억을 되집어보며 다시 만들어 봤습니다.
이제는 IE 서비스도 종료되었고, 아무 의미도 없지만 한번쯤 재미로 만들어보는 것도 나쁘지 않았던 것 같습니다.
그래서 새롭게 Promise를 만들어보면서 공부한 내용을 한 번 정리해 보았습니다.
부족한 점을 알려주시는 댓글은 언제나 환영입니다.
반응형
'WEB > JS' 카테고리의 다른 글
(js)Promise만들기(3) - CustomEvent, EventTarget (0) | 2024.01.28 |
---|---|
(js)Promise만들기(2) - 구상 (0) | 2024.01.27 |
(js)마지막 달, 마지막 날 구하기 (0) | 2021.11.05 |
(js)만 나이 계산 (0) | 2021.11.05 |
(js) class 상속 연습 (0) | 2021.08.22 |