Как сохранить правильный контекст `this» в JavaScript при использовании рекурсии, наследования и инкапсуляции

#javascript #events #ecmascript-6 #this

#javascript #Мероприятия #ecmascript-6 #это

Вопрос:

Я использую ES6 в FF (родной, я не использую переводчик). У меня есть следующая настройка класса:

Диаграмма классов

Обратите внимание, что я показываю только отношения на этой диаграмме, а не правильные типы и что-то еще.

Класс Canvas представляет холст HTML5, а класс Ellipse представляет Эллипс, нарисованный на холсте. Моя цель здесь в том, чтобы я мог перемещать мышь по холсту и получать события для фигур, которые я рисую, таких как эллипс. Поскольку вопрос не касается трансляции события, пожалуйста, предположите, что это работает так же хорошо, как и следовало ожидать с HTML-элементами. Однако я отмечу, что _propagate вызов — это то, что обрабатывает это, и он работает с рекурсией (объяснено позже).

Полное определение subscribe выглядит точно так же, как addEventListener : (eventName, callback, [isCapture]) .

Внутри canvas у меня есть следующий код:

 addObject(object){
    this.__children.unshift(object);

    console.log("Susbscribing to "   object.toString());
    object.subscribe(MouseEventType.MouseDown, this.objectMouseDown);
}
// ...
canvas.addObject(new Ellipse());
  

Итак, я добавляю многоточие в Canvas, и когда я это делаю, я подписываюсь на событие MouseDown эллипса. Обратите внимание, что на этом этапе console.log будет выводиться «Подписка на Ellipse»

Теперь для кода подписки это:

 subscribe(eventName, func, useCapture = false){
    // If the event exists, add the method, otherwise, throw an exception
    if(this._subscribers[eventName]){
        this._subscribers[eventName][useCapture].push(func);
        console.log(this.toString()   " has had somebody subscribe to "   eventName);
    }
    else{
        throw eventName   " is not a valid event";
    }
}
  

И снова, на этом этапе console.log правильно идентифицирует объект как Ellipse.

Наконец, мы переходим к __dispatchEvent коду, который является следующим:

  __dispatchEvent(eventName, eventData, isCapture = false){
    if(this._subscribers[eventName] amp;amp; this._subscribers[eventName][isCapture]){
        for(var func of this._subscribers[eventName][isCapture]){

            // Set the sender to `this`
            console.log("Sending "   eventName   " event from "   this.toString());
            eventData.sender = this;

            // setTimeout = run in new thread
            // Really weird syntax so things are kept in scope correctly
            // -- func is passed into an anonymous method, which then calls that function with the event data
            ((f) => setTimeout(() => f(eventData), 0))(func);
        }
    }
}
  

На этом этапе я хочу щелкнуть по многоточию, и я должен получить следующий поток:

  • Обнаружен щелчок мыши на элементе Canvas HTML5, и он отправлен в класс Canvas
  • Класс Canvas отправляет событие захвата MouseDown самому себе
  • Вызовы Canvas _propagate для эллипса
  • Ellipse отправляет событие захвата и всплывающее окно самому себе << Проблема заключается вот в чем
  • Canvas отправляет событие bubble самому себе

Достаточно просто и в значительной степени работает так же, как события мыши в HTML.

Итак, проблема в том, что в моем обратном вызове у меня есть этот код:

 objectMouseDown(e){
    console.log(e.sender.toString());
}
  

Я ожидаю, что я снова должен быть зарегистрирован «Ellipse», однако вместо этого я получаю «Canvas».

Следует отметить пару моментов: — Диспетчер сообщает, что отправляет событие Ellipse mouse-down — Вызов обратного вызова, похоже, не происходит до тех пор, пока Canvas get не выполнит обратный вызов для наведения курсора мыши (во время пузырьковой обработки) — Если я удалю подписку на наведение курсора мыши Canvas, тогда наведение курсора мыши Ellipse будет вызвано правильно.

Вот дамп журнала со стандартной настройкой:

 > Sending MouseDown event from Ellipse    << From the dispatch
> Sending MouseDown event from Canvas     << From the dispatch
> Canvas                                  << From the event handler
  

И, если я удалю наведение курсора мыши на Canvas

 > Sending MouseDown event from Ellipse    << From the dispatch
> Ellipse                                 << From the event handler
  

Пожалуйста, дайте мне знать, если я что-то недостаточно хорошо объяснил. Я предполагаю, что это как-то связано с тем, что JavaScript this действительно странный. Однако я не смог выяснить, как его правильно сохранить. Я даже пытался установить переменную-член на значение this , когда происходит подписка, поскольку на этом уровне все правильно, но это не сработало. И на данный момент я в растерянности.

Ответ №1:

Проблема, похоже, не имеет ничего общего с this конкретно, а с тем фактом, что вы откладываете выполнение обработчика событий и что вы разделяете eventData между несколькими __dispatchEvent вызовами.

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

Это именно то, что вы видите на выходе:

 > Sending MouseDown event from Ellipse    << From the dispatch
> Sending MouseDown event from Canvas     << From the dispatch
> Canvas                                  << From the event handler
  

Второй вызов будет установлен eventData.sender на объект canvas, следовательно, когда f(eventData) он будет вызван впоследствии на следующем тике, eventData.sender будет ссылаться на объект canvas.

В комментарии вы говорите

setTimeout = выполнить в новом потоке

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


Не зная больше о вашем коде, простым решением было бы отложить настройку отправителя до вызова обработчика:

 __dispatchEvent(eventName, eventData, isCapture = false){
    if(this._subscribers[eventName] amp;amp; this._subscribers[eventName][isCapture]){
        // Use `let` for block scope
        for(let func of this._subscribers[eventName][isCapture]){
            setTimeout(() => {
                // Set the sender to `this`
                console.log("Sending "   eventName   " event from "   this.toString());
                eventData.sender = this;
                func(eventData);
            }, 0);
        }
    }
}
  

Альтернативой было бы передать новый экземпляр eventData каждому обработчику.

Комментарии:

1. Спасибо! В итоге я просто поместил все в setTimeout на данный момент.