d3数据绑定源码学习

527 阅读3分钟

前言

数据绑定一直是d3的核心之一,好奇其神奇的实现,翻看了其源码,以此记录,其实并不难理解,

在看其原理实现前,最好先了解好d3 selection的工作机制update、enter、exit状态。再来看其源码也不难理解。

以下为源码链接:selection.data()——GitHub源码

本文章将整理其思路。

selection.data()简介

简单介绍selection.data()的功能:

  • 若对一个selection直接调用selection.data(),其会直接返回已经绑定的数据
  • 当传入value时,如selection.data(value),就会对selection进行数据绑定

了解完selection.data()的基本功能后,开始介绍原理思路。分为selection.data()与selection.data(value)两种情况,即获取已有data和进行数据绑定的两种情况。

selection.data()获取已绑定数据

当直接调用selection.data()时,其会返回已绑定的数据。

这个的原理非常简单,只要创建一个数组,对selection做一个遍历,把数据放进数组里,返回即可。

这里直接看对应完全的源码就可看懂。

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

selection.data(value)绑定数据

接下来是本文的重点。

首先,判断是否有使用key,如果没有的话就使用bindIndex,同时取得selection对应的_parents和_groups。

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

接着判断传过来的value是不是函数,若不是函数,就通过传参,返回闭包函数。

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

...
//constant.js源码:
export default function(x) {
  return function() {
    return x;
  };
}

然后就是遍历selection中的group。这里关键点用注释写出。

  //遍历groups
  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),//获取data数据
        dataLength = data.length,
        //创建enter、update、exit三个状态的数组
        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);//根据先前判断的绑定函数进行绑定
    //此处我们假设绑定的函数为bindIndex,即默认的通过index绑定
    //为方便起见把bindIndex函数写在此处,进入函数解析
    function bindIndex(parent, group, enter, update, exit, data) {
      var i = 0,//依靠i遍历group中的元素
          node,
          groupLength = group.length,
          dataLength = data.length;

      // Put any non-null nodes that fit into update.
      // Put any null nodes into enter.
      // Put any remaining data into enter.
      // 先对数据进行遍历
      for (; i < dataLength; ++i) {
        if (node = group[i]) {//若group[i]有DOM节点,则赋值成功,node = group[i]返回true,进行数据绑定
          node.__data__ = data[i];
          update[i] = node;//绑定后放置进update
        } else {//无group[i]无DOM节点,即node = undefined时进入此分支
          enter[i] = new EnterNode(parent, data[i]);设置占位符EnterNode作为占位符
        }
     }
      // Put any non-null nodes that don’t fit into exit.
      for (; i < groupLength; ++i) {//数据遍历完之后,若group有剩余,则作为exit节点
        if (node = group[i]) {
          exit[i] = node;
        }
      }
    }
    //以上,bind过程结束


    // 最后如注释所言,把enter node接到对应的update node后面,在插入节点时就不会使得
    // 一定插入在父容器的尾部。(个人还没遇到过这种应用场景,还望大佬举例)
    // 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;
      }
    }
  }//至此,一个group的循环结束,多个group的话则进行多层循环。

最后创建update,设置__exit__、enter,返回update。

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

至此结束。

总结

最后做了个流程图作为总结。

理解不到位之处,还请大佬赐教。

参考资料

selection.data([data[, key]])
selection.data()——GitHub源码
d3 constant.js ——GitHub源码