d3js 学习 :d3-selection(二)源代码阅读

一、文档资源

D3JS 中文文档:https://d3js.org.cn/document/
D3JS英文文档:https://github.com/d3/d3-selection/blob/main/README.md
D3选集(Selection)解析-附API文档翻译:https://juejin.cn/post/7004753817652854814

二、源码阅读

以下列代码为例,跟踪代码的执行。

var selection = d3.select("svg");
var data = [50,100];
var a = selection.selectAll("rect");
var b = a.data(data);
var c = b.join('rect');
var d=c.attr('x',d=>d).attr('y',d=>d);		
var e = d.attr("width",50).attr("height", 50);
var onClick = (e,d)=>console.log("clicked");
e.on("click",onClick);

该段时序生成了两个正方形,连长为50和100。生成结果如下,点击矩形在控制台打印"clicked"消息:
![在这里插入图片描述](https://img-blog.csdnimg.cn/d593bb95cb1a4cfda7e5b02896b9174a.png

1、 var selection=d3.select("svg")
// select.js
import {Selection, root} from "./selection/index.js";

export default function(selector) {
  return typeof selector === "string"
      ? new Selection([[document.querySelector(selector)]], [document.documentElement])
      : new Selection([[selector]], root);
}

如果是string,直接将selector传给dom方法 querySelector,根节点是document.documentElement ;所以querySelector支持的selector都可以使用。主要的selector有:

选择器用法
id选择器#myid
类选择器.myclassname
标签选择器div,h1,p
相邻选择器h1+p
子选择器ul > li
后代选择器li a
通配符选择器*
属性选择器a[rel=“external”]
伪类选择器a:hover, li:nth-child

如果参数不是string(比如 null 或者 function),直接传递selector,root( [null] ) 给Selection对象的构造函数。新生成的Selection对象在导入的时候就已定义,定义为:

// selection/index.js
Selection.prototype = selection.prototype = {
  constructor: Selection,
  select: selection_select,
  selectAll: selection_selectAll,
  selectChild: selection_selectChild,
  selectChildren: selection_selectChildren,
  filter: selection_filter,
  data: selection_data,
  enter: selection_enter,
  exit: selection_exit,
  join: selection_join,
  merge: selection_merge,
  selection: selection_selection,
  order: selection_order,
  sort: selection_sort,
  call: selection_call,
  nodes: selection_nodes,
  node: selection_node,
  size: selection_size,
  empty: selection_empty,
  each: selection_each,
  attr: selection_attr,
  style: selection_style,
  property: selection_property,
  classed: selection_classed,
  text: selection_text,
  html: selection_html,
  raise: selection_raise,
  lower: selection_lower,
  append: selection_append,
  insert: selection_insert,
  remove: selection_remove,
  clone: selection_clone,
  datum: selection_datum,
  on: selection_on,
  dispatch: selection_dispatch,
  [Symbol.iterator]: selection_iterator
};

Selection类的构造函数对参数的处理仅是将参数传递给了内部变量。

// selection/index.js
export function Selection(groups, parents) {
  this._groups = groups;
  this._parents = parents;
}

2、 var a = selection.selectAll("rect");

如果有rect,返回rect,如果无,返回new Selection的空集。

// selection/selectAll.js
export default function(select) {
  if (typeof select === "function") select = arrayAll(select);
  else select = selectorAll(select);

  for (var groups = this._groups, m = groups.length, subgroups = [], parents = [], j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
      if (node = group[i]) {
        subgroups.push(select.call(node, node.__data__, i, group));
        parents.push(node);
      }
    }
  }

  return new Selection(subgroups, parents);
}

selectAll首先判断select参数是否为function ,如果是,调用arrayAll方法:

// selection/selectAll.js
// select 参数为selection.selectAll中传入的function
function arrayAll(select) {
  return function() {
    return array(select.apply(this, arguments));
  };
}

这里提升了selectAll的功能,既能直接处理字符,也能处于函数,使用function的代码示例如下:

const sibling = d3.selectAll("p").selectAll(function() {
return [
    this.previousElementSibling,
    this.nextElementSibling   ]; }); 

这段代码可以自定义返回所选节点的兄弟节点。

如果不是,调用selectAll.js中的selectAll方法。
回到本例,select为’rect’,因此直接转向selectAll方法,该方法定义如下:

// selectorAll.js
export default function(selector) {
  return selector == null ? empty : function() {
    return this.querySelectorAll(selector);
  };
}

selector参数被转到dom的querySelectorAll方法。此时svg元素还没有rect元素。
然后进入两个for循环,生成新的元素集并指定父元素。用这两个参数生成新的selection。

for (var groups = this._groups, m = groups.length,
subgroups = [], parents = [], j = 0; j < m; ++j) {
    for (var group = groups[j], n = group.length, node, i = 0; i < n; ++i) {
      if (node = group[i]) {
        subgroups.push(select.call(node, node.__data__, i, group));
        parents.push(node); 

所以,每一次对selection使用selectAll方法,都会返回新的selection,selection===a的结果是false。

3、 var b = a.data(data);

这里完成了元素与data的绑定,返回新的selection,并附加了两个方法:enter和exit。
要实现绑定,内部需要处理很多工作,数据可能经现有元素多,也可以一样,也可能少。如果源数据比现有的元素多,则通过enter()返回多的这部元素集;如果少,现在的元素就得删除一些,通过exit()可以返回要删除的元素集。
3.1 主方法data方法

// selection/data.js
export default function(value, key) {
  //如果data()参数为空,则返回__data__数据(如有)
  if (!arguments.length) return Array.from(this, datum);

  //bind为指定按序号还是按指定的key访问(如传入的是{[k:v]}类型)
  var bind = key ? bindKey : bindIndex,
      parents = this._parents,
      groups = this._groups;//如文档中没有选择的元素,则为空集。

  //如果value不是function,则调用constant函数(用于封装传入的值为function)。即将value范式化为function,function用于返回传入的数据。
  if (typeof value !== "function") value = constant(value);

  //update, enter, exit用于存储更新、新进入、删除的元素。
  // 连续的selectAll方法,会使groups长度大于1。
  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,
        //返回数组形式,如[50,100]
        data = arraylike(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方法为bindKey或者bindIndex。
	// bindIndex按数据data和原有元素把不同节点放到对应容器(enter/update/exit)
	// enter存储了新的节点,定义为EnterNode,其中数据会被存到__data__
	// 由于enterGroup /updataGroup数组长度是一样的,对于非新入,非update的位置值为空。
    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/enter/exit各自存各自的数据,而且返回了一个新的Selection对象,
  //含有的是update的数据
  //绑定操作需要进一步进行(join)。  
  update = new Selection(update, parents);
  //链接至enter和exit方法,但还未执行。
  update._enter = enter;
  update._exit = exit;
  return update;
}

上述代码中的datum函数

function datum(node) {
   return node.__data__; 
} 

上述代码中的constant函数

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

上述代码中的arraylike函数

// data.js
function arraylike(data) {
 return typeof data === "object" && "length" in data
   ? data // Array, TypedArray, NodeList, array-like
   : Array.from(data); // Map, Set, iterable, string, or anything else
}

上述代码中的EnterNode

// enter.js
export function EnterNode(parent, datum) {
 this.ownerDocument = parent.ownerDocument;
 this.namespaceURI = parent.namespaceURI;
 this._next = null;
 this._parent = parent;
 this.__data__ = datum;
}

3.2 bindIndex方法

// data.js 
function bindIndex(parent, group, enter, update, exit, data) {
  //groups为空,
  var i = 0,
      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.
  // 根据要绑定的数据长度来看,由于是绑定index,所以如果原来有元素,就先更新;
  // 更新完如果data还没结束,就应该是enter。
  for (; i < dataLength; ++i) {
    if (node = group[i]) {
      node.__data__ = data[i];
      update[i] = node;
    } else {
      enter[i] = new EnterNode(parent, data[i]);
    }
  }

  // Put any non-null nodes that don’t fit into exit.
  // 此时i并未初始化,沿用上面的,指向了data之后。
  // 所以所以如果data结束了但原来的元素还有,就迭代并加入exit。
  for (; i < groupLength; ++i) {
    if (node = group[i]) {
      exit[i] = node;
    }
  }
}

3.3 bindKey方法

// selection/data.js
function bindKey(parent, group, enter, update, exit, data, key) {
  var i,
      node,
      nodeByKeyValue = new Map,
      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 = key.call(node, node.__data__, i, group) + "";
      if (nodeByKeyValue.has(keyValue)) {
        exit[i] = node;
      } else {
        nodeByKeyValue.set(keyValue, node);
      }
    }
  }

4、 var c = b.join('rect');

join方法接受onXXX系列3个参数(onenter, onupdate, onexit),参数可以是3个function(onenter还可以是string)。function可以被自动传入相应的Selection数据。最后返回的是合并的selection(如果有新增加的数据)或者更新后的selection。

  • 4.1 主代码
// selection/join.js
export default function(onenter, onupdate, onexit) {
  //执行enter()、exit()方法。获得对应的选择集。
  var enter = this.enter(), update = this, exit = this.exit();
  if (typeof onenter === "function") {
    enter = onenter(enter);
    if (enter) enter = enter.selection();
  } else {
    //enter是选择集,append是selection.append方法,最后会使用dom的appendChild方法添加元素
    enter = enter.append(onenter + "");
  }
  if (onupdate != null) {
    update = onupdate(update);
    if (update) update = update.selection();
  }
  if (onexit == null) exit.remove(); else onexit(exit);
  return enter && update ? enter.merge(update).order() : update;
}

上述代码中的enter和exit方法。

// selection/enter.js 
export default function() {  
  return new Selection(this._enter || this._groups.map(sparse),
  this._parents); 
}

// selection/exit.js 
export default function() {  
  return new Selection(this._exit || this._groups.map(sparse),
this._parents); }

  • 4.2 append方法
  • 用于根据选择集向dom树添加元素,使用dom的appendChild方法。
// selection/append.js
export default function(name) {
  var create = typeof name === "function" ? name : creator(name);
  return this.select(function() {
    return this.appendChild(create.apply(this, arguments));
  });
}

上述代码中的creator方法,进行命名空间处理,最后调用dom的createElement或者createElementNS生成新节点

// creator.js 
export default function(name) {
var fullname = namespace(name);
return (fullname.local
  ? creatorFixed
 : creatorInherit)(fullname);
}

creator代码中的namespace方法

// namespaces.js
export default function(name) {
 var prefix = name += "", i = prefix.indexOf(":");
 if (i >= 0 && (prefix = name.slice(0, i)) !== "xmlns") name = name.slice(i + 1);
 return namespaces.hasOwnProperty(prefix) ? {space: namespaces[prefix], local: name} : name; // eslint-disable-line no-prototype-builtins
}

creator代码中的creatorInherit方法

//creator.js
function creatorInherit(name) {
 return function() {
   var document = this.ownerDocument,
       uri = this.namespaceURI;
   return uri === xhtml && document.documentElement.namespaceURI === xhtml
       ? document.createElement(name)
       : document.createElementNS(uri, name);
 };
}

4.2 merge方法
合并enter和update选择集。因enter和update中只在对应位置存储新进入和更新的数据,而其他位置是空,用一个新的merges数据,所以通过遍历enter和update,可以合并非空值,实现数据和元素的同步。最后返回的是一个新Selection对象。

//selection/merge.js
export default function(context) {
  var selection = context.selection ? context.selection() : context;

  for (var groups0 = this._groups, groups1 = selection._groups, m0 = groups0.length, m1 = groups1.length, m = Math.min(m0, m1), merges = new Array(m0), j = 0; j < m; ++j) {
    for (var group0 = groups0[j], group1 = groups1[j], n = group0.length, merge = merges[j] = new Array(n), node, i = 0; i < n; ++i) {
      if (node = group0[i] || group1[i]) {
        merge[i] = node;
      }
    }
  }

  for (; j < m0; ++j) {
    merges[j] = groups0[j];
  }

  return new Selection(merges, this._parents);
}

5、var d=c.attr('x',d=>d).attr('y',d=>d);
6、 var e = d.attr("width",50).attr("height", 50);

这里会使用selection.each方法,迭代元素并调用回调函数。
回调函数处理了传入attr的参数,如果attr只有一个参数,则为返回属性值;如果是null,则删除属性;如果是function,执行function,根据返回结果删除属性(null)或者调用dom的setAttribute方法设置属性;如果是数值,将会被传递给setAttribute。
返回值是this,并未生成新选择集。

// selection/attr.js
export default function(name, value) {
  var fullname = namespace(name);

  if (arguments.length < 2) {
    var node = this.node();
    return fullname.local
        ? node.getAttributeNS(fullname.space, fullname.local)
        : node.getAttribute(fullname);
  }

  return this.each(
    (value == null?
      //如果为null,删除属性
     (fullname.local ? attrRemoveNS : attrRemove) 
     : 
     (
        //如果是function,调用attrFunction/attrFunctionNS,否则使用attrConstant/attrConstantNS
        typeof value === "function"? 
          (fullname.local ? attrFunctionNS : attrFunction)
          : 
          (fullname.local ? attrConstantNS : attrConstant)
     )
    )(fullname, value));
}

上述代码中的each函数

// selection/each.js 
export default function(callback) {

  for (var groups = this._groups, j = 0, m = groups.length; j < m; ++j) {
    for (var group = groups[j], i = 0, n = group.length, node; i < n; ++i) {
      if (node = group[i]) callback.call(node, node.__data__, i, group);
    }   }

  return this; }

7、 e.on("click",onClick);

on接受三个参数typename, value, options。typename是事件名,value是回调函数,options可选,是addEventListener的附加参数。
on函数最终会调用dom的addEventListener。如果参数小于2,由取消事件监听。使用each迭代,通过onAdd处理添加逻辑。

// selection/on.js
export default function(typename, value, options) {
  // 利用parseTypenames实现参数标准化
  var typenames = parseTypenames(typename + ""), i, n = typenames.length, t;

  if (arguments.length < 2) {
    var on = this.node().__on;
    if (on) for (var j = 0, m = on.length, o; j < m; ++j) {
      for (i = 0, o = on[j]; i < n; ++i) {
        if ((t = typenames[i]).type === o.type && t.name === o.name) {
          return o.value;
        }
      }
    }
    return;
  }

  
  on = value ? onAdd : onRemove;
  for (i = 0; i < n; ++i) this.each(on(typenames[i], value, options));
  return this;
}

上述代码中的parseTypenames和onAdd函数

// selection/on.js 
function parseTypenames(typenames) { 
  return typenames.trim().split(/^|\s+/).map(function(t) {
    var name = "", i = t.indexOf(".");
    if (i >= 0) name = t.slice(i + 1), t = t.slice(0, i);
    return {type: t, name: name};   }); }

function onAdd(typename, value, options) {   return
  function() {
    var on = this.__on, o, listener = contextListener(value);
    if (on) for (var j = 0, m = on.length; j < m; ++j) {
      if ((o = on[j]).type === typename.type && o.name === typename.name) {
        this.removeEventListener(o.type, o.listener, o.options);
        this.addEventListener(o.type, o.listener = listener, o.options = options);
        o.value = value;
        return;
      }
    }
    this.addEventListener(typename.type, listener, options);
    o = {type: typename.type, name: typename.name, value: value, listener: listener, options: options};
    if (!on) this.__on = [o];
    else on.push(o);   }; } 

三、总结

d3.selection实现了dom元素与数据的对应,将对数据的迭代转化为对dom元素的迭代操作,且能自动向回调函数传送数据,简化了复杂的for循环逻辑。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值