当前位置: 首页 > news >正文

前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线

本章响应小伙伴的反馈,除了算法自动画连接线(仍需优化完善),实现了可以手动绘制直线、折线连接线功能。

请大家动动小手,给我一个免费的 Star 吧~

大家如果发现了 Bug,欢迎来提 Issue 哟~

github源码

gitee源码

示例地址

模式切换

在这里插入图片描述

前置工作

连接线 模式种类

// src/Render/types.ts
export enum LinkType {'auto' = 'auto','straight' = 'straight', // 直线'manual' = 'manual' // 手动折线
}

连接线 模式状态

// src/Render/draws/LinkDraw.ts// 连接线(临时)
export interface LinkDrawState {// 略linkType: Types.LinkType // 连接线类型linkManualing: boolean // 是否 正在操作拐点
}

连接线 模式切换方法

// src/Render/draws/LinkDraw.ts/*** 修改当前连接线类型* @param linkType Types.LinkType*/changeLinkType(linkType: Types.LinkType) {this.state.linkType = linkTypethis.render.config?.on?.linkTypeChange?.(this.state.linkType)}

连接线 模式切换按钮

<!-- src/App.vue --><button @click="onLinkTypeChange(Types.LinkType.auto)":disabled="currentLinkType === Types.LinkType.auto">连接线:自动</button>
<button @click="onLinkTypeChange(Types.LinkType.straight)":disabled="currentLinkType === Types.LinkType.straight">连接线:直线</button>
<button @click="onLinkTypeChange(Types.LinkType.manual)":disabled="currentLinkType === Types.LinkType.manual">连接线:手动</button>

连接线 模式切换事件

// src/App.vue
const currentLinkType = ref(Types.LinkType.auto)function onLinkTypeChange(linkType: Types.LinkType) {(render?.draws[Draws.LinkDraw.name] as Draws.LinkDraw).changeLinkType(linkType)
}

当前 连接对(pair) 记录当前 连接线 模式

// src/Render/draws/LinkDraw.tsexport class LinkDraw extends Types.BaseDraw implements Types.Draw {// 略override draw() {// 略// 连接点for (const point of points) {// 略// 非 选择中if (group && !group.getAttr('selected')) {// 略const anchor = this.render.layer.findOne(`#${point.id}`)if (anchor) {// 略circle.on('mouseup', () => {if (this.state.linkingLine) {// 略// 不同连接点if (line.circle.id() !== circle.id()) {// 略if (toGroup) {// 略if (fromPoint) {// 略if (toPoint) {if (Array.isArray(fromPoint.pairs)) {fromPoint.pairs = [...fromPoint.pairs,{// 略linkType: this.state.linkType // 记录 连接线 类型}]}// 略}}}}// 略}})// 略}}}}
}

直线

在这里插入图片描述

绘制直线相对简单,通过判断 连接对(pair)记录的 连接线 模式,从起点绘制一条 Line 到终点即可:

// src/Render/draws/LinkDraw.tsexport class LinkDraw extends Types.BaseDraw implements Types.Draw {// 略override draw() {// 略// 连接线for (const pair of pairs) {if (pair.linkType === Types.LinkType.manual) {// 略,手动折线} else if (pair.linkType === Types.LinkType.straight) {// 直线if (fromGroup && toGroup && fromPoint && toPoint) {const fromAnchor = fromGroup.findOne(`#${fromPoint.id}`)const toAnchor = toGroup.findOne(`#${toPoint.id}`)// 锚点信息const fromAnchorPos = this.getAnchorPos(fromAnchor)const toAnchorPos = this.getAnchorPos(toAnchor)const linkLine = new Konva.Line({name: 'link-line',// 用于删除连接线groupId: fromGroup.id(),pointId: fromPoint.id,pairId: pair.id,linkType: pair.linkType,points: _.flatten([[this.render.toStageValue(fromAnchorPos.x),this.render.toStageValue(fromAnchorPos.y)],[this.render.toStageValue(toAnchorPos.x), this.render.toStageValue(toAnchorPos.y)]]),stroke: 'red',strokeWidth: 2})this.group.add(linkLine)}} else {// 略,原算法画连接线逻辑}}}
}

折线

在这里插入图片描述

绘制折线,先人为定义 3 种“点”:
1、连接点,就是原来就有的。
2、拐点(待拐),蓝色的,从未拖动过的,一旦拖动,会新增拐点记录。
3、拐点(已拐),绿色的,已经拖动过的,依然可以拖动,但不会新增拐点记录。

在这里插入图片描述

请留意下方代码的注释,关键:

  • fromGroup 会记录 拐点 manualPoints。
  • 连接线 的绘制是从 起点 -> 拐点(们)-> 终点(linkPoints)。
  • 拐点正在拖动时,绘制临时的虚线 Line。
  • 分别处理 拐点(待拐)和 拐点(已拐)两种情况。

处理 拐点(待拐)和 拐点(已拐)主要区别是:

  • 处理 拐点(待拐),遍历 linkPoints 的时候,是成对遍历的。
  • 处理 拐点(已拐),遍历 linkPoints 的时候,是跳过 起点 和 终点 的。
  • 拖动 拐点(待拐),会新增拐点记录。
  • 拖动 拐点(已拐),不会新增拐点记录。
// src/Render/draws/LinkDraw.tsexport class LinkDraw extends Types.BaseDraw implements Types.Draw {// 略override draw() {// 略// 连接线for (const pair of pairs) {if (pair.linkType === Types.LinkType.manual) {// 手动折线if (fromGroup && toGroup && fromPoint && toPoint) {const fromAnchor = fromGroup.findOne(`#${fromPoint.id}`)const toAnchor = toGroup.findOne(`#${toPoint.id}`)// 锚点信息const fromAnchorPos = this.getAnchorPos(fromAnchor)const toAnchorPos = this.getAnchorPos(toAnchor)// 拐点(已拐)记录const manualPoints: Array<{ x: number; y: number }> = Array.isArray(fromGroup.getAttr('manualPoints'))? fromGroup.getAttr('manualPoints'): []// 连接点 + 拐点const linkPoints = [[this.render.toStageValue(fromAnchorPos.x),this.render.toStageValue(fromAnchorPos.y)],...manualPoints.map((o) => [o.x, o.y]),[this.render.toStageValue(toAnchorPos.x), this.render.toStageValue(toAnchorPos.y)]]// 连接线const linkLine = new Konva.Line({name: 'link-line',// 用于删除连接线groupId: fromGroup.id(),pointId: fromPoint.id,pairId: pair.id,linkType: pair.linkType,points: _.flatten(linkPoints),stroke: 'red',strokeWidth: 2})this.group.add(linkLine)// 正在拖动效果const manualingLine = new Konva.Line({stroke: '#ff0000',strokeWidth: 2,points: [],dash: [4, 4]})this.group.add(manualingLine)// 拐点// 拐点(待拐)for (let i = 0; i < linkPoints.length - 1; i++) {const circle = new Konva.Circle({id: nanoid(),pairId: pair.id,x: (linkPoints[i][0] + linkPoints[i + 1][0]) / 2,y: (linkPoints[i][1] + linkPoints[i + 1][1]) / 2,radius: this.render.toStageValue(this.render.bgSize / 2),stroke: 'rgba(0,0,255,0.1)',strokeWidth: this.render.toStageValue(1),name: 'link-manual-point',// opacity: 0,linkManualIndex: i // 当前拐点位置})// hover 效果circle.on('mouseenter', () => {circle.stroke('rgba(0,0,255,0.8)')document.body.style.cursor = 'pointer'})circle.on('mouseleave', () => {if (!circle.attrs.dragStart) {circle.stroke('rgba(0,0,255,0.1)')document.body.style.cursor = 'default'}})// 拐点操作circle.on('mousedown', () => {const pos = circle.getAbsolutePosition()// 记录操作开始状态circle.setAttrs({// 开始坐标dragStartX: pos.x,dragStartY: pos.y,// 正在操作dragStart: true})// 标记状态 - 正在操作拐点this.state.linkManualing = true})this.render.stage.on('mousemove', () => {if (circle.attrs.dragStart) {// 正在操作const pos = this.render.stage.getPointerPosition()if (pos) {// 磁贴const { pos: transformerPos } = this.render.attractTool.attract({x: pos.x,y: pos.y,width: 1,height: 1})// 移动拐点circle.setAbsolutePosition(transformerPos)// 正在拖动效果const tempPoints = [...linkPoints]tempPoints.splice(circle.attrs.linkManualIndex + 1, 0, [this.render.toStageValue(transformerPos.x - stageState.x),this.render.toStageValue(transformerPos.y - stageState.y)])manualingLine.points(_.flatten(tempPoints))}}})circle.on('mouseup', () => {const pos = circle.getAbsolutePosition()if (Math.abs(pos.x - circle.attrs.dragStartX) > this.option.size ||Math.abs(pos.y - circle.attrs.dragStartY) > this.option.size) {// 操作移动距离达到阈值// stage 状态const stageState = this.render.getStageState()// 记录(插入)拐点manualPoints.splice(circle.attrs.linkManualIndex, 0, {x: this.render.toStageValue(pos.x - stageState.x),y: this.render.toStageValue(pos.y - stageState.y)})fromGroup.setAttr('manualPoints', manualPoints)}// 操作结束circle.setAttrs({dragStart: false})// state 操作结束this.state.linkManualing = false// 销毁circle.destroy()manualingLine.destroy()// 更新历史this.render.updateHistory()// 重绘this.render.redraw()})this.group.add(circle)}// 拐点(已拐)for (let i = 1; i < linkPoints.length - 1; i++) {const circle = new Konva.Circle({id: nanoid(),pairId: pair.id,x: linkPoints[i][0],y: linkPoints[i][1],radius: this.render.toStageValue(this.render.bgSize / 2),stroke: 'rgba(0,100,0,0.1)',strokeWidth: this.render.toStageValue(1),name: 'link-manual-point',// opacity: 0,linkManualIndex: i // 当前拐点位置})// hover 效果circle.on('mouseenter', () => {circle.stroke('rgba(0,100,0,1)')document.body.style.cursor = 'pointer'})circle.on('mouseleave', () => {if (!circle.attrs.dragStart) {circle.stroke('rgba(0,100,0,0.1)')document.body.style.cursor = 'default'}})// 拐点操作circle.on('mousedown', () => {const pos = circle.getAbsolutePosition()// 记录操作开始状态circle.setAttrs({dragStartX: pos.x,dragStartY: pos.y,dragStart: true})// 标记状态 - 正在操作拐点this.state.linkManualing = true})this.render.stage.on('mousemove', () => {if (circle.attrs.dragStart) {// 正在操作const pos = this.render.stage.getPointerPosition()if (pos) {// 磁贴const { pos: transformerPos } = this.render.attractTool.attract({x: pos.x,y: pos.y,width: 1,height: 1})// 移动拐点circle.setAbsolutePosition(transformerPos)// 正在拖动效果const tempPoints = [...linkPoints]tempPoints[circle.attrs.linkManualIndex] = [this.render.toStageValue(transformerPos.x - stageState.x),this.render.toStageValue(transformerPos.y - stageState.y)]manualingLine.points(_.flatten(tempPoints))}}})circle.on('mouseup', () => {const pos = circle.getAbsolutePosition()if (Math.abs(pos.x - circle.attrs.dragStartX) > this.option.size ||Math.abs(pos.y - circle.attrs.dragStartY) > this.option.size) {// 操作移动距离达到阈值// stage 状态const stageState = this.render.getStageState()// 记录(更新)拐点manualPoints[circle.attrs.linkManualIndex - 1] = {x: this.render.toStageValue(pos.x - stageState.x),y: this.render.toStageValue(pos.y - stageState.y)}fromGroup.setAttr('manualPoints', manualPoints)}// 操作结束circle.setAttrs({dragStart: false})// state 操作结束this.state.linkManualing = false// 销毁circle.destroy()manualingLine.destroy()// 更新历史this.render.updateHistory()// 重绘this.render.redraw()})this.group.add(circle)}}} else if (pair.linkType === Types.LinkType.straight) {// 略,直线} else {// 略,原算法画连接线逻辑}}}
}

最后,关于 linkManualing 状态,会用在 2 个地方,避免和其它交互产生冲突:

// src/Render/handlers/DragHandlers.ts// 略export class DragHandlers implements Types.Handler {// 略  handlers = {stage: {mousedown: (e: Konva.KonvaEventObject<GlobalEventHandlersEventMap['mousedown']>) => {// 拐点操作中,防止异常拖动if (!(this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state.linkManualing) {// 略}},// 略}}
}
// src/Render/tools/LinkTool.ts// 略
export class LinkTool {// 略pointsVisible(visible: boolean, group?: Konva.Group) {// 略// 拐点操作中,此处不重绘if (!(this.render.draws[Draws.LinkDraw.name] as Draws.LinkDraw).state.linkManualing) {// 重绘this.render.redraw()}}// 略
}

Done!

More Stars please!勾勾手指~

源码

gitee源码

示例地址

相关文章:

前端使用 Konva 实现可视化设计器(19)- 连接线 - 直线、折线

本章响应小伙伴的反馈&#xff0c;除了算法自动画连接线&#xff08;仍需优化完善&#xff09;&#xff0c;实现了可以手动绘制直线、折线连接线功能。 请大家动动小手&#xff0c;给我一个免费的 Star 吧~ 大家如果发现了 Bug&#xff0c;欢迎来提 Issue 哟~ github源码 gitee…...

C#:通用方法总结—第15集

大家好&#xff0c;今天继续分享我们的通用方法系列。 下面是今天的通用方法&#xff1a; &#xff08;1&#xff09;这个通用方法为用文件流写数据 /// <summary> /// 用文件流写数据 /// </summary> /// <param name"data"></param> //…...

LoadRunner12 添加事务并添加检查点

1、先要添加事务开始函数lr_start_transaction("登陆事务");&#xff0c;在接口上方右击点击-插入-开始事务。输入事务名称&#xff1b; 2、在某个接口想法 右击点击-插入-结束事务&#xff0c;输入事务名称&#xff0c;与开始事务名称要保持一致&#xff0c;lr_end_…...

python中的文件

绝对路径和相对路径 一般情况下绝对路径就是从根目录开始描述的路径 相对路径就是相对于当前目录 . 没错,就是一个点,表示的是当前文件夹;.. 两个点表示的是上一层文件夹 os模块与os.path os 和 os.path 是两个非常重要的标准库模块,它们分别用于操作系统相关的功能操…...

Powerdesigner连接mysql数据库,逆向工程生成ER图 (保姆级教程:下载->连接->配置)看这一篇就够了

一、下载powerdesigner 下载的教程请看如下链接&#xff0c;我太懒了&#xff0c;直接借鉴&#xff01; 把别大佬的博客搬过来了嘿嘿~我真聪明&#xff01;ㄟ( ▔, ▔ )ㄏ 操作到完成汉化就好&#xff01;&#xff01;第5步不看了&#xff0c;别按那个走&#xff0c;因为新手…...

商家转账到零钱分销返佣申请方案及驳回处理办法

分销返佣场景是商家申请最多的场景&#xff0c;因而申请被驳回也是最多的&#xff0c;根据我们上万次成功开通商家转账到零钱的经验&#xff0c;当商家转账到零钱的分销返佣场景被驳回时&#xff0c;按照以下步骤&#xff0c;商家都可以快速过审&#xff1a; 一、分析驳回原因 …...

荟萃科技:国外问卷调查有没有实时更新的题库?

有的&#xff0c;口子查和渠道查都是。 口子查的题目都是国外的公司发放在网络上&#xff0c;都是实时发布&#xff0c;所以我们需要去国外的各大社交平台做题。 这些题目不是集中的&#xff0c;而是散布在网站里面&#xff0c;需要我们去找&#xff0c;都是老外上班实时发放…...

【课程总结】Day18:Seq2Seq的深入了解

前言 在上一章【课程总结】Day17&#xff08;下&#xff09;&#xff1a;初始Seq2Seq模型中&#xff0c;我们初步了解了Seq2Seq模型的基本情况及代码运行效果&#xff0c;本章内容将深入了解Seq2Seq模型的代码&#xff0c;梳理代码的框架图、各部分组成部分以及运行流程。 框…...

C++利用开发人员命令提示工具查看对象模型

1.跳转文件路径 cd 具体路径 2.输入c1 /d1 reportSingleClassLayout类名 文件名 操作示例如下图&#xff1a;...

白骑士的PyCharm教学高级篇 3.4 服务器部署与配置

系列目录 上一篇&#xff1a;白骑士的PyCharm教学高级篇 3.3 Web开发支持 在开发完成后&#xff0c;将代码部署到服务器上是一个关键步骤。PyCharm不仅提供了强大的本地开发支持&#xff0c;还为远程服务器配置与部署、自动化部署流程提供了便捷的工具和功能。本文将详细介绍如…...

数据库管理-第226期 内存至超线程(20240805)

数据库管理226期 2024-08-05 数据库管理-第226期 内存至超线程&#xff08;20240805&#xff09;1 CPU内缓存结构2 缓存与内存3 单核单线程4 超线程5 超线程的利弊总结 数据库管理-第226期 内存至超线程&#xff08;20240805&#xff09; 作者&#xff1a;胖头鱼的鱼缸&#xf…...

Django学习-数据迁移与数据导入导出

文章目录 一、数据迁移二、数据导入导出1. 数据导出2. 数据导入 一、数据迁移 数据迁移是将项目里定义的模型生成相应的数据表。主要的迁移指令如下&#xff1a; # 第一次生成自定义模型与django admin自带模型迁移文件&#xff0c;后续只生成新增模型迁移文件。后面加App名…...

【Nuxt】编程式导航和动态路由

编程式导航 navigateTo&#xff1a; 更多用法&#xff1a;navigateTo <template><div class"app-container"><button click"goToCategory">Category</button><NuxtPage/></div> </template> <script setup&…...

14. 计算机网络HTTPS协议(二)

1. 前言 上一章节中我们主要就 HTTPS 协议的前置知识进行介绍,下面会继续介绍 HTTPS 的通信过程以及抛出一些常见问题的探讨。因为候选人准备面试的时间和精力是比较有限的,我们在学习的过程要抓住重点,如果感觉对于细节缺乏了解,可以通过维基百科和查阅 StackOverflow 等…...

【算法设计题】实现以字符串形式输入的简单表达式求值,第2题(C/C++)

目录 第2题 实现以字符串形式输入的简单表达式求值 得分点&#xff08;必背&#xff09; 题解 1. 初始化和变量定义 2. 获取第一个数字并存入队列 3. 遍历表达式字符串&#xff0c;处理运算符和数字 4. 初始化 count 并处理加减法运算 代码详解 &#x1f308; 嗨&#xf…...

Kylin系列-入门

Kylin系列-入门 Apache Kylin是一个开源的分布式分析引擎&#xff0c;提供Hadoop/Spark之上的SQL查询接口及多维分析&#xff08;OLAP&#xff09;能力&#xff0c;以支持超大规模数据。以下是对Kylin系列的入门介绍&#xff1a; 一、基本概念 1. 定义 Apache Kylin是由eBa…...

力扣-46.全排列

刷力扣热题–第二十六天:46.全排列 新手第二十六天 奋战敲代码&#xff0c;持之以恒&#xff0c;见证成长 1.题目简介 2.题目解答 这道题目想了会,思路比较好想,但一直没调试成功,所以就参考了力扣官网的代码,积累一下回溯算法的实现和基本实现思路,即先试探后回溯,结果在下面…...

博物馆展厅AI交互数字人,解锁创新的文化交互体验

在智能化时代&#xff0c;博物馆展厅融入AI交互数字人&#xff0c;可以为游客给予实时交互的旅游服务&#xff0c;AI交互数字人可以承担智能引导、讲解、接待、客服与导游等多重角色&#xff0c;为游客塑造崭新的旅游体验。 AI交互数字人相比传统的录屏解说相比&#xff0c;AI…...

DS18B20数字温度传感器操作解析

文章目录 引言特点工作原理引脚说明配置寄存器温度寄存器时序初始化时序写时序读时序 引言 DS18B20 是一种广泛使用的数字温度传感器&#xff0c;具有高精度和易用性。是Dallas Semiconductor公司&#xff08;现为Maxim Integrated公司&#xff09;生产的单总线数字温度传感器…...

你的财富正在被一个叫做通货膨胀的怪兽给吞噬掉,你却浑然不觉。

据统计&#xff0c;2024年全球总体通货膨胀率预计达到5.8%&#xff0c;这意味着&#xff1a;你的财富正在被一个叫做通货膨胀的怪兽给吞噬掉&#xff0c;你却浑然不觉。 数据来源&#xff1a;国际货币基金组织 如何跑赢通货膨胀&#xff1f; 家庭财富的积累速度&#xff0c;要…...

#Uniapp篇:chrome调试unapp适配

chrome调试设备----使用Android模拟机开发调试移动端页面 Chrome://inspect/#devices MuMu模拟器Edge浏览器&#xff1a;Android原生APP嵌入的H5页面元素定位 chrome://inspect/#devices uniapp单位适配 根路径下 postcss.config.js 需要装这些插件 “postcss”: “^8.5.…...

Fabric V2.5 通用溯源系统——增加图片上传与下载功能

fabric-trace项目在发布一年后,部署量已突破1000次,为支持更多场景,现新增支持图片信息上链,本文对图片上传、下载功能代码进行梳理,包含智能合约、后端、前端部分。 一、智能合约修改 为了增加图片信息上链溯源,需要对底层数据结构进行修改,在此对智能合约中的农产品数…...

视频行为标注工具BehaviLabel(源码+使用介绍+Windows.Exe版本)

前言&#xff1a; 最近在做行为检测相关的模型&#xff0c;用的是时空图卷积网络&#xff08;STGCN&#xff09;&#xff0c;但原有kinetic-400数据集数据质量较低&#xff0c;需要进行细粒度的标注&#xff0c;同时粗略搜了下已有开源工具基本都集中于图像分割这块&#xff0c…...

七、数据库的完整性

七、数据库的完整性 主要内容 7.1 数据库的完整性概述 7.2 实体完整性 7.3 参照完整性 7.4 用户定义的完整性 7.5 触发器 7.6 SQL Server中数据库完整性的实现 7.7 小结 7.1 数据库的完整性概述 数据库完整性的含义 正确性 指数据的合法性 有效性 指数据是否属于所定…...

20个超级好用的 CSS 动画库

分享 20 个最佳 CSS 动画库。 它们中的大多数将生成纯 CSS 代码&#xff0c;而不需要任何外部库。 1.Animate.css 一个开箱即用型的跨浏览器动画库&#xff0c;可供你在项目中使用。 2.Magic Animations CSS3 一组简单的动画&#xff0c;可以包含在你的网页或应用项目中。 3.An…...

从 GreenPlum 到镜舟数据库:杭银消费金融湖仓一体转型实践

作者&#xff1a;吴岐诗&#xff0c;杭银消费金融大数据应用开发工程师 本文整理自杭银消费金融大数据应用开发工程师在StarRocks Summit Asia 2024的分享 引言&#xff1a;融合数据湖与数仓的创新之路 在数字金融时代&#xff0c;数据已成为金融机构的核心竞争力。杭银消费金…...

API网关Kong的鉴权与限流:高并发场景下的核心实践

&#x1f525;「炎码工坊」技术弹药已装填&#xff01; 点击关注 → 解锁工业级干货【工具实测|项目避坑|源码燃烧指南】 引言 在微服务架构中&#xff0c;API网关承担着流量调度、安全防护和协议转换的核心职责。作为云原生时代的代表性网关&#xff0c;Kong凭借其插件化架构…...

VisualXML全新升级 | 新增数据库编辑功能

VisualXML是一个功能强大的网络总线设计工具&#xff0c;专注于简化汽车电子系统中复杂的网络数据设计操作。它支持多种主流总线网络格式的数据编辑&#xff08;如DBC、LDF、ARXML、HEX等&#xff09;&#xff0c;并能够基于Excel表格的方式生成和转换多种数据库文件。由此&…...

AxureRP-Pro-Beta-Setup_114413.exe (6.0.0.2887)

Name&#xff1a;3ddown Serial&#xff1a;FiCGEezgdGoYILo8U/2MFyCWj0jZoJc/sziRRj2/ENvtEq7w1RH97k5MWctqVHA 注册用户名&#xff1a;Axure 序列号&#xff1a;8t3Yk/zu4cX601/seX6wBZgYRVj/lkC2PICCdO4sFKCCLx8mcCnccoylVb40lP...

JUC并发编程(二)Monitor/自旋/轻量级/锁膨胀/wait/notify/锁消除

目录 一 基础 1 概念 2 卖票问题 3 转账问题 二 锁机制与优化策略 0 Monitor 1 轻量级锁 2 锁膨胀 3 自旋 4 偏向锁 5 锁消除 6 wait /notify 7 sleep与wait的对比 8 join原理 一 基础 1 概念 临界区 一段代码块内如果存在对共享资源的多线程读写操作&#xf…...