d3js и функция первого класса

Передача функции в качестве аргумента другим функциям - трудная для понимания концепция для новичков, особенно для тех, кто использует другие языки. D3 еще больше усугубляет эту трудность, делая модель функций-аргументов неотъемлемой частью основного шаблона, но не заставляет проектировщика понимать, что, черт возьми, происходит, когда они вводят функцию в середине их код.

Вместо того, чтобы изучать, что делает основной код, некоторые просто слепо набирают…

// figure 4.1
  // ...
  .append('rect')
  .attr('fill', function(d,i){ return 'brown' })
  .attr('stroke', function(d,i){ return 'none' })

… Даже когда в этом явно нет необходимости. Клянусь, код, который выглядит так, как будто он написан каждый день начинающими и опытными разработчиками d3, потому что, хотя он явно не взрывает их код, он все равно работает. Это работает.

В этой статье мы постараемся убедиться, что это больше не повторится. Мы рассмотрим, что делает d3, когда вы передаете функцию вместо значения. К концу вы лучше поймете чудесно полезную часть javascript.

примечание: эту статью лучше всего читать в исходной уценке на моем репозитории Github

функция (г, я) {вернуть г; }

Почти весь код D3 принимает такую ​​форму:

Кодовая ссылка

// figure 4.2
var p = [ 3, 1, 4, 1, 5]
var svg = d3.select('body').append('svg')
var elements = svg.selectAll('rect#foo')
  .data(p)
  .enter()
    .append('rect')
    .attr('id', 'zomg')
    .attr('x', function(d){
      return (d*10.0) // what the heck is this?
    })
    .attr('y',0).attr('width',4).attr('height',200)

В приведенном выше коде мы создаем элемент svg. Затем добавьте к svg несколько элементов rect.

При создании этого элемента rect мы устанавливаем для атрибута id строку zomg. ‘zomg’ - это значение, например 0 или true. Это значение передается в качестве второго аргумента в вызове attr ().

В следующей строке кода, где мы вызываем attr и устанавливаем атрибут x, вместо передачи значения, такого как число 108.1, я передаю функция как аргумент.

attr (имя, значение)

attr (имя, функция () {возвращаемое значение})

как правило

// figure 4.3
// this is normal-ish looking code
function add(a,b){
  return (a+b)
}
var a = add(2,2)
// a = 4 (2 + 2)
var b = add(8,a)
// b = 12 (4 + 8)

Пишу функцию добавить. Затем я использую его, чтобы складывать вещи вместе.

// figure 4.4
// this is a little different
function add(a,b){
  return a() + b()
}
//               first argument         second argument
var a = add(function(){ return 3 }, function(){ return 1 })
// a = 4

Приведенный выше код странный. Зачем тебе вообще это нужно?

На самом деле нет смысла передавать эти функции в качестве аргументов, я просто делаю это, чтобы показать, что вы можете. Функции, которые я передаю, всегда возвращают одно и то же значение. Все, кроме 3 в этом первом аргументе, излишне.

Возможность передать функцию в качестве аргумента становится важной, когда вы делаете больше, чем просто return 3 в передаваемой функции. Что, если «функция аргумента» имеет сложное поведение, например, вызывает сервер, чтобы проверить, аутентифицирован ли пользователь?

Возможность передавать функцию в качестве аргумента другим функциям упрощает разделение задач. Это должно помочь вам создавать ваши программы более удобными для сопровождения.

Вы также, вероятно, уже делаете это, не осознавая этого.

// figure 4.5
// common jquery event code
$('#foo').click(function(){
  $('#bar').hide()
})

Приведенный выше код присоединяет обработчик событий к элементам DOM. Функция передается в качестве аргумента функции click, эта «функция аргумента» будет запускаться всякий раз, когда событие click происходит в элементе DOM с идентификатором foo.

// figure 4.6
// this is a lot different
function adder_maker(a){
  var c = a
  return function d(b){
    return c() + b
  }
}
var random_adder = adder_maker(
    function(){ return Math.random() }  // a, in adder_maker
  )
random_adder(1) // 1.1
random_adder(1) // 1.93
random_adder(1) // 1.03
var static_adder = adder_maker (
    function(){ return 4 }
  )
static_adder(1) // 5
static_adder(3) // 7

Приведенный выше код еще более странный. Функция, которую вы передаете в качестве аргумента a, получает переменную c. Затем возвращается еще одна функция d, которая оценивает c и добавляет то, что возвращается из c к b, и верните все это наконец.

Здесь мы назначаем возвращенную функцию d переменной random_adder.

Теперь, когда вы вызываете random_adder много раз, вы должны каждый раз получать новый номер. Это связано с тем, что Math.random () заново вычисляется при каждом вызове random_adder, поскольку ссылка на функцию a сохраняется как c и возвращается в функции d, которую мы назначаем переменной random_adder.

назад к d3

В первом примере…

// ...
.attr('x', function(d,i){
  return (d*10.0) // what the heck is this?
})
// ...

Почему мы передаем функцию второму аргументу attr? Эта функция, которую вы передаете в качестве аргумента, сохраняется и прикрепляется к элементу rect. (см. соответствующий исходный код d3).

Почему функция сохраняется и прикрепляется к элементу rect? Функция запускается для каждого элемента в массиве, который вы передали в data ().

// figure 4.2 (again)
var p = [ 3, 1, 4, 1, 5]
var svg = d3.select('body').append('svg')
var elements = svg.selectAll('rect#foo')
  .data(p)
  .enter()
    .append('rect')
    .attr('id', 'zomg')
    .attr('x', function(d){
      return (d*10.0) // what the heck is this?
    })
    .attr('y',0).attr('width',4).attr('height',200)

Поскольку у вас есть 5 элементов в массиве p, вы добавите 5 элементов rect с идентификатором foo. По одному для каждого элемента в массиве p.

Для каждого элемента rect, добавляемого к svg, анонимная встроенная функция обратного вызова, переданная в качестве второго аргумента функции attr, запускается, когда тег Атрибут x устанавливается для каждого прямоугольника. Каждый раз, когда эта функция запускается, элементы массива p передаются в качестве аргументов анонимной функции, и все, что возвращает эта функция, определяет значение атрибута x.

// figure 4.7
// example
// first rect is created
function(3){  // run the function, pass in p[0]
  return (3*10) // 30
}
// second rect is created
function(1){  // run the function, pass in p[1]
  return (1*10) // 10
}
// third rect is created
function(4){  // run the function, pass in p[2]
  return (4*10) // 40
}
// fourth rect is created
function(1){  // run the function, pass in p[3]
  return (1*10) // 10
}
// fifth rect is created
function(5){  // run the function, pass in p[4]
  return (5*10) // 50
}

Ниже приведены результаты выполнения кода из Рисунка 4.2.

// figure 4.8
<svg>
  <rect id='zomg' x=30 y=0 width=4 height=200></rect>
  <rect id='zomg' x=10 y=0 width=4 height=200></rect>
  <rect id='zomg' x=40 y=0 width=4 height=200></rect>
  <rect id='zomg' x=10 y=0 width=4 height=200></rect>
  <rect id='zomg' x=50 y=0 width=4 height=200></rect>
</svg>

зачем мучиться с этим?

более легкие переходы

Если вы следуете шаблону во время цикла обновление ›переход, вы можете просто снова вызвать data, передать новый массив, вызвать переход, а затем список атрибутов.

// figure 4.9
// update the bound data
elements.data([ 1, 2, 3, 6, 8])
elements.transition()
  .delay(1000)
  .duration(3000)
  .attr('x', function(d) { return (d*10.0) })

Теперь все элементы будут иметь обновленные атрибуты x и перейдут на новые позиции на экране. Код function (d) {return (d * 10.0)} будет снова вычислен для каждого элемента в массиве data.

// figure 4.10
// new x attributes from new data array [1,2,3,6,8]
<svg>  //           \/ here
  <rect id='zomg' x=10 y=0 width=4 height=200></rect>
  <rect id='zomg' x=20 y=0 width=4 height=200></rect>
  <rect id='zomg' x=30 y=0 width=4 height=200></rect>
  <rect id='zomg' x=60 y=0 width=4 height=200></rect>
  <rect id='zomg' x=80 y=0 width=4 height=200></rect>
</svg>

лучшее понимание javascript в целом

Если вы уже понимали функции как аргументы, присвоение функций значениям и общую идею функции как первоклассного гражданина, то мне очень жаль, если я разговаривал с вами свысока во время этой статьи.

Большинство программистов d3 начинают в мире, где это не является обычным шаблоном программирования. Обратные вызовы обычно являются областью сетевых операций и других операций ввода-вывода. Если вы полностью понимали шаблон обратного вызова javascript до того, как начали с D3, то у вас было на одно препятствие меньше на кажущейся крутой кривой обучения D3.

копировальная паста

window.onload = function () {… легко написать, и он работает, даже если вы не знаете, что он делает. Вам не нужно знать, что вы назначаете анонимную функцию обратного вызова обработчику событий окна. Ваш код, заключенный в квадратные скобки {}, будет работать нормально.

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