
问题
- 当数据过多时,渲染出现卡顿。因为 dom 数量过大。
实现方案
- 将树形数据和 dom 扁平化
- 虚拟长列表控制 dom 渲染数量
1.扁平化

扁平化tree的DOM结构图-1
由上图可以看出经过扁平化处理后的父子节点dom是同级的,接下来我们来实现将tree数据扁平化处理。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| const treeData = [ { id: 1, title: "课程1", children: [ { id: 4, title: "课程1-1" }, { id: 5, title: "课程1-2", children: [ { id: 6, title: "课程1-2-1" }, { id: 7, title: "课程1-2-2" }, ], }, ], }, { id: 2, title: "课程2" }, { id: 3, title: "课程3" }, ]; const flatData = [ { id: 1, title: "课程1" }, { id: 4, title: "课程1-1" }, { id: 5, title: "课程1-2" }, { id: 6, title: "课程1-2-1" }, { id: 7, title: "课程1-2-2" }, { id: 2, title: "课程2" }, { id: 3, title: "课程3" }, ]
|
将 treeData 的数据格式转变成 faltData,将数据扁平化的过程
迭代和递归实现扁平化
我们先来讲讲递归和迭代的异同。
- 递归使用的是选择结构,能使程序的结构更清晰、更简洁、更容易让人理解,从而减少读懂代码的时间。但是大量的递归调用会建立函数的副本,会耗费大量的时间和内存。
- 迭代使用的是循环结构,不需要反复调用函数和占用额外的内存。因此我们应该视不同 情况选择不同的代码实现方式。
在算法中我们会遇到很多递归实现的案例,所有的递归都可以转换成非递归实现,其中转换的本质是:递归是解析器(引擎)来帮我们做了栈的存取,非递归是手动创建栈来模拟栈的存取过程。
下面我们通过栈来实现
1 2 3 4 5 6 7 8 9 10 11 12
| function faltten(tree) { let flatData = [] let stack = [...tree] while (stack.length) { let node = stack.shift() if (node.children) { stack.unshift(...node.children) } flatData.push({ id: node.id, title: node.title }) } return flatData }
|
下面我们通过递归来实现
1 2 3 4 5 6 7 8 9 10 11 12 13
| function faltten(tree) { let flatData = [] for (var i = 0; i < data.length; i++) { formatData.push({ id: data[i].id, title: data[i].title, }) if (data[i].children) { formatData = formatData.concat(faltten(data[i].children)) } } return formatData }
|
为了实现扁平化的树型组件功能,我们的数据字段需要 expand
,visible
,children
,level
字段。大致结构:
{
key:
title:
level: 通过 level 标识节点层级,通过 css 实现层级的区别
visible: 节点的显示隐藏属性
expand: 展开状态
children: 存储子节点的引用,通过引用控制子节点的显示隐藏
}
通过上面的分析,我们来继续实现新增字段之后的扁平化处理
迭代实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| flatten () { let flatData = [] let stack = [...tree] let parentIndex = {} while (stack.length) { let node = stack.shift() if (!node.level) { node.level = 0 node.visible = true } if (node.children) { parentIndex[node.level] = flatData.length const children = node.children.map(item => { return {...item, level: node.level + 1, visible: node.expand}} }) stack.unshift(...children) } flatData.push({...node, children: []}) if (node.level !== 0) { flatData[parentIndex[node.level - 1]].children.push(flatData[flatData.length - 1]) } } return flatData }
|
递归实现方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| flatten (data, arr = [], parent = null, level = 0, visible = true, children = [], insert = null) { arr.push({...data, level, parent, visible, children}) if (insert !== null) { arr[insert].children.push(arr[arr.length - 1]) } if (data.children) { insert = arr.length - 1 data.children.forEach((item) => { this.flatten(item, arr, arr[arr.length - 1], level + 1, data.expand, [], insert) }) } return arr }
|
难点在于存储子节点的 data 引用,存储子节点的引用是因为子节点对象和 children 对象指向的是同一个内存地址,所以后续隐藏节点的时候,我们可以直接通过获取当前点击节点的 children 字段,进行 visible 属性的隐藏设置。
扁平化数据的树型渲染
1.树型节点的遍历
1 2 3 4 5 6 7 8
| <div :key="i" class="tree-item" v-show="item.visible" @click="handleExpand(item)" v-for="(item, i) in unHiddenList.slice(startIndex, endIndex)" :style="`transform:translateX(${item.level * 20}px)`" >
|
节点的层级区别通过level属性来控制
unHiddenList代表visible属性为false (即未展开) 的数据列表
2.节点的展开和隐藏实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| handleExpand (node) { const status = !node.expand node.expand = status if (status && node.children.length > 0) { node.children.forEach((item) => { item.visible = true }) } else if (!status) { this.handleClose(node.children) } }, handleClose (node) { node.forEach((item) => { item.visible = false if (item.children.length > 0) { item.expand = false this.handleClose(item.children) } }) }
|
虚拟滚动
dom大致结构
1 2 3 4 5 6
| <div class="wrap" ref="wrap" @scroll="handleScroll"> <div class="invent-space" :style="`height:${contentHeight}`"></div> <div class="container" ref="list"> </div> </div>
|
contentHeight代表整个滚动的内容高度
*contentHeight = unHiddenList.length * itemHeight(节点高度)*
监听滚动事件
1 2 3 4 5 6 7 8 9
| updateVisibleData (scrollTop = 0) { requestAnimationFrame(() => { if (scrollTop < (this.unHiddenList.length - 20) * this.itemHeight) { this.$refs.list.style.transform = 'translate3d(0,' + scrollTop + 'px,0)' this.startIndex = Math.floor(scrollTop / this.itemHeight) this.endIndex = this.startIndex + 20 } }) }
|
其中的20代表的是可视item,25代表的是item的高度,数字可以根据需求自己设置。
其中使用transform
来实现虚拟滚动的原因是由于 transform
是位于 Composite Layers
层,而 width
、 left
、 margin
等则是位于 Layout
层,在 Layout
层发生的改变必定导致 Paint Setup and Paint
-> Composite Layers
,所以相对而言使用 transform
实现的动画效果肯定比 left
这些更加流畅。