记录--虚拟滚动探索与封装

news2024/11/15 15:24:50

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

1. 介绍

什么是虚拟滚动虚拟滚动就是通过js控制大列表中的dom创建与销毁,只创建可视区域dom非可视区域的dom不创建。这样在渲染大列表中的数据时,只创建少数的dom,提高性能。

2. 分类

在虚拟滚动技术中,虚拟滚动可以分为定高虚拟滚动非定高虚拟滚动。定高指的是每一个列表元素都是高度固定的,非定高指的是每一个列表元素的高度是动态变化的。定高虚拟滚动的实现比较容易,而且性能高;非定高虚拟滚动的失效稍微复杂,而且性能比定高虚拟滚动要差一些。无论是定高虚拟滚动还是非定高虚拟滚动,都是虚拟滚动技术的分类,对于大数据渲染都有很大的性能提升

下面我们逐步分析两种虚拟滚动技术的实现,并且封装成常用的组件。选用技术栈是vue,改成react或angular也是十分方便的。

3. 定高虚拟滚动

3.1 封装思路

虚拟滚动的结构如下图:

image.png 虚拟滚动由图三部分组成,渲染容器container渲染数据list撑开滚动条的容器clientHeightRef。渲染容器就是我们需要渲染的区域,渲染数据就是可视区域的数据,撑开滚动条的容器代表所有数据渲染出来后的高度。

由于我们只渲染可视区域的数据,那么渲染容器的滚动条高度是不正确的,需要撑开滚动条的容器来撑开实际的高度

html结构如下:

<div class="virtual-list">
      <!-- 这里是用于撑开高度,出现滚动条用 -->
      <div class="list-view-phantom" ref="clientHeightRef" :style="{ height: list.length*itemHeight + 'px' }"></div>
      <ul v-if="list.length > 0" class="option-warp" ref="contentRef">
        <li
          :style="{ height: itemHeight + 'px' }"
          class="option"
          v-for="(item, index) in virtualRenderData"
          :key="index"
        >
          {{item}}
        </li>
      </ul>
  </div>

每一条数据的高度是this.itemHeight=10,渲染容器的高度是this.containerHeight=300,那么一屏需要渲染的数据是count=Math.ceil(this.containerHeight / this.itemHeight)

假设我们我们的需要的数据list如下:

const list = [
    {id:1,name:1},
    {id:2,name:3},
    ....
]

那么撑开滚动条的容器的高度是this.list.length*this.itemHeight

我们给渲染容器加一个监听滚动的事件,主要是获取当前滚动的scrollTop,用来更新渲染可视区域的数据。如下,我们封装一个更新渲染可视区域的数据函数:

const update = function(scrollTop = 0){
    this.$nextTick(() => {
        // 获取当前可展示数量
        const count = Math.ceil(this.containerHeight / this.itemHeight)
        const start = Math.floor(scrollTop / this.itemHeight)
        // 取得可见区域的结束数据索引
        const end = start + count
        // 计算出可见区域对应的数据,让 Vue.js 更新
        this.virtualRenderData = this.list.slice(start, end)
    })
}

当滚动条滚动的时候,我们需要从list中截取当前渲染容器刚刚好可以渲染的数据,达到像真的滚动的了一样。上面的滚动函数虽然已经更新了渲染可视区域的数据,但是当我们滚动的时候会发现内容块被滚动到了上面,再次滚动的时候直接就不见了。这是由于滚动条是由撑开滚动条的容器撑开的,渲染的内容高度只有容器的高度,所以它只会在顶部出现,滚动的时候自然就不会动,效果如下:

image.png 所以当我们滚动滚动的时候,还需要将渲染内容往对应的方向偏移。比如偏移的y方向距离就是scrollTop的距离,

this.$refs.contentRef.style.webkitTransform = `translate3d(0, ${scrollTop * this.itemHeight}px, 0)`

这样就能达到滚动的时候,渲染内容始终保持在渲染容器的顶部,好像真的随着滚动条滚动而滚动。

但是实际上这样的效果是不好的,因为我们更新的颗粒度是按每一条数据来分的,而不是按scrollTop来进行的,所以渲染内容的偏移量也需要按照每一条数据的颗粒度来进行更新,代码如下:

const update = function(scrollTop = 0){
    this.$nextTick(() => {
        // 获取当前可展示数量
        const count = Math.ceil(this.containerHeight / this.itemHeight)
        const start = Math.floor(scrollTop / this.itemHeight)
        // 取得可见区域的结束数据索引
        const end = start + count
        // 计算出可见区域对应的数据,让 Vue.js 更新
        this.virtualRenderData = this.list.slice(start, end)
        + this.$refs.contentRef.style.webkitTransform = `translate3d(0, ${start * this.itemHeight}px, 0)`
    })
}

上面的代码基本可以满足基本的使用,但是当我们滚动比较快的时候,渲染区域底部会出现瞬间留白,是因为dom没有及时的渲染,原因是我们只渲染刚刚好一屏的数据。

为了减少留白的出现,我们应该预渲染几条数据bufferCount,增加渲染缓存区间

const update = function(scrollTop = 0){
    this.$nextTick(() => {
        // 获取当前可展示数量
        const count = Math.ceil(this.containerHeight / this.itemHeight)
        const start = Math.floor(scrollTop / this.itemHeight)
        // 取得可见区域的结束数据索引
        + const end = start + count + bufferCount
        // 计算出可见区域对应的数据,让 Vue.js 更新
        this.virtualRenderData = this.list.slice(start, end)
        this.$refs.contentRef.style.webkitTransform = `translate3d(0, ${start * this.itemHeight}px, 0)`
    })
}

3.2 完整代码和演示地址

定高虚拟滚动演示地址:atdow.github.io/learning-co…

定高虚拟滚动代码地址:github.com/atdow/learn…

4. 非定高虚拟滚动

4.1 封装思路

看了上面的定高虚拟滚动,我们对虚拟滚动技术已经有了基本的了解。对于非定高虚拟滚动,需要解决的最大问题就是每一条需要渲染的数据的高度是不确定,这样我们就很难确定一屏需要渲染多少条数据。

为了确定一屏需要渲染多少条数据,我们需要假设每条需要渲染数据的高度为一个假设值estimatedItemHeight=40,定义一个用于存储每一条渲染数据高度的数组itemHeightCache=[],定义一个用于存储每一条渲染数据距离顶部距离的数组itemTopCache=[](用于提升性能用,后面会做解释),以及定义撑开滚动条滚动容器高度的变量scrollBarHeight

假设我们我们的需要的数据list如下:

const list = [
    {id:1,name:1},
    {id:2,name:3},
    ....
]

我们先初始化itemHeightCache、itemTopCache和scrollBarHeight

const estimatedTotalHeight = this.list.reduce((pre, current, index) => {
        // 给每一项一个虚拟高度
        this.itemHeightCache[index] = { isEstimated: true, height: this.estimatedItemHeight }
        // 给每一项距顶部的虚拟高度
        this.itemTopCache[index] = index === 0 ? 0 : this.itemTopCache[index - 1] + this.estimatedItemHeight
        return pre + this.estimatedItemHeight
      }, 0)
// 列表总高
this.scrollBarHeight = estimatedTotalHeight

有了上面的初始化数据,我们就可以进行第一次假设渲染了:

// 更新数据函数
const update = function() {
      const startIndex = this.getStartIndex()
      // 如果是奇数开始,就取其前一位偶数
      if (startIndex % 2 !== 0) {
        this.startIndex = startIndex - 1
      } else {
        this.startIndex = startIndex
      }
      this.endIndex = this.getEndIndex()
      this.visibleList = this.list.slice(this.startIndex, this.endIndex)
      // 移动渲染区域
      if (this.$refs.contentRef) {
        this.$refs.contentRef.style.webkitTransform = `translate3d(0, ${this.itemTopCache[this.startIndex]}px, 0)`
      }
}

// 获取开始索引
cont getStartIndex = function() {
      const scrollTop = this.scrollTop
      // 每一项距顶部的距离
      const arr = this.itemTopCache
      let index = -1
      let left = 0,
        right = arr.length - 1,
        mid = Math.floor((left + right) / 2)
      // 判断 有可循环项时进入
      while (right - left > 1) {
        /*
        二分法:拿每一次获得到的 距顶部距离 scrollTop 同 获得到的模拟每个列表据顶部的距离作比较。
        arr[mid] 为虚拟列高度的中间项
        不断while 循环,利用二分之一将数组分割,减小搜索范围
        直到最终定位到 目标index 值
      */
        // 目标数在左侧
        if (scrollTop < arr[mid]) {
          right = mid
          mid = Math.floor((left + right) / 2)
        } else if (scrollTop > arr[mid]) {
          // 目标数在右侧
          left = mid
          mid = Math.floor((left + right) / 2)
        } else {
          index = mid
          return index
        }
      }
      index = left
      return index
}

// 获取结束索引
const getEndIndex = function() {
      const clientHeight = this.$refs.scrollbarRef?.clientHeight //渲染容器高度
      let itemHeightTotal = 0
      let endIndex = 0
      for (let i = this.startIndex; i < this.dataList.length; i++) {
        if (itemHeightTotal < clientHeight) {
          itemHeightTotal += this.itemHeightCache[i].height
          endIndex = i
        } else {
          break
        }
      }
      endIndex = endIndex
      return endIndex
}

update函数是用来更新需要渲染的数据的,核心逻辑就是获取截取数据的开始索引getStartIndex和结束索引getEndIndex以及移动被渲染数据容器

当滚动条滚动的时候,我们就将scrollTop存起来,这个时候从itemTopCache中获取距离scrollTop最近的索引,就是我们需要截取数据的开始索引。因为itemTopCache存储的就是每一条数据距离顶部的距离,所以直接取就行了,这也是为什么我们要先存储itemTopCache。因为滚动的时候,我们都要从itemTopCache中使用二分法查找,不然就得从itemHeightCache中从头到尾一个一个遍历去对比查找,在数据量大的时候容易造成卡顿。

getEndIndex核心就是从itemHeightCache(存储每一条渲染数据高度的数组)中一条一条拿数据,从startIndex开始拿,一直拿到刚好填满渲染容器高度即可,就可以得到我们的截取数据的最后索引 (实际上这样是不够完美的,后面继续讲解)。

移动被渲染数据容器的技巧和上面定高虚拟滚动类似,这里不做太多解释。

在初始化完itemHeightCache、itemTopCache和scrollBarHeight后,我们就可以手动调一次update函数进行第一次渲染了(this.update()),使用的都是预设的假定值。

在说更新之前,我们需要先定义一下子组件,也就是每一条被渲染数据的容器。这样当数据被更新渲染之后(需要通知暴露indexheight参数),就可以得到真实的dom的高度,通知我们去更新itemHeightCache、itemTopCache和scrollBarHeight,更新逻辑如下:

const updateItemHeight = function({ index, height }) {
      // 每次创建的时候都会抛出事件,因为没有处理异步的情况,所以必须每次高度变化都需要更新
      // dom元素加载后得到实际高度 重新赋值回去
      this.itemHeightCache[index] = { isEstimated: false, height: height }
      // 重新确定列表的实际总高度
      this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => {
        return pre + current.height
      }, 0)
      // 更新itemTopCache
      const newItemTopCache = [0]
      for (let i = 1, l = this.itemHeightCache.length; i < l; i++) {
        // 虚拟每项距顶部高度 + 实际每项高度
        newItemTopCache[i] = this.itemTopCache[i - 1] + this.itemHeightCache[i - 1].height
      }
      // 获得每一项距顶部的实际高度
      this.itemTopCache = newItemTopCache
}

dom更新完之后,初始化预定值计算出来需要的渲染数据就真的被渲染了,我们这个时候就可以再次调用update函数再次更新数据,自动更新弥补到渲染真实一屏需要渲染的数据了。

const updateItemHeight = function({ index, height }) {
      // 每次创建的时候都会抛出事件,因为没有处理异步的情况,所以必须每次高度变化都需要更新
      // dom元素加载后得到实际高度 重新赋值回去
      this.itemHeightCache[index] = { isEstimated: false, height: height }
      // 重新确定列表的实际总高度
      this.scrollBarHeight = this.itemHeightCache.reduce((pre, current) => {
        return pre + current.height
      }, 0)
      // 更新itemTopCache
      const newItemTopCache = [0]
      for (let i = 1, l = this.itemHeightCache.length; i < l; i++) {
        // 虚拟每项距顶部高度 + 实际每项高度
        newItemTopCache[i] = this.itemTopCache[i - 1] + this.itemHeightCache[i - 1].height
      }
      // 获得每一项距顶部的实际高度
      this.itemTopCache = newItemTopCache
      + this.update() // 自动更新
}

当滚动的时候,存储scrollTop,手动调用update函数,将会自动更新,整个过程如下:

image.png

html结构如下:

 <div class="virtual-list-dynamic-height" ref="scrollbarRef" @scroll="onScroll">
    <div class="list-view-phantom" :style="{ height: scrollBarHeight + 'px' }"></div>
    <!-- 列表总高 -->
    <ul ref="contentRef">
      <Item
        v-for="item in visibleList"
        :data="item.data"
        :index="item.index"
        :key="item.index"
        @update-height="updateItemHeight"
      >
        {{item}}
      </Item>
    </ul>
</div>

跟定高虚拟滚动不同点就是,需要定义子组件,同时传递给子组件index索引。visibleList需要定义为[{index:xxx,data:xxx}]的数据格式,将index给储存起来,这样在子组件更新的时候才能获取到index

4.2 调优

在上面的代码中,基本可以实现基础的非定高虚拟滚动了,但是还是无法应对复杂的情况。

我们举一个极端的例子:当一条数据的真实高度是200,其他数据的真实高度高度是10,渲染容器的高度是300。在第一次假设渲染并且更新后我们的itemHeightCache、itemTopCache和scrollBarHeight后,我们将会得到这样的结果。渲染容器中渲染的是数据是第一条数据和剩下的9条数据,刚刚好渲染一屏数据,这样是没有任何问题的。

当滚动条滚动的时候,我们滚动了20px的距离,获取到的startIndex应该是0,因为距离顶部最近的数据是第一条数据,这个就会造成下部空白20px的区域。当滚动了80px的时候,获取到的startIndex也是0,原理同上,下部造成了空白区域将会是恐怖的80px

image.png

为了解决空白局域,靠缓冲渲染bufferCount是不够的,就算bufferCount给了4,多四条数据也无法填充满空白区域。调大bufferCount容易造成性能问题,也不能确定bufferCount到底给多少才能合适。所以需要调整getEndIndex的逻辑,不再是从startIndex获取到刚好填充满渲染区域,而是从startIndex获取到刚好填充满渲染区域+statIndex的高度。这样无论startIndex的高度是多少,我们都能填充满整个渲染容器,因为空白区域最大高度就是startIndex的高度。同时我们在endIndex上加上bufferCount,就可以达到完美的效果。

// 获取结束索引
const getEndIndex = function() {
      + const whiteHeight = this.scrollTop - this.itemTopCache[this.startIndex] // 出现留白的高度
      const clientHeight = this.$refs.scrollbarRef?.clientHeight //渲染容器高度
      let itemHeightTotal = 0
      let endIndex = 0
      for (let i = this.startIndex; i < this.dataList.length; i++) {
        + if (itemHeightTotal < clientHeight+whiteHeight) {
          itemHeightTotal += this.itemHeightCache[i].height
          endIndex = i
        } else {
          break
        }
      }
      + endIndex = endIndex + bufferCount
      return endIndex
}

3.3 完整代码和演示地址

非定高虚拟滚动演示地址:atdow.github.io/learning-co…

非定高虚拟滚动代码地址:github.com/atdow/learn…

4 总结

有了非定高虚拟滚动组件,不就是可以应对各种情况了,为什么还需要做定高虚拟滚动组件?

在上面的封装思路中,我们能清晰知道非定高虚拟滚动组件是用假定值进行渲染的,在真实渲染过后才会弥补更新,而定高虚拟滚动所有东西都是确定的。所以定高虚拟滚动的优势就是比非定高虚拟滚动性能高,缺点就是只能应对每一条渲染数据是固定的情况。

定高虚拟滚动:

  • 优点:性能比非定高虚拟滚动高
  • 缺点:只能应用于每一条渲染数据高度是固定的场景

非定高虚拟滚动:

  • 优点:性能比定高虚拟滚动低
  • 缺点:能应用于每一条渲染数据高度是动态的场景

本文转载于:

https://juejin.cn/post/7204450037031092283

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/381965.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

快速生成QR码的方法:教你变成QR Code Master

目录 简介: 具体实现步骤&#xff1a; 一、可以使用Python中的qrcode和tkinter模块来生成QR码。以下是一个简单的例子&#xff0c;演示如何在Tkinter窗口中获取用户输入并使用qrcode生成QR码。 1&#xff09;首先需要安装qrcode模块&#xff0c;可以使用以下命令在终端或命令…

aws batch 理解和使用batch进行批处理计算

文档 Compute Resource Memory ManagementRunning Workload on AWS Batch aws batch 是云上的批处理平台&#xff0c;通过托管环境减少了管理成本。包括配置大量计算资源&#xff0c;更具任务负载优化资源分配。 基本概念 job&#xff08;任务&#xff09;&#xff0c;提交到…

C语言-结构体【详解】

一、 结构体的基础知识 结构是一些值的集合&#xff0c;这些值称为成员变量结构的每个成员可以是不同类型的变量 &#xff08;1&#xff09;结构体的声明 写法一&#xff1a; 注&#xff1a; 括号后边的分号不能忘结构体末尾可以不创建变量&#xff0c;在主函数中再创建 struc…

【ChatGPT】sqlachmey 多表连表查询语句

感受下科技带来的魅力&#xff0c;这篇文章是通过ChatGPT自动生成的&#xff0c;不得不说技术强大!!! 在SQLAlchemy中进行多表连接查询可以使用join()方法或join()函数&#xff0c;具体用法如下&#xff1a; join()方法 join()方法可以在SQLAlchemy ORM中的查询中使用。假设…

根据指定函数对DataFrame中各元素进行计算

【小白从小学Python、C、Java】【计算机等级考试500强双证书】【Python-数据分析】根据指定函数对DataFrame中各元素进行计算以下错误的一项是?import numpy as npimport pandas as pdmyDict{A:[1,2],B:[3,4]}myDfpd.DataFrame(myDict)print(【显示】myDf)print(myDf)print(【…

SMILES标准化方法以及其中的一个坑(手性)

rdkit.Chem.MolToSmiles()方法是用于将RDKit分子对象转换为SMILES字符串的方法。它的参数如下&#xff1a; mol&#xff1a;必需&#xff0c;要转换为SMILES字符串的RDKit分子对象。isomericSmiles&#xff1a;bool类型&#xff0c;是否生成同分异构体SMILES&#xff0c;默认为…

培训班出身的同学简历怎么做?面试要注意哪些?来自资深大厂HR的忠告

目录 1 不少培训班候选人的简历中&#xff0c;缺乏足够的商业项目年限 2 直接描述培训班学习经历会带来的负面影响 3 大龄转行Vs年轻的初级程序员&#xff0c;公司一般会如何选择&#xff1f; 4 经过培训班突击后&#xff0c;可以先面试小公司 5 面试官怎么面试有培训班经历…

安卓开发到底是做什么的

前言 在某平台看到了这样一个问题&#xff1a; 要知道&#xff0c;安卓开发是当前软件行业中的一个热门方向&#xff0c;它涉及到使用 Java 或 Kotlin 语言开发应用程序&#xff0c;运行在安卓操作系统上的手机、平板电脑、电视等设备上。在过去的几年中&#xff0c;随着智能…

追溯ChatGPT

ChatGPT 国内趋势 在国际学术界看来&#xff0c;ChatGPT / GPT-3.5 是一种划时代的产物 它与之前常见的语言模型 (Bert/ Bart/ T5) 的区别&#xff0c;几乎是导弹与弓箭的区别&#xff0c;一定要引起最高程度的重视 国际上的主流学术机构 (如斯坦福大学&#xff0c;伯克利加…

Ep_计网面试题-UDP实现TCP?

其实把TCP优点拿过来就行 直接上答案: 1、添加seq/ack机制&#xff0c;确保数据发送到对端 2、添加发送和接收缓冲区 3、添加超时重传机制 视频讲解: https://edu.csdn.net/course/detail/38090 点我进入 面试宝典 很多人不知道面试问什么,或者其他的XXGuide,那里边的太多没…

示波器上位机软件下载安装教程

软件&#xff1a;示波器软件NS-Scope 语言&#xff1a;简体中文 环境&#xff1a;NI-VISA安装环境&#xff1a;Win10以上版本&#xff08;特殊需求请后台私信联系客服&#xff09; 硬件要求&#xff1a;CPU2GHz 内存4G(或更高&#xff09;硬盘500G(或更高&#xff09; 驱动…

第十二 代码块、设计模式(懒汉、饿汉)

代码块概述 ●代码块是类的5大成分之一(成员变量、构造器&#xff0c;方法&#xff0c;代码块&#xff0c;内部类)&#xff0c;定义在类中方法外。 ●在ava类下&#xff0c;使用{}括起来的代码被称为代码块。 代码块分为 静态代码块: 格式:static{ 特点:需要通过static关键字修…

企业个人,没有品牌不好混

企业、个人没品牌没法持续混下去 “品牌资产”需要持续积累 频繁切换&#xff0c;品牌无法立 趣讲大白话&#xff1a;没点品牌没法混 【安志强趣讲信息科技91期】 ******************************* 企业品牌资产&#xff1a;指能给企业带来效益的消费者品牌认知&#xff0c;包括…

『C/C++养成计划』C++中的双冒号::名解析(Scope Resolution Operator)

C中的双冒号::名解析(Scope Resolution Operator)&#xff01; 文章目录1. 访问命名空间中的成员2. 访问类中的静态成员3. 嵌套类访问4. 在类之外定义函数5. 当存在具有相同名称的局部变量时&#xff0c;要访问全局变量6. C模板参数的自动推导参考文献C中的双冒号名解析&#…

外贸人如何写出优秀的开发信?附详细思路

如何写出优秀的开发信&#xff1f;最近做出口生意的客户都在抱怨&#xff0c;开发信的回复率越来越低&#xff0c;其实原因有很多&#xff0c;有时候并非自己的能力实在很欠缺。原因总结如图&#xff1a;第一&#xff1a;市场不景气这个就是就属于客观因素了&#xff0c;这也许…

InstructGPT论文笔记

论文链接&#xff1a;https://arxiv.org/pdf/2203.02155.pdf 1 摘要 做的事&#xff1a; 1、标注了数据&#xff0c;问题和答案写出来&#xff0c;然后训练模型 2、收集数据集&#xff0c;排序模型的输出&#xff0c;使用强化学习训练这个排序的过程 效果层面来说&#xff1…

思迅软件端口不通导致软件和软锁报错的问题

一、端口不通导致软件和软锁报错的问题 问题说明&#xff1a;打开软件提示到&#xff1a;xxx.xxx.xxx.xxx失败&#xff01; 处理步骤1&#xff1a; 假设软锁服务器IP为192.168.0.1&#xff0c;分别在服务器本机和客户端电脑测试软锁服务: 在服务器的浏览器中访问地址: http:/…

ChatGPT(GPT3.5) OpenAI官方API正式发布

OpenAI社区今天凌晨4点多发送的邮件&#xff0c;介绍了ChatGPT官方API的发布。官方介绍文档地址为“OpenAI API”和“OpenAI API”。 ChatGPT(GPT3.5)官方API模型名称为“gpt-3.5-turbo”和“gpt-3.5-turbo-0301”。API调用价格比GPT text-davinci-003模型便宜10倍。调用费用为…

补档:红黑树代码实现+简略讲解

红黑树讲解和实现1 红黑树介绍1.1 红黑树特性1.2 红黑树的插入1.3 红黑树的删除2 完整代码实现2.1 rtbtree.h头文件2.2 main.c源文件1 红黑树介绍 红黑树( Red-Black tree&#xff0c;简称RB树)是一种自平衡二叉查找树&#xff0c;是计算机科学中常见的一种数据结构&#xff0c…

python画直方图,刻画数据分布

先展示效果 准备一维数据 n 个数据元素计算最大值&#xff0c;最小值、均值、标准差、以及直方图分组 import numpy as np data list() for i in range(640):data.append(np.random.normal(1)) print(data)z np.histogram(data, bins64) print(list(z[0])) ### 对应 x 轴数据…