Cytoscape.js 布局启动指南:深入理解 layout.run() 与布局生命周期
2026/9/24 23:13:50 网站建设 项目流程
  • 数据可视化

【免费下载链接】cytoscape.js

Graph theory (network) library for visualisation and analysis

项目地址:https://gitcode.com/gh_mirrors/cy/cytoscape.js
点击查看免费下载

导读

在 Cytoscape.js 中,布局(layout)是决定图节点坐标的核心机制。本文聚焦于布局对象的run()方法,讲解如何启动同步(discrete)与异步(continuous)两类布局、如何通过layoutstart/layoutstop事件掌控布局生命周期,并结合仓库源码剖析run()背后的执行链路,帮助你正确编写可复现、可维护的布局调用代码。

一、布局是什么:先厘清run()的作用对象

Cytoscape.js 将布局设计为可插拔的扩展机制,其唯一职责是"为图中的节点设置位置"。布局通过cy.layout(options)eles.layout(options)创建,返回一个布局对象,而run()正是驱动该对象真正开始计算与落位的方法。

从布局介绍文档可以明确以下几点前提:

  • 布局作用于你指定的子图cy.layout()使用图中全部元素,eles.layout()仅作用于所选元素集合;元素的可见性等状态不影响其是否参与布局。
  • 每种布局有自己的位置算法,可通过 options 定制(例如力导向布局的边权重、间距因子、角度、重叠避免等)。
  • 无头(headless)实例运行时通常需要显式指定boundingBox,而渲染实例可由container的 DOM 尺寸推断范围。

在此基础上,run()的意义才得以展开——它是布局从"配置完成"走向"开始执行"的唯一入口。

二、layout.run()的核心语义:同步与异步的差异

根据run()方法文档:

如果布局是异步的(即连续型 continuous),调用layout.run()只是"启动"布局;而同步(即离散型 discrete)布局会在run()返回之前就完成。无论何时启动布局,都会触发layoutstart事件。

这段话定义了run()最关键的语义边界:

  • 同步 / 离散型布局layout.run()调用返回时,所有节点位置已经计算并设置完毕。典型代表是内置的randomgridcircleconcentricbreadthfirst等布局。
  • 异步 / 连续型布局run()立即返回,布局在后台持续迭代计算(通常是力导向物理模拟),位置随每一帧刷新,直到收敛或被手动停止。典型代表是cose(以及常见的第三方fcosecola等)。

以内置random布局为例,其 random.mjs 源码 中run()直接完成随机位置计算并通过layoutPositions()落位,整个过程在调用栈内同步完成:

RandomLayout.prototype.run = function(){ let options = this.options; let cy = options.cy; let eles = options.eles; let bb = math.makeBoundingBox( options.boundingBox ? options.boundingBox : { x1: 0, y1: 0, w: cy.width(), h: cy.height() } ); let getPos = function( node, i ){ return { x: bb.x1 + Math.round( Math.random() * bb.w ), y: bb.y1 + Math.round( Math.random() * bb.h ) }; }; eles.nodes().layoutPositions( this, options, getPos ); return this; // chaining };

cose布局的 cose.mjs 源码 则展示了异步路径:run()内部通过requestAnimationFrame驱动的frame()循环逐步执行物理模拟(step()),并在温度低于minTemp或达到numIter迭代上限时收敛。显然,这种情况下run()返回时布局仍在进行。

同步与异步的统一入口

有趣的是,库本身并不强制布局作者区分二者。extension.mjs 源码 显示,只要布局原型上定义了start()run()其中之一,另一个会自动生成并互相委托:

// either .start() or .run() is defined, so autogen the other if( layoutProto.start && !layoutProto.run ){ layoutProto.run = function(){ this.start(); return this; }; } else if( !layoutProto.start && layoutProto.run ){ layoutProto.start = function(){ this.run(); return this; }; }

因此无论内置布局还是第三方扩展,开发者都统一通过layout.run()启动。

三、生命周期事件:layoutstartlayoutstop

run()之所以重要,不仅因为它触发计算,更因为它串起了布局的完整生命周期事件。

layoutstart:布局启动

当布局启动时触发layoutstart。在集合级布局落位实现中,layoutPositions()的第一行便是:

layout.emit( { type: 'layoutstart', layout: layout } );

cose等连续布局的run()在开始迭代前也会显式发出该事件(见 cose.mjs)。此外,内置的null布局同样遵循此约定(null.mjs),说明layoutstart是所有布局的通用契约。

layoutstop:布局结束

文档进一步说明:

布局在完成或被人为停止(例如调用layout.stop())时,会触发layoutstop事件。开发者可以通过layout.on()监听该事件,或在布局选项中配置回调。

在同步路径下,layoutPositions()在设置完节点位置后按顺序发出layoutreadylayoutstop(collection/layout.mjs):

layout.one( 'layoutready', options.ready ); layout.emit( { type: 'layoutready', layout: layout } ); layout.one( 'layoutstop', options.stop ); layout.emit( { type: 'layoutstop', layout: layout } );

若开启了animate,则layoutstop会推迟到所有节点动画(以及可选的fit视图动画)的 Promise 全部 resolve 之后再触发(collection/layout.mjs):

Promise.all( layout.animations.map(function( ani ){ return ani.promise(); }) ).then(function(){ layout.one( 'layoutstop', options.stop ); layout.emit( { type: 'layoutstop', layout: layout } ); });

而连续布局被提前终止时,stop()方法会直接发出layoutstop(cose.mjs):

CoseLayout.prototype.stop = function(){ this.stopped = true; if( this.thread ){ this.thread.stop(); } this.emit( 'layoutstop' ); return this; // chaining };

注意extension.mjs中还有一个兜底逻辑:布局销毁时若未正常结束也会补发layoutstop(extension.mjs),确保监听方不会因异常终止而漏掉收尾事件。

四、实战:三种启动与监听方式

方式一:直接调用 + 事件监听

var layout = cy.layout({ name: 'random' }); layout.run();

对于后续需要感知结束时机(例如结束后执行导图、截图或统计)的场景,可监听layoutstop

var layout = cy.layout({ name: 'random' }); layout.on('layoutstop', function( event ){ console.log('layout 已结束'); }); layout.run();

方式二:在布局选项中配置回调

文档指出layoutstop也可以通过布局 options 中的回调绑定。内置布局默认选项中均定义了stop: undefined // callback on layoutstop(可参见 random.mjs 默认选项、breadthfirst.mjs、grid.mjs 等),cose的选项注释同样标注// Called on layoutstop(cose.mjs):

var layout = cy.layout({ name: 'cose', // 布局结束时触发(无论自然收敛还是被 stop() 终止) stop: function(){ console.log('cose 布局停止'); } }); layout.run();

由于底层通过layout.one('layoutstop', options.stop)绑定(见上文 collection/layout.mjs),该回调保证只触发一次,适合一次性收尾逻辑。

方式三:使用 Promise 链式等待(pon()

布局对象还暴露了pon()方法,可返回事件对应的 Promise,便于融入 async/await 流程:

var layout = cy.layout({ name: 'random' }); layout.pon('layoutstop').then(function( event ){ console.log('layoutstop promise fulfilled'); }); layout.run();
// 配合 async/await 的典型写法 async function runAndWait(){ var layout = cy.layout({ name: 'cose' }); var p = layout.pon('layoutstop'); layout.run(); await p; console.log('布局完成,可安全导出或截图'); }

中断连续布局:layout.stop()

对于异步连续布局,可以在任意时刻调用layout.stop()提前终止:

var layout = cy.layout({ name: 'cose' }); layout.run(); // 100ms 后强制停止 setTimeout(function(){ layout.stop(); }, 100);

调用后布局会置stopped标志、停止内部线程并发出layoutstop,之前注册的stop回调与pon('layoutstop')同样会生效。

五、从源码看run()的完整调用链

将文档描述与源码对照,可以得到layout.run()的完整行为链路:

  1. 开发者调用cy.layout(options)/eles.layout(options)创建布局对象;eles.layout()内部通过cy.makeLayout并把自身集合注入options.eles(collection/layout.mjs)。
  2. 调用layout.run()。对同步布局(如random),run()内完成位置计算后调用eles.nodes().layoutPositions(layout, options, fn)
  3. layoutPositions()依次:
    • 发出layoutstart(若启用动画则同时构建节点动画与 fit/zoom-pan 动画);
    • 应用spacingFactor缩放与transform位置变换(collection/layout.mjs);
    • 同步或动画式地设置节点位置;
    • 按需执行cy.fit/cy.zoom/cy.pan
    • 发出layoutready,随后发出layoutstop(动画模式下等待动画 Promise 完成)。
  4. 对异步布局(如cose),run()返回后由requestAnimationFrame驱动的模拟循环继续执行,收敛后走同样的layoutstop流程;被stop()中断时由stop()直接发出layoutstop

其中layoutPositionsspacingFactortransformanimateFilter的处理(collection/layout.mjs)是理解"布局选项如何影响最终落位"的关键——例如通过transform(node, newPos)可以在离散布局中整体改变流向,通过animateFilter可以只对部分节点做动画过渡。

六、常见误用与注意事项

  • 对异步布局假设"run() 后位置已就绪":连续布局调用run()后立刻读取节点坐标可能还是初始值,务必在layoutstop/pon('layoutstop')/stop回调中再处理后续逻辑。
  • 重复调用run():同一布局对象再次run()会重新触发layoutstart并重新计算;如需全新配置,建议重新cy.layout()
  • 事件监听时机layoutstartrun()内部同步发出(同步布局),因此监听器必须在run()之前通过on()或 options 回调注册,否则可能错过事件。
  • 无头模式下的boundingBox:headless 实例没有 DOM 容器推断尺寸,需在 options 中显式传入boundingBox(如{ x1, y1, x2, y2 }{ x1, y1, w, h }),否则部分布局会退化为默认范围(random布局默认以cy.width()/cy.height()为边界,见 random.mjs)。

结语

layout.run()是 Cytoscape.js 布局体系中"启动一切"的开关:对离散布局它同步交付结果,对连续布局它开启后台迭代,并通过layoutstart/layoutstop事件把开始与结束的时机完整暴露给开发者。理解同步与异步语义的差异,熟练运用on()、options 回调与pon()三种监听手段,再结合对layoutPositionsextension.mjs底层约定的认识,你就能在任意场景下稳妥地编排布局流程,避免时序竞争类 bug。

  • 数据可视化

【免费下载链接】cytoscape.js

Graph theory (network) library for visualisation and analysis

项目地址:https://gitcode.com/gh_mirrors/cy/cytoscape.js
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询