22
头图

In most cases, the data obtained from the server for tree display is flat in itself, that is, a list. This is because the relational database saves data in units of "rows", so it saves the data of each node, and this data contains the connection between it and the parent node (such as parentId ).

To display such list data in a tree form, the front end needs to convert the list data into tree structure data. This tree structure means: each node data contains its child node set (usually children attribute). Therefore, the data structure of the tree nodules mainly needs to contain the following information (TypeScript data structure declaration):

interface TreeNodeBase<TKey extends string | number = number> {
    id: TKey;
    parentId: TKey
    children?: TreeNodeBase<TKey>[]
}

The generic syntax of TypeScript is used here to describe the tree node structure. It is not difficult to understand from the natural semantics:

  • id (including parentId ) of the tree node string or number . In rare cases, it may also be a mixed type.
  • The tree node contains a parentId , because this parentId is not optional (the ? number is not declared), so the root node usually uses a special value, or an ID value that should not exist, such as 0 (if it is a database auto-increment field, generally Will start from 1 )
  • The tree node contains an optional set of child nodes children , each element of which has the same type as the current element
  • Defined TKey this role in order to constrain the type parameter is the type of child node and parent must be consistent, avoid the parent node id is string type of child nodes id they became engaged string this case (mixed type id situation without including )

After popularizing the data structure of the tree node, let's talk about the conversion process. Generally speaking, the conversion may take place in three stages:

  • Process the backend before sending it out
  • After the front end gets it, convert it by itself, and then use the converted array to render the page
  • The UI components used in the front-end have their own conversion function, so developers don’t need to worry about it (such as zTree )

Here, I will take JS/TS as an example to talk about how to perform the conversion. Language is not important, what is important is how to think and what method to use for conversion. The reason why both JS and TS are used here is that TS with types can clearly describe the data structure, while JS code may seem less stressful for most people.

1. Prepare sample data (randomly generated)

In the tree data represented by a list, each (row) of the tree data must clearly describe the three elements of this node:

  1. Self-identification (ID), usually id , key , name and other attribute names, which can uniquely identify a node
  2. The relationship with the parent node, by using parentId , upstreamId etc., clearly indicate its parent node
  3. The data carried by the node itself, such as the displayed text, title , label etc., and some other data.

In order to quickly prepare sample data, we use the simplest data structure with very clear attribute meanings. Note that this structure is a flat structure that matches the data table, and does not contain the children subset.

// TypeScript
interface TreeNode {
    id: number;
    parentId: number;
    label: string;
}

Then write a piece of code to randomly generate data. Before that, we agreed that the id valid node starts from 1 . If a node's parentId === 0 , it means that the node has no parent node. Ideas:

  • Generate a set of nodes in a loop, the id each node is serial number + 1 (the serial number starts 0

    // JavaScript
    const nodes = [];
    count nodesCount = 20;
    for (let i = 0; i < nodesCount; i++) {
        nodes.push({
            id: i + 1,
        })
    }
  • Next, parentId is a node that has been generated before, and its ID range is in the interval [0, i] (closed interval, if you don’t understand, please review high school mathematics). We randomly select one from this range as its parent node. Here we need to generate a random integer , so first write a randomInt(max)

    // JavaScript
    function randomInt(max) {
        return Math.floor(Math.random());
    }
注意 `Math.random()` 的取值范围是 `[0, 1)`(左闭右开区间)内的浮点数,所以 `randomInt()` 的结果集在 `[0, max)` 范围内。为了保证需要的 `[0, i]` 内的整数,即 `[0, i + 1)` 间的整数,需要调用时给参数为 `i + 1`:`randomInt(i + 1)` 。

继续完善上面 `node.push( ... )` 中的 `parentId` 部分:

```typescript
{
    id: i + 1,
    parentId: randomInt(i + 1)
}
```
  • The next step is to generate random label . [5, 15) too complicated rules, a random string composed of uppercase and lowercase letters and numbers with a length in the range of 060efac630fa4f is generated. Since the string itself can be iterated (traversed) , it can be converted into an array using the Spread operator to get the character set. The code is as follows:

    // JavaScript
    const CHARS = ((chars, nums) => {
        return [...`${chars}${chars.toLowerCase()}${nums}`];
    })("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789");
其实直接给一个包含所有字符的字符串就可以,但是不想把 `a~z` 再写一遍,所以用了一个 IIFE 来复用 `A~Z`。

另外有一点需要注意:字符串本身可以使用 `[]` 索引运算符来取字符,所以不预先转换成字符数组也是可以的。

接下来是随机产生字符串。根据长度随机选择 n 个字符,再连接起来即可:

```js
// JavaScript
function randomString(length) {
    return Array.from(
        Array(length),
        () => CHARS[randomInt(CHARS.length)]
    ).join("");
}
```

`randomString()` 可以产生一个指定长度的随机字符串。这里 `Array(length)` 会产生一个长度为 `length` 但不含任何元素的数组,可以用 `Array.from()` 把它变成含有元素(默认是 `undefined`)的数组。在转换的过程中,`Array.from()` 的第二个参数是一个映射函数,跟 `Array.prototype.map()` 的参数一样。

现在,我们可以继续完善 `node.push(...)` 中的 `label` 部分:

```js
{
    id: i + 1,
    parentId: randomInt(i + 1),
    label: randomString(5 + randomInt(10))    // 长度在 [5, 15) 区间的随机字符串
}
```

Up to now, the key code for preparing sample data is available. Here is a complete

// TypeScript

interface TreeNode {
    id: number;
    parentId: number;
    label: string;
}

const CHARS = ((chars, nums) => {
    return [...`${chars}${chars.toLowerCase()}${nums}`];
})("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "0123456789");

function randomInt(max: number): number {
    return Math.floor(Math.random() * max);
}

function randomString(length: number = 10): string {
    return Array.from(
        Array(length),
        () => CHARS[randomInt(CHARS.length)]
    ).join("");
}

function randomTreeNodes(count: number = 20): TreeNode[] {
    return [...Array(count).keys()]
        .map(i => ({
            id: i + 1,
            parentId: randomInt(i + 1),     // 从已经产生的节点中去随机找一个
            label: randomString(5 + randomInt(10))  // 随机产生长度为 [5, 15) 的字符串
        }));
}
The complete code is written by TypeScript. If you need JavaScript, you can spell it out according to the key code of each step above. Or take the TypeScript code to TypeScript Playground to convert it.

Finally, we can directly call randomTreeNodes() to generate the tree structure we need:

// JavaScript | TypeScript
const treeNodes = randomTreeNodes();

The obtained treeNodes will be used as the data source of the spanning tree demo code below. Here is the result of one of the runs:

[
  { id: 1, parentId: 0, label: '8WUg35y' },
  { id: 2, parentId: 1, label: 'Pms1S5Mx' },
  { id: 3, parentId: 1, label: 'RUTKSF' },
  { id: 4, parentId: 1, label: 'IYkxXlhmU12x' },
  { id: 5, parentId: 4, label: 'p2Luabg9mK2' },
  { id: 6, parentId: 0, label: 'P6mtcgfCD' },
  { id: 7, parentId: 1, label: 'yluJgpnqKthR' },
  { id: 8, parentId: 6, label: 'm6o5UsytQ0' },
  { id: 9, parentId: 2, label: 'glcR5yGx' },
  { id: 10, parentId: 0, label: 'lhDGTNeeSxLNJ' },
  { id: 11, parentId: 1, label: 'r7ClxBCQS6' },
  { id: 12, parentId: 7, label: '5W6vy0EuvOjN' },
  { id: 13, parentId: 5, label: 'LbpWq' },
  { id: 14, parentId: 6, label: 'ysYwG8EFLAu1a' },
  { id: 15, parentId: 8, label: 'R2PmAh1' },
  { id: 16, parentId: 10, label: 'RKuQs4ki65wo' },
  { id: 17, parentId: 10, label: 'YN88ixWO1PY7f4' },
  { id: 18, parentId: 13, label: '03X6e4UT' },
  { id: 19, parentId: 7, label: 'LTJTeF' },
  { id: 20, parentId: 19, label: '3rqUqE3MLShh' }
]

If it is represented graphically, it is:

flowchart LR
%%{ init: { "theme": "forest" } }%%

S(("Virtual\nRoot")) --> N1
S --> N6
S --> N10

N1("1 | 8WUg35y") --> N2("2 | Pms1S5Mx")
N1 --> N3("3 | RUTKSF")
N1 --> N4("4 | IYkxXlhmU12x")
N4 --> N5("5 | p2Luabg9mK2")
N6("6 | P6mtcgfCD")
N1 --> N7("7 | yluJgpnqKthR")
N6 --> N8("8 | m6o5UsytQ0")
N2 --> N9("9 | glcR5yGx")
N10("10 | lhDGTNeeSxLNJ")
N1 --> N11("11 | r7ClxBCQS6")
N7 --> N12("12 | 5W6vy0EuvOjN")
N5 --> N13("13 | LbpWq")
N6 --> N14("14 | ysYwG8EFLAu1a")
N8 --> N15("15 | R2PmAh1")
N10 --> N16("16 | RKuQs4ki65wo")
N10 --> N17("17 | YN88ixWO1PY7f4")
N13 --> N18("18 | 03X6e4UT")
N7 --> N19("19 | LTJTeF")
N19 --> N20("20 | 3rqUqE3MLShh")
Mermaid is a good thing, do you want to support it?

Second, generate a tree from the demo data

Before the idea is fully formed, pick up the keyboard and start typing code-this behavior is generally regarded as "experiment." But even if it is an experiment, you should try your thoughts first.

It is known, each node has a key data comprising: means for identifying a node id , for identifying a parent relationship parentId . Then, only need to find the parent node parentId children[] array of the parent node to generate tree structure data. There are several related issues to consider here:

  1. Since a node has only one parentId , it will only be added to the children[] of a certain node, and it cannot appear in the children[] of multiple nodes;
  2. For parentId or parentId as 0 , we consider it as the root node. But it may not be the only root node, so we need an additional roots[] array to save all root nodes.
  3. Thinking: How to find the corresponding node data parentId

The first two questions are easy to understand, and the third question requires thinking about algorithms. Since there is a parent node in the node list, parentId

// JavaScript
const parentNode = treeNodes.find(node => node.id === parentId);

In the process of traversing the processing node, according to the logic of the data generated above, it can be concluded that the parent node of the current node must be before it. If you know parent node and child node is close to , it can be optimized to reverse search. This process can be defined as a function findParent() :

// JavaScript
/**
 * @param id 要查找的父节点 id
 * @param index 当前节点在 treeNodes 中的序号
 */
const findParent = (id, index) => {
    for (let i = index - 1; i >= 0; i--) {
        if (treeNodes[i].id === id) {
            return treeNodes[i];
        }
    }    
};
In fact, in most cases, it is not clear whether the parent node is close to the starting position or the ion node, so there is no need to write a reverse search, just Array.prototype.find()

After finding the parent node, before adding the current node node to the parentNode child node set, pay special attention to whether its child node set exists. You can use the Logical nullish assignment ( ??= ) operator to simplify the code and get it done in one sentence:

(parentNode.children ??= []).push(node);

There is also a performance-related issue here. In the case of a large amount of data, a very large number of nodes may be scanned regardless of the order or reverse order, and the use of Map can greatly improve the search efficiency. When the nodes are in order (that is, the parent node must be in front), the Map can be generated while traversing. Relatively complete code example:

// JavaScript

function makeTree(treeNodes) {
    const nodesMap = new Map();
    const roots = [];
    treeNodes.forEach((node, i) => {
        nodesMap.set(node.id, node);

        if (!node.parentId) {
            roots.push(node);
            return;
        }

        const parent = nodesMap.get(node.parentId);
        (parent.children ??= []).push(node);
    });

    return roots;
}

If the above JavaScript code is changed to TypeScript code, there will still be a problem when the type declaration is added: near the end of the parent.children monogram red parent and report "Object is possibly'undefined'.".

This problem shows that parentId , there is a possibility that it cannot be found.

In this example, since treeNodes can be guaranteed to be found, we can ignore the concerns of the compiler and directly change it to parent!.children hide this risk prompt. However, the real data from the background does not guarantee that nodesMap.get(node.parentId) will not return undefined . There are at least two situations that cause this problem:

  1. The order of the nodes is not in the order of father first and then son (mostly occurs after the node has been moved). In this case, since the parent node is after the child node, it must be searched from it before being added to the Map. Of course, it cannot be found. To solve this problem, just submit to traverse all nodes to generate a complete Map.
  2. Due to back-end errors or business needs, all nodes could not be sent back. In addition to reporting errors, the front end also has two fault-tolerant processing methods:

    1. Discard the node whose parent node is not found. This fault-tolerant method is not difficult to deal with, so I won't say more.
    2. Treat the node with no parent node as the root node-since the data is here, will use this fault-tolerant method in most cases

The next step is to add makeTree fault-tolerant processing.

Third, the complete TypeScript code to generate the tree from the list

interface TreeNode {
    id: number;
    parentId: number;
    label: string;
    children?: TreeNode[]
}

function makeTree(treeNodes: TreeNode[]): TreeNode[] {
    // 提前生成节点查找表。
    // 如果明确节点是顺序可以保证先父后子,可以省去这次遍历,在后面边遍历过程中填充查找表
    const nodesMap = new Map<number, TreeNode>(
        treeNodes.map(node => [node.id, node])
    );

    // 引入虚拟根节点来统一实现 parent 始终有效,避免空判断
    const virtualRoot = { } as Partial<TreeNode>;
    treeNodes.forEach((node, i) => {
        const parent = nodesMap.get(node.parentId) ?? virtualRoot;
        (parent.children ??= []).push(node);
    });

    return virtualRoot.children ?? [];
}

Yes, this code is not long. but,

  • Has the progressive analysis and processing process been Get?
  • Did TypeScript's type checking impress you?

Come to my class: TypeScript from entry to practice [2021 version] programming 160efac631003e, you can

  • Deeply understand TypeScript language features and write high-quality code
  • Master the use of TypeScript-based Vue front-end and Koa back-end technologies
  • Master the development mode, design mode and release method of separation of front and back ends
  • Integrate type system into programming thinking to improve understanding and design skills

TypeScript 从入门到实践


边城
59.8k 声望29.6k 粉丝

一路从后端走来,终于走在了前端!