JavaScript의 함수 ID 또는 이벤트 탐지기를 올바르게 삭제하는 방법

지난주에 나는 한 동료가 쓴 자바스크립트를 살펴보았는데, 재미있는 오류가 하나 발견되었다.흥미로운 것은 내가 어느 때 이 문제에 걸려 넘어졌다는 것을 알고 다른 사람의 코드에서 그것을 보았다는 것이다. 나는 이것이 정말 까다로운 문제라는 것을 발견했다. 많은 사람들이 열심히 해결하고 있기 때문에 나는 이것이 공유할 가치가 있다고 생각한다.
(나는 또한 그들을 위해 아주 긴 문장을 썼고 이 문제를 묘사하고 어떻게 해결하는지를 묘사했다. 이것은 본문의 기초이다.)
다음은 문제 코드의 첫 번째 부분입니다. (브라우저의 순수한 ES6는 프레임워크가 없습니다. 또한 이것은 원본 코드가 아닙니다. 제가 표현하고자 하는 관점과 무관한 모든 내용을 삭제했습니다.)

// For completeness, imagine these being something sensible:
let elements = document.querySelectorAll(/* ... */)
function mouseenterHandler(element) {
  // ...
}

// This is the interesting part:
elements.forEach(element => {
  element.addEventListener('mouseenter', () => {
    mouseenterHandler(element)
  })
})
일부 DOM 요소를 질의하고 각 요소에 이벤트 탐지기를 첨부합니다.
그리고 더 깊은 곳 어딘가에서 찢어지는 방식으로

elements.forEach(element => {
  element.removeEventListener('mouseenter', () => {
    mouseenterHandler(element)
  })
})
분명히 이것은 호출 removeEventListener 을 통해 이벤트 탐지기를 취소하려는 시도이며, 호출할 때 같은 효과의 익명 함수를 매개 변수로 사용합니다.

문제.() => { this.mouseenterHandler(element) }인치Ⓐ 익명 함수입니다. 인용을 보류하지 않습니다. (즉, 변수에 저장하지 않고, 어떤 방식으로도 이름을 제공하지 않습니다.)() => { this.mouseenterHandler(element) }인치Ⓑ 같은 값의 익명 함수다.여기서 주의해야 할 중요한 점은 그것들은 등가이지만 결코 같지 않다는 것이다.

JavaScript 함수 비교 방법
JavaScript의 함수는 다른 모든 객체와 마찬가지로 참조를 통해 비교되는 객체입니다.즉, JavaScript에서 두 함수의 등가성을 확인할 수 없습니다.

연재 안 했어요?
이제 자바스크립트는 함수를 서열화할 수 있는데 왜 문자열 표시를 통해 그것들을 비교하지 않겠는가?
let f1 = (x) => { return x + 1 }
let f2 = (x) => { return x + 1 }

console.log(f1.toString()) // '(x) => { return x + 1 }'
console.log(f2.toString()) // '(x) => { return x + 1 }'

// ... so:
console.log(f1.toString() === f2.toString()) // true - yay!?
그러나 이 함수는 약간 다르지만 등효라고 할 수 있는 함수를 생각해 보자.
function f3(x) {
  return x + 1
}

console.log(f3.toString()) // 'function f3(x) {\n  return x + 1\n}'
분명히 f1.toString() === f3.toString()f2.toString() === f3.toString()는 가짜일 것이다. f1(x) === f3(x)f2(x) === f3(x)는 [Number.MIN_SAFE_INTEGER,Number.MAX_SAFE_INTEGER - 1](그리고 x의 많은 다른 값) 중 어떤 주어진 x도 사실이지만 이것도 보잘것없다.
그래서 이런 방법은 완전히 같은 방식으로 쓴 함수에만 적용된다.

실제로 어떻게 했는지.
JavaScript에서 기본적으로 세 가지의 * 기본 데이터 형식은 변할 수 없다. 이것은 특이한comp-sci 방식으로 그들의 행위가 종이와 펜 수학에서의 행위와 유사하다는 것을 나타낸다.그중 하나는 Number형이다.수학에는 단지 하나의 숫자만 있다𝟐. 이 일을 이야기하는 것은 아무런 의의가 없다𝟐 여기랑 저거.𝟐 저쪽입니다.우리는 '을 쓸 수 있다𝟐우리가 원하는 모든 것은 동일한 숫자의 참조𝟐. JavaScript에서 동일한 방식으로 작동합니다.
let a = 2
let b = 2

console.log(a === b) // true
JS의 또 다른 두 가지 * 기본 데이터 유형은 StringBoolean이다.이것이 바로 우리가 등가성을 통해 f1,f2f3의 문자열을 비교할 수 있는 이유이다.
JavaScript의 다른 모든 컨텐트는 참조를 통해 비교됩니다.쓰기 [] 할 때마다 새 그룹을 만듭니다. 다음 쓰기 [] 할 때와 다른 그룹을 만듭니다. 쓰기 {} 할 때마다 새 대상을 만들고, 쓰기 () => {} 할 때마다 새 함수를 만듭니다.
(엄밀히 말하면 우리가 쓸 때마다[],{}() => {}가 아니라 매번 그 중 하나를 평가한다.이것은 사실상 매우 큰 차이이다.function makeObj () { return {} } 라는 함수를 상상해 보세요. makeObj() 호출할 때마다 새로운 대상을 되돌려줍니다.)
다시 말하면,
console.log([] === []) // false
console.log({} === {}) // false
console.log((() => {}) === (() => {})) // false, too!
그렇지만
let a1 = []
let a2 = a1
console.log(a2 === a1) // true
let o1 = {}
let o2 = o1
console.log(o2 === o1) // true
let f4 = () => {}
let f5 = f4
console.log(f5 === f4) // also true

우리 이벤트 리스트랑 무슨 상관이야?
JavaScript는 DOM의 모든 요소에 대해 하나의 그룹을 만들고 그 안에 모우스enter 탐지기를 저장합니다. 다음과 같습니다.
let myElementMouseenterListeners = []
매번 저희가 이벤트 감청기를 추가합니다.
myElement.addEventListener('mouseenter', () => { console.log('yay') })
JavaScript 내부는 배열에만 추가됩니다.
let myListenerToAdd = () => { console.log('yay') }

myElementMouseenterListeners.push(myListenerToAdd)

console.log(myElementMouseenterListeners) // [ [Function] ]
'mouseenter' 이벤트가 발생하면 JS에서 배열의 각 함수를 호출합니다.
let myMouseenterEvent = new MouseEvent('mouseenter')

myElementMouseenterListeners.forEach(mouseenterListener => {
  mouseenterListener(myMouseenterEvent)
})
이벤트 감청기를 삭제하려고 할 때, 자바스크립트는 이벤트 감청기 그룹에서 교체되며, 그 중의 모든 함수를 우리가 삭제하려고 하는 함수와 비교하고, 만약 꼭 같으면 그룹에서 삭제합니다.
우리가 이렇게 했다고 상상해 보세요.
myElement.removeEventListener('mouseenter', () => { console.log('yay') })
JavaScript는 다음을 수행할 수 있습니다.
let myListenerToRemove = () => { console.log('yay') }

for (let i = 0; i < myElementMouseenterListeners.length; i++) {
  if (myElementMouseenterListeners[i] === myListenerToRemove) {
    myElementMouseenterListeners.splice(i, 1)
    break
  }
}

console.log(myElementMouseenterListeners) // still [ [Function] ]
이것은 우리가 시작할 때 추가한 탐지기에 순환이 도착하면 우리가 제공한 탐지기removeEventListener와 비교하기 때문에 발생하는 기본적인 상황은 다음과 같다는 것을 의미한다.
() => { console.log('yay') } === () => { console.log('yay') }
우리가 전에 검사한 바와 같이, 그것의 평가 결과는 틀렸다.
이것은 코드가 이렇다는 것을 의미합니까
element.removeEventListener('eventname', () => { console.log('event handled') })
그때 새로 만든 익명 함수를 두 번째 매개 변수로 호출 removeEventListener 하면 영원히 아무런 효과가 없습니다.반대로 묵묵히 실패할 것이다.

우리가 무엇을 해야 하는가(가능한 해결 방안)removeEventListener를 효력을 발생시키기 위해서 우리는 반드시 하나의 함수에 대한 인용을 제공해야 한다. 이 함수는 우리가 이전에 addEventListener를 통해 실제 등록한 것이다.
일반적으로 말하면
let element = document.querySelector(/* ... */)
function mouseenterHandler() {
  // ...
}

element.addEventListener('mouseenter', mouseenterHandler)

element.removeEventListener('mouseenter', mouseenterHandler)
우리는 어느 곳에서든 같은 함수의 인용을 사용하기 때문에 removeEventListener을 호출할 때 비교this.mouseenterHandler === this.mouseenterHandler를 통해 삭제할 함수를 찾아내는 것이 옳다는 것을 알 수 있다.
현재의 '문제' 는 우리의 실제 값 mouseenterHandler 이 일반화되어 있다는 것이다. 원소를 매개 변수로 삼는 것이다.이것은 우리가 사용하고자 하는 모든 요소를 위해 새로운 mouseenterHandler 함수를 작성하는 것보다 틀림없이 더 좋을 것이다.그러나 현재 우리는 어떤 방식으로 파라미터를 그 안에 넣어야 하며, 익명 함수에 mouseenterHandler 호출을 포장하는 것은 여기에서 작용하지 않는다. 내가 위에서 상세하게 설명한 바와 같다.

솔루션 1: 각 요소에 대해 특수 버전의 이벤트 프로세서 만들기
우리는 mouseenterHandler의 전문 버전을 만들 수 있으며 elements을 채운 후에 이미 추가 파라미터가 생겼다.예를 들면 다음과 같습니다.
let elements = document.querySelectorAll(/* ... */)
let enhancedElements = []

elements.forEach(element => {
  enhancedElements.push({
    element,
    mouseenterHandler() { mouseenterHandler(element) },
  })
}
그리고 코드를 변경하고 처리 프로그램을

enhancedElements.forEach(ee => {
  ee.element.addEventListener('mouseenter', ee.mouseenterHandler)
})
각각 이입

enhancedElements.forEach(ee => {
  ee.element.removeEventListener('mouseenter', ee.mouseenterHandler)
})
이것은 작용하지만, 또한 모든 원소에 추가 대상과 함수를 만들 것이다. 만약 그렇게 많은 원소가 없다면, 이것은 문제가 되지 않을 수도 있지만, 여전히 더욱 우아한 방식이 있다.

솔루션 2: 이미 얻은 매개 변수를 처리하기 위해 이벤트 처리 프로그램을 변경합니다
브라우저는 이벤트를 첫 번째 매개 변수로 우리의 이벤트 처리 프로그램을 호출할 것입니다.이벤트는 여러 속성을 가진 대상일 뿐이고 그 중 하나는 event.target이며 이벤트가 발생하는 요소에 대한 인용이다.그러면, 왜 우리의 이벤트 처리 프로그램을 바꾸지 않고 그것을 사용합니까? 그러면 우리는 수동으로 원소를 거기에 고정시킬 필요가 없습니다.
예를 들어 MouseenterHandler가 다음과 같이 보일 경우
mouseenterHandler(element) {
  element.classList.add(/* ... */)
}
Dell은 이를 사용으로 변경할 수 있습니다event.target.
mouseenterHandler(event) {
  event.target.classList.add(/* ... */)
}
또는 매개 변수 목록에 destructuring을 사용하면 우리는 event. 부분을 중복할 필요가 없다.

mouseenterHandler({ target }) {
  target.classList.add(/* ... */)
}
이 해결 방안이 있으면 우리는 let elements = document.querySelectorAll(/* ... */)행을 원상태로 유지할 수 있다.추가 대상이나 기능이 필요하지 않습니다. 저희는 변경만 하면 됩니다.Ⓐ 입장:
elements.forEach(element => {
  element.addEventListener('mouseenter', mouseenterHandler)
})
및Ⓑ, 따라서
elements.forEach(element => {
  element.removeEventListener('mouseenter', mouseenterHandler)
})
우리의 이벤트 처리 프로그램은 현재 '유니버설' 으로 모든 요소와 함께 사용할 수 있습니다.
*거짓말했어요.undefined도 한 유형이다.
읽어주셔서 감사합니다!이것은 제가 dev.to에 올린 첫 번째 댓글입니다. 그리고 저는 영어를 모국어로 하는 사람이 아니기 때문에 스타일, 도움 등에 대한 일반적인 피드백에 감사하겠습니다.

좋은 웹페이지 즐겨찾기