1

前言

最近在学习d3.js,刚开始到选择器这一节,有一个selection.data方法,发现这个方法第二个参数可传可不传,第二个参数是函数,但文档里面并没有第二个参数使用方式的讲解,很难找到列子,所以就干脆撸源码了。

这里values是数组,key为一个函数;大多数例子里面写的都是不传第二个函数参数,今天就传第二个参数的情况讲解。

先看一段代码

    <div class="Ford"></div>
    <div class="Jarrah"></div>
    <div class="Kwon"></div>
    <div class="Locke"></div>
    <div class="Reyes"></div>
    <div class="Shephard"></div>

body里面有这些div,每个上面有一个class。

    const data = [
        { key: "Locke", number: 4 },
        { key: "Reyes", number: 8 },
        { key: "Ford", number: 15 },
        { key: "Jarrah", number: 16 },
        { key: "Shephard", number: 23 },
        { key: "Kwon", number: 42 }
    ];

    d3.selectAll("div")
        .data(data, function (d) {
            return d ? d.key : this.className;
        }).text(d=>d.key)

这样就把数据根据class名字给填充了

    <div class="Ford">Ford</div>
    <div class="Jarrah">Jarrah</div>
    <div class="Kwon">Kwon</div>
    <div class="Locke">Locke</div>
    <div class="Reyes">Reyes</div>
    <div class="Shephard">Shephard</div>

然后就疑问了?why?为什么这么写,函数里面谁教你的这么写?你怎么知道要这么写?很遗憾,我没找到说明文档

那就看源码了,上一段代码

Selection.prototype = selection.prototype = {
  constructor: Selection,
  select: selection_select,
  selectAll: selection_selectAll,
  filter: selection_filter,
  data: selection_data,
  ...

这一段我们发现Selection.prototype里面有个data方法,对应的是selection_data,那我们就去找selection_data,

function selection_data(value, key) {
  if (!value) {
    data = new Array(this.size()), j = -1;
    this.each(function(d) { data[++j] = d; });
    return data;
  }

  var bind = key ? bindKey : bindIndex,
      parents = this._parents,
      groups = this._groups;

  if (typeof value !== "function") value = constant$1(value);

  for (var m = groups.length, update = new Array(m), enter = new Array(m), exit = new Array(m), j = 0; j < m; ++j) {
    var parent = parents[j],
        group = groups[j],
        groupLength = group.length,
        data = value.call(parent, parent && parent.__data__, j, parents),
        dataLength = data.length,
        enterGroup = enter[j] = new Array(dataLength),
        updateGroup = update[j] = new Array(dataLength),
        exitGroup = exit[j] = new Array(groupLength);

    bind(parent, group, enterGroup, updateGroup, exitGroup, data, key);

    // Now connect the enter nodes to their following update node, such that
    // appendChild can insert the materialized enter node before this node,
    // rather than at the end of the parent node.
    for (var i0 = 0, i1 = 0, previous, next; i0 < dataLength; ++i0) {
      if (previous = enterGroup[i0]) {
        if (i0 >= i1) i1 = i0 + 1;
        while (!(next = updateGroup[i1]) && ++i1 < dataLength);
        previous._next = next || null;
      }
    }
  }

  update = new Selection(update, parents);
  update._enter = enter;
  update._exit = exit;
  return update;
}

这个函数接受两个参数value, key;当key存在的时候var bind = key ? bindKey : bindIndex,这时候我们先知道这个bind赋值就是bindkey,就是绑定key,然后下面大概意思是
一个for循环,然后处理变量,然后生成了一个update,返回undate;
这个过程有几个参数传给了bind函数,

    var parent = parents[j],
    group = groups[j],
    groupLength = group.length,
    data = value.call(parent, parent && parent.__data__, j, parents),
    dataLength = data.length,
    enterGroup = enter[j] = new Array(dataLength),
    updateGroup = update[j] = new Array(dataLength),
    exitGroup = exit[j] = new Array(groupLength);

看过d3开头文档的人都应该是到enter,exit,group,update,就是选择器生成的对象;
enter意思就是数据个数比dom个数多出来的部分,将要插入的节点;
exit意思就是dom个数比数据个数比多出来的部分,将要删除的部分
update意思是将要更新的部分。
不明白的先看一下文档,再反过来看这个。
那么bind函数就是bindkey,看一下bindkey是啥

function bindKey(parent, group, enter, update, exit, data, key) {
  var i,
      node,
      nodeByKeyValue = {},
      groupLength = group.length,
      dataLength = data.length,
      keyValues = new Array(groupLength),
      keyValue;

  // Compute the key for each node.
  // If multiple nodes have the same key, the duplicates are added to exit.
  for (i = 0; i < groupLength; ++i) {
    if (node = group[i]) {
      keyValues[i] = keyValue = keyPrefix + key.call(node, node.__data__, i, group);
      if (keyValue in nodeByKeyValue) {
        exit[i] = node;
      } else {
        nodeByKeyValue[keyValue] = node;
      }
    }
  }

  // Compute the key for each datum.
  // If there a node associated with this key, join and add it to update.
  // If there is not (or the key is a duplicate), add it to enter.
  for (i = 0; i < dataLength; ++i) {
    keyValue = keyPrefix + key.call(parent, data[i], i, data);
    if (node = nodeByKeyValue[keyValue]) {
      update[i] = node;
      node.__data__ = data[i];
      nodeByKeyValue[keyValue] = null;
    } else {
      enter[i] = new EnterNode(parent, data[i]);
    }
  }

  // Add any remaining nodes that were not bound to data to exit.
  for (i = 0; i < groupLength; ++i) {
    if ((node = group[i]) && (nodeByKeyValue[keyValues[i]] === node)) {
      exit[i] = node;
    }
  }
}

可以看到它接收到这些个变量,然后三个循环,这三个循环都是围绕nodeByKeyValue,exit,那么先看第一个循环,

这路 groupLength 就是获取的dom节点的长度,group是一个dom数组;
nodeByKeyValue开始是一个空对象{}
exit先不用管他,意思就是生成将要移除的部分dom节点数组

keyValues[i] = keyValue = keyPrefix + key.call(node, node.__data__, i, group);

这句的意思
keyPrefix 内部一个常量吧 打印出来是 '$'
key是你传的函数参数
node = group[i]就是当前遍历的dom节点

function (d) {
            return d ? d.key : this.className;
        }

函数内部this指向node,函数可带三个参数node.__data__, i, group,

node.__data__ 是dom节点上的__data__,
i 索引
group是dom数组

当我们执行

d3.selectAll("div")
        .data(data, function (d) {
            return d ? d.key : this.className;
        }).text(d=>d.key)

的时候第一个遍历我们可以得到的结果是nodeByKeyValue

{
    $Ford: div.Ford
    $Jarrah: div.Jarrah
    $Kwon: div.Kwon
    $Locke: div.Locke
    $Reyes: div.Reyes
    $Shephard: div.Shephard
}

函数内的this.className是节点的className,这个时候每个节点上面还没有__data__ 属性,所以你打印d是undefined;

这时候我们的函数已经执行了5次,因为有5个dom节点。

记住nodeByKeyValue,后面有用

那么我们再看第二个循环,很明显遍历的是data,

for (i = 0; i < dataLength; ++i) {
    keyValue = keyPrefix + key.call(parent, data[i], i, data);
    if (node = nodeByKeyValue[keyValue]) {
      update[i] = node;
      node.__data__ = data[i];
      nodeByKeyValue[keyValue] = null;
    } else {
      enter[i] = new EnterNode(parent, data[i]);
    }
  }

与刚才的逻辑差不多,还是要执行我们的函数
这次第一个参数是data[i],先生成

keyValue = keyPrefix + key.call(parent, data[0], i, data);//$Locke
node = nodeByKeyValue[keyValue] // node=nodeByKeyValue.$Locke
node.__data__ = data[i]; // node.__data__ = { key: "Locke", number: 4 }

到这里已经看到数据已经跟dom对应了。

到目前为止函数执行了groupLength + dataLength次

至于其他代码相关代码,大家可以去看看,这次就不看了。

我的感觉:D3的源码写法并不高级,里面也会有冗余的代码。


强子
335 声望10 粉丝

尽人事,安天命!