首先先推荐一下某呆翻译的d3-force的中文文档:https://github.com/xswei/D3-V... 。
在咱们解读源码前还请读者先熟悉一下force相关的API,以及es6语法 .前端
若有分析不当之处还请留言指出,谢谢~node
那咱们进入正题吧git
咱们来看一下index.js 这个文件你们能够理解为force的对外统一出口。固然你也能够自定义使用这些模块。es6
// index.js export {default as forceCenter} from "./src/center"; // 设置力导图点阵中心 export {default as forceCollide} from "./src/collide"; // 碰撞 export {default as forceLink} from "./src/link"; export {default as forceManyBody} from "./src/manyBody"; export {default as forceSimulation} from "./src/simulation"; export {default as forceX} from "./src/x"; export {default as forceY} from "./src/y";
其余引用模块github
//collide.js import constant from "./constant"; // 构造常量函数 import jiggle from "./jiggle"; // 微小晃动随机数 import {quadtree} from "d3-quadtree"; // 四叉树
此处代码使用的是单例对象模式,读者要注意,切勿与类对象理解混了。性能优化
export default function(x, y) { var nodes; // 使用闭包构建私有变量,存储nodes。 if (x == null) x = 0; // 力导图中心位置 x 默认值为0 if (y == null) y = 0; // 力导图中心位置 y 默认值为0 // force 单例对象 function force() { var i, n = nodes.length, node, // 临时变量用于循环 sx = 0, // 临时变量用于计算 sy = 0; // 临时变量用于计算 for (i = 0; i < n; ++i) { // sx = sum(node.x); 节点x之和 // sy = sum(node.y); 节点y之和 node = nodes[i], sx += node.x, sy += node.y; } for (sx = sx / n - x, sy = sy / n - y, i = 0; i < n; ++i) { // sx / n 是点阵的中心x坐标;sy / n 是点阵的中心y坐标。 // node.x = node.x + (x - (sx / n)); 该计算与此表达式等价,这样读者应该更好理解; // 坐标加减即平移坐标,即将整个点阵中心平移到坐标(x,y) node = nodes[i], node.x -= sx, node.y -= sy; } } // 初始化,为nodes私有变量赋值 force.initialize = function(_) { nodes = _; }; // 若是传入参数x则设置x,不然返回当前力导图中心位置 x force.x = function(_) { return arguments.length ? (x = +_, force) : x; }; // 若是传入参数y则设置y,不然返回当前力导图中心位置 y force.y = function(_) { return arguments.length ? (y = +_, force) : y; }; return force; // 返回 force对象 }
// 构造一个返回参数值的常量函数 // let a = constant(123); a() 输出: 123 export default function(x) { return function() { return x; }; }
// jiggle.js // 微小晃动随机数 export default function() { return (Math.random() - 0.5) * 1e-6; // 1e-6 ==> 1*10的-6次方 }
import constant from "./constant"; // 构造常量函数 import jiggle from "./jiggle"; // 微小晃动随机数 import {quadtree} from "d3-quadtree"; // 四叉树 // vx vy 是指当前节点的运动速度 function x(d) { return d.x + d.vx; // 运动一步 x + vx } function y(d) { return d.y + d.vy; // 运动一步 y + vy } export default function(radius) { var nodes, radii, strength = 1, // 力度 iterations = 1; // radius 设置默认值,值类型为常量函数; if (typeof radius !== "function") radius = constant(radius == null ? 1 : +radius); // 单例对象模式 function force() { var i, n = nodes.length, tree, node, xi, yi, ri, // 半径 ri2; // 半径平方 // -------------- 四叉树相关,**后文有详细分析**---------- for (var k = 0; k < iterations; ++k) { // 以x,y访问器构建一个四叉树,即节点运动到下一步位置为坐标(就像咱们走夜路,探出一步试试看) // visitAfter是后序遍历树的节点,执行prepare为每一个节点求半径r,参数为各个节点, // 返回树的跟节点root。 tree = quadtree(nodes, x, y).visitAfter(prepare); // for循环普通遍历节点 for (i = 0; i < n; ++i) { node = nodes[i]; ri = radii[node.index], ri2 = ri * ri; // r平方(勾股定理用) xi = node.x + node.vx;// 运动一步 x + vx yi = node.y + node.vy;// 运动一步 y + vy // 前序遍历全部节点,apply返回true则不访问其子节点 tree.visit(apply); } } function apply(quad, x0, y0, x1, y1) { var data = quad.data, rj = quad.r, r = ri + rj;// 两个点与其做用域构成两个圆,请参考以前的文章,圆与圆的碰撞测验。 if (data) { // 存在data即叶子节点,每一个叶子节点为一个坐标点 if (data.index > node.index) { // 由于这是二重循环,全部index小于自身的点坐标已经与自身判断过了,此处是为了不重复测验 // 设第一重循环Node[i]为节点A(xi,yi) 第二重循环为节点B(data.x,data.y)下一步运动(+=vx,+=vy) var x = xi - data.x - data.vx, // Ax - Bx y = yi - data.y - data.vy, // Ay - By l = x * x + y * y; // 勾股定理 d^2 = x^2 +y^2 if (l < r * r) { // 判断是否碰撞,若是碰撞执行如下,l:实际距离平方,r:半径之和 if (x === 0) x = jiggle(), l += x * x; // 避免x值为0 if (y === 0) y = jiggle(), l += y * y; // 避免y值为0 // strength:碰撞力的强度,能够理解为两点之间的斥力系数 // 见后文碰撞测验的图 // l = 重叠长度/实际距离 * 碰撞力度 // 重叠约多,斥力越大。斥力影响点的运动速度 l = (r - (l = Math.sqrt(l))) / l * strength; // 根据求出的斥力计算AB点新的运动速度与方向 // A点x方向的运动速度 // A速度 += B速度 -= 使得AB两点往相反方向运动。注意,这里的x是B到A的距离,全部是A+= ,B-= // 但斥力的缘由会使得节点的vx ,vy 趋近于0. // node.vx = B-A点x方向距离 *= 斥力 * B半径平方(rj = B半径平方)/( A半径平方+B半径平方);r = B半径平方/( A半径平方+B半径平方) node.vx += (x *= l) * (r = (rj *= rj) / (ri2 + rj)); // 同x方向 node.vy += (y *= l) * r; data.vx -= x * (r = 1 - r); data.vy -= y * r; } } return; } // 若是是父节点,这里须要读者理解四叉树【后面一篇文章会讲解】 // 节点坐标为中心的正方形,若是没有覆盖到该父节点的正方形区域,这改点与此父节点的任何子节点都不会发生碰撞,则无需遍历其子节点校验。 // 返回true 不遍历子节点 // 这也是v4 相比v3对性能优化最重要的一个步骤,成倍的减小计算量 return x0 > xi + r || x1 < xi - r || y0 > yi + r || y1 < yi - r; } } // 遍历树节点过滤器,返回true节点不可见 function prepare(quad) { // quad.data是叶子节点才有的,因此这里是判断是不是叶子节点 if (quad.data) return quad.r = radii[quad.data.index]; for (var i = quad.r = 0; i < 4; ++i) { // 由于是后序遍历,因此节点的叶子节点必定在以前已经遍历过。 // 取叶子节点四个象限最大的r if (quad[i] && quad[i].r > quad.r) { quad.r = quad[i].r; } } } //--------------------------------------------------------------------------------------- function initialize() { if (!nodes) return; // 判断是否有节点 var i, n = nodes.length, node; radii = new Array(n); // 按照node.index索引排序nodes 并又 radius【后文解析】 计算出半径 后 存储在 radii for (i = 0; i < n; ++i) node = nodes[i], radii[node.index] = +radius(node, i, nodes); } force.initialize = function(_) { nodes = _; // 赋值节点 initialize(); // 初始化 }; force.iterations = function(_) { // get or set iterations (迭代次数) return arguments.length ? (iterations = +_, force) : iterations; }; force.strength = function(_) { // get or set strength(力度) return arguments.length ? (strength = +_, force) : strength; }; force.radius = function(_) { // 前端加+号 将字符串转为number +"123" === 123 // 有参数: // 执行1:(radius = typeof _ === "function" ? _ : constant(+_) //radius 值是一个返回自身的函数 // 执行2:initialize() // 执行3:return force // 无参数: // 执行:return radius return arguments.length ? (radius = typeof _ === "function" ? _ : constant(+_), initialize(), force) : radius; }; return force; }
碰撞测验闭包