整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

碎片时间学编程「10」:JavaScript 数据结

碎片时间学编程「10」:JavaScript 数据结构 - 树


树是一种数据结构,由一组表示分层树结构的链接节点组成。每个节点都通过父子关系链接到其他节点。树中的第一个节点是根,而没有任何子节点的节点是叶子。

JavaScript 树可视化

树数据结构中的每个节点都必须具有以下属性:

key: 节点的键

value: 节点的值

parent: 节点的父节点(null如果没有)

children: 指向节点子节点的指针数组

树形数据结构的主要操作有:

insert: 插入一个节点作为给定父节点的子节点

remove: 从树中删除一个节点及其子节点

find: 检索给定节点

preOrderTraversal: 通过递归遍历每个节点及其子节点来遍历树

postOrderTraversal: 通过递归遍历每个节点的子节点来遍历树

class TreeNode {

constructor(key, value=key, parent=null) {

this.key=key;

this.value=value;

this.parent=parent;

this.children=[];

}


get isLeaf() {

return this.children.length===0;

}


get hasChildren() {

return !this.isLeaf;

}

}


class Tree {

constructor(key, value=key) {

this.root=new TreeNode(key, value);

}


*preOrderTraversal(node=this.root) {

yield node;

if (node.children.length) {

for (let child of node.children) {

yield* this.preOrderTraversal(child);

}

}

}


*postOrderTraversal(node=this.root) {

if (node.children.length) {

for (let child of node.children) {

yield* this.postOrderTraversal(child);

}

}

yield node;

}


insert(parentNodeKey, key, value=key) {

for (let node of this.preOrderTraversal()) {

if (node.key===parentNodeKey) {

node.children.push(new TreeNode(key, value, node));

return true;

}

}

return false;

}


remove(key) {

for (let node of this.preOrderTraversal()) {

const filtered=node.children.filter(c=> c.key !==key);

if (filtered.length !==node.children.length) {

node.children=filtered;

return true;

}

}

return false;

}


find(key) {

for (let node of this.preOrderTraversal()) {

if (node.key===key) return node;

}

return undefined;

}

}

用class创建一个类TreeNode,使用构造函数初始化 key, value, parent 和 children。

定义一个isLeaf getter方法,用Array.prototype.length检查children是否为空。

定义一个hasChildren getter方法,用来检查是否有子节点。

用 class 创建一个Tree,使用构造函数初始化树的根节点。

定义一个preOrderTraversal() 方法,按顺序遍历树的生成器方法,使用 yield* 语法将遍历委托给自身。

定义一个postOrderTraversal()方法,以后序遍历树的生成器方法,使用yield*语法将遍历委托给自身。

定义一个insert()方法,使用preOrderTraversal()和Array.prototype.push()方法向树中添加一个TreeNode 节点。

定义一个remove()方法,使用preOrderTraversal()和Array.prototype.filter()方法从树删除一个TreeNode节点。

定义一个find()方法,使用preOrderTraversal()方法检索树中的给定节点。

const tree=new Tree(1, 'AB');

tree.insert(1, 11, 'AC');

tree.insert(1, 12, 'BC');

tree.insert(12, 121, 'BG');

[...tree.preOrderTraversal()].map(x=> x.value);

// ['AB', 'AC', 'BC', 'BCG']


tree.root.value; // 'AB'

tree.root.hasChildren; // true


tree.find(12).isLeaf; // false

tree.find(121).isLeaf; // true

tree.find(121).parent.value; // 'BC'


tree.remove(12);


[...tree.postOrderTraversal()].map(x=> x.value);

// ['AC', 'AB']

  1. eb浏览器创建Document对象,并且开始解析Web页面,解析HTML元素和它们的文本内容后添加Element对象和Text节点到文档中.在这个阶段document.readystate属性的值是”loading”.

  2. 当HTML解析器遇到没有async和defer属性的<script>元素时,它把这些元素添加到文档中,然后执行行内或外部脚本.这些脚本会同步执行,并且在脚本下载(如果需要)和执行时解析器会暂停.这样脚本就可以用document.write()来把文本插入到输入流中.解析器恢复时这些文本会成为文档的一部分.同步脚本经常简单定义函数和注册后面使用的注册事件处理程序,但它们可以遍历和操作文档树,因为在它们执行时已经存在了.这样,同步脚本可以看到它自己的<script>元素和它们之前的文档内容.

  3. 当解析器遇到设置了async属性的<script>元素时,它开始下载脚本文本,并继续解析文档.脚本会在它下载完成后尽快执行,但是解析器没有停下来等它下载.异步脚本禁止使用document.write()方法.它们可以看到自己的<script>元素和它之前的所有文档元素,并且可能或干脆不可能访问其他的文档内容.

  4. 当文档完成解析,document.readyState属性变成“interactive”.

  5. 所有有defer属性的脚本,会按它们在文档的里的出现顺序执行.异步脚本可能也会在这个时间执行.延迟脚本能访问完整的文档树,禁止使用document.write()方法.

  6. 浏览器在Document对象上触发DOMContentLoaded事件.这标志着程序执行从同步脚本执行阶段转换到了异步事件驱动阶段.但要注意,这时可能还有异步脚本没有执行完成.

  7. 这时,文档已经完全解析完成,但是浏览器可能还在等待其他内容载入,如图片.当所有这些内容完成载入时,并且所有异步脚本完成载入和执行,document.readyState属性改变为“complete”,Web浏览器触发Window对象上的load事件.

  8. 从此刻起,会调用异步事件,以异步响应用户输入事件、网络事件、计时器过期等.

David Flanagan. JavaScript权威指南

JavaScript时间线

原文:https://os-note.com/articles/client-side-javascript-timeline.html

JavaScript 画一棵树?

产品说要让前端用 JavaScript 画一棵树出来,但是这难道不能直接让 UI 给一张图片吗?

后来一问才知道,产品要的是一颗 随机树,也就是树的茂盛程度、长度、枝干粗细都是随机的,那这确实没办法叫 UI 给图,毕竟 UI 不可能给我 10000 张树的图片吧?

所以第一时间想到的就是 Canvas,用它来画这棵随机树(文末有完整代码)

Canvas 画一颗随机树

接下来使用 Canvas 去画这棵随机树

基础页面

我们需要在页面上写一个 canvas 标签,并设置好宽高,同时需要获取它的 Dom 节点、绘制上下文,以便后续的绘制

坐标调整

默认的 Canvas 坐标系是这样的

但是我们现在需要从中间去向上去画一棵树,所以坐标得调整成这样:

  • X 轴从最上面移动到最下面
  • Y 轴的方向由往下调整成往上,并且从最左边移动到画布中间

这些操作可以使用 Canvas 的方法

  • ctx.translate: 坐标系移动
  • ctx.scale: 坐标系缩放

绘制一棵树的要素

绘制一棵树的要素是什么呢?其实就是树枝果实,但是其实树枝才是第一要素,那么树枝又有哪些要素呢?无非就这几个点

  • 起始点
  • 树枝长度、树枝粗细
  • 生长角度
  • 终点

开始绘制

所以我们可以写一个 drawBranch 来进行绘制,并且初始调用肯定是绘制树干,树干的参数如下:

  • 起始点:(0, 0)
  • 树枝长度、树枝粗细:这些可以自己自定义
  • 生长角度:90度
  • 终点:需要算

这个终点应该怎么算呢?其实很简单,根据树枝长度、生长角度就可以算出来了,这是初高中的知识

于是我们可以使用 Canvas 的绘制方法,去绘制线段,其实树枝就是一个一个的线段

到现在我绘制出了一个树干 出来

但是我们是想让这棵树开枝散叶,所以需要继续递归继续去绘制更多的树枝出来~

递归绘制

其实往哪开枝散叶呢?无非就是往左或者往右

所以需要递归画左边和右边的树枝,并且子树枝肯定要比父树枝更短、比父树枝更细,比如我们可以定义一个比例

  • 子树枝是父树枝长度的 0.8
  • 子树枝是父树枝粗细的 0.75

而子树枝的生长角度,其实可以随机,我们可以在 0° - 30° 之间随机选一个角度,于是增加了递归调用的代码

但是这个时候会发现,报错了,爆栈了,因为我们只递归开始,但却没有在某个时刻递归停止

我们可以自己定义一个停止规则(规则可以自己定义,这会决定你这棵树的茂盛程度):

  • 粗细小于 2 时马上停止
  • (粗细小于 10 时 + 随机数)决定是否停止

现在可以看到我们已经大致绘制出一棵树了

不过还少了树的果实

绘制果实

绘制果实很简单,只需要在绘制树枝结束的时候,去把果实绘制出来就行,其实果实就是一个个的白色实心圆

至此这棵树完整绘制完毕

绘制部分的代码如下

完整代码