Búsqueda de sitios web

javascript - Evitar apuntar al alcance del objeto de this dentro de una función en ES6


Por ejemplo, estoy ejecutando un proyecto con D3.js, importando módulos específicos y llamando a sus funciones.

Configuración:

  • TypeScript/ES6
  • Importando componentes específicos de D3
  • Angular 6

Tengo un objeto, en este caso una directiva angular, y dibujo algunos círculos en un lienzo SVG y quiero que activen una función en un evento de arrastre.

Fragmento de código reducido: Por favor, eche un vistazo a drawPoints() al final de este fragmento

import { ElementRef, HostListener, Output, EventEmitter, OnInit, Input, OnChanges, SimpleChanges, Directive } from '@angular/core';
import * as Selection from 'd3-selection';
import * as Shape from 'd3-shape';
import * as Random from 'd3-random';
import * as Drag from 'd3-drag';

import { Config } from './../models/config.model';
import { Point } from './../models/point.model';
import { Param } from './../models/param.model';

@Directive({
  selector: '[appCanvas]'
})
export class CanvasDirective implements OnInit, OnChanges {
  private canvas: any;
  private defs: any;
  private gradient: any;
  private svg: any;
  private expandedPoints: Point[];
  private drag: Point;
  public config: Config;

  @Input()
  private param: Param;

  @Output()
  private emitConfig: EventEmitter<Config>;

  @HostListener('window:resize', ['$event'])
  private onResize(event) {
    this.init();
  }

  constructor(
    private el: ElementRef
  ) {
    this.canvas = el.nativeElement;
    this.emitConfig = new EventEmitter();
  }

  ngOnInit() {
    intSvg();
    // ..
  }

  private initSvg() {
    if (!this.svg) {
      this.svg = Selection.select(this.canvas).append('svg');
    }
    this.svg
      .attr('width', this.config.width)
      .attr('height', this.config.height);
  }

  private drawPoints(points: Point[]) {
    points.forEach(point => {
      this.svg.append('circle')
        .attr('r', point.color ? 20 : 10)
        .attr('cx', point.x)
        .attr('cy', point.y)
        .attr('fill', point.color ? point.color : 'lightgray')
        .attr('stroke-width', !point.color ? 2 : 0)
        .attr('stroke', !point.color ? 'gray' : '')
        .call(Drag.drag()
          .on('drag', () => {
            // What to call here?
            // Selection.select(this) will not work
            // So how to target the correct „this“?
          }));
    });
  }
  // ...
}

El problema que ocurre es no poder alcanzar el this correcto dentro de la función de arrastre de los círculos añadidos.

Hay múltiples ejemplos, pero no funcionarán dentro de la clase, porque el argumento this está protegido.

Créditos al ejemplo de Mike Bostock https://bl.ocks.org/mbostock/22994cc97fefaeede0d861e6815a847e

Respuesta #1

Las bibliotecas más antiguas como D3 dependen del contexto dinámico de this en lugar de pasar todos los datos necesarios como argumentos y requieren usar el truco const self=this para alcanzar el this léxico en las devoluciones de llamada. El truco se considera obsoleto en ES6 pero es necesario en tales casos. Se debe usar una función regular en lugar de una flecha para obtener el contexto dinámico:

private drawPoints(points: Point[]) {
  const self = this;
  ...
    .call(Drag.drag()
      .on('drag', function (this: ProperContextTypeIfNecessary) {
        Selection.select(this);
        // class instance can be referred as `self`
      }));
});

Puede parecer inconsistente que la instancia de clase deba referirse como this en un lugar y como self en otro (esto no era un problema en ES5 porque se suponía que self se utilizaba completamente en tales casos para mantener la consistencia).

Como se explica en esta respuesta relacionada, una alternativa es una función envolvente que proporcionará la función de devolución de llamada el contexto de D3 como un parámetro, mientras que this aún puede referirse a la instancia de la clase. Las funciones flecha pueden ser utilizadas en este caso:

function contextWrapper(fn) {
    const self = this;

    return function (...args) {
        return fn.call(self, this, ...args);
    }
}

...

private drawPoints(points: Point[]) {
  const self = this;
  ...
    .call(Drag.drag()
      .on('drag', contextWrapper((d3Context: ProperContextTypeIfNecessary) => {
        Selection.select(d3Context);
        // class instance can be referred as `this`
      }));
});

Respuesta #2

Al evitar la popular sintaxis de función flecha en los componentes ES6 y usar la buena y antigua sintaxis function() {} en su lugar, se resolvió el problema. Las funciones flecha cambian las reglas del juego, porque dentro de estas funciones llamadas resulta que this no se reduce al contexto llamado de la función, sino que el contexto se extiende globalmente hasta el nivel de la clase.

Solución:

private drawPoints(points: Point[]) {
  points.forEach(point => {
    this.svg.append('circle')
      .attr('r', point.color ? 20 : 10)
      .attr('cx', point.x)
      .attr('cy', point.y)
      .attr('fill', point.color ? point.color : 'lightgray')
      .attr('stroke-width', !point.color ? 2 : 0)
      .attr('stroke', !point.color ? 'gray' : '')
      .call(Drag.drag()
        .on('drag', function() {
          console.log(this);
        }));
  });
}

Lectura adicional:

[...] y ES2015 [ES6] introdujo las funciones flecha que no proporcionan su propio enlace de this (conserva el valor de this del contexto léxico que lo rodea). rwaldron, abasao, martian2049 et. al, this, MDN web docs

Artículos relacionados