示例项目:https://download.csdn.net/download/K86338236/86860248
效果:999条数据,drawcall稳定在15

使用:ScrollViewCtrl挂载到滑动列表上,将滑动的预制体拖入节点itemPrefab属性上

     //传入的数据,需要是数组形式let arr = [{ name: "这是" + 1, id: 1 },{ name: "这是" + 2, id: 2 },{ name: "这是" + 3, id: 3 }]this.scrollView.getComponent(ScrollViewCtrl).init(arr);

预制体根节点上不能挂载组件
预制体上的脚本:


const { ccclass, property } = cc._decorator;@ccclass
export default class ItemCtrl extends cc.Component {@property(cc.Label)desc: cc.Label = null;start() {}/*** 传入数据* @param data 数据* @param index 顺序* @param extData 额外数据*/initData(data, index, extData) {this.desc.string = data.name + " " + data.id;}
}

代码预览:
ScrollViewCtrl:

import NodePool from "./NodePool";const { ccclass, property, menu } = cc._decorator;@ccclass
@menu('自定义组件/ScrollViewCtrl')
export default class ScrollViewCtrl extends cc.Component {@property(cc.Prefab)itemPrefab: cc.Prefab = null;bindIndexList: {};scrollView: cc.ScrollView = null;content: cc.Node = null;view: cc.Node = null;layout: cc.Layout = null;itemName: string = null;mat4: cc.Mat4 = null;isInit: boolean = null;data: any[] = null;callbackList: any[] = null;extData: any = null;firstX: number = null;firstY: number = null;itemCache: any[] = null;itemBuffer: any = null;_tmpV2: any = null;viewRect: any = null;start() {this.initOnce();}initOnce() {this.bindIndexList = {};    /**记录哪个index需要绑定Item */this.scrollView = this.node.getComponent(cc.ScrollView);this.content = this.scrollView.contentthis.view = this.content.parent;this.layout = this.content.getComponent(cc.Layout);this.itemName = this.itemPrefab.name;this.mat4 = cc.mat4();this.initOnce = function () { }}onDestroy() {this.content.off(cc.Node.EventType.POSITION_CHANGED, this.scrollEvent, this);this.recycle();}/**注册滚动事件 */registerScrollEvent(handler, target) {if (!handler.name) {return;}var scrollView = this.node.getComponent(cc.ScrollView);var eventHandler = new cc.Component.EventHandler();eventHandler.target = target.node;eventHandler.component = cc.js.getClassName(target);eventHandler.handler = handler.name;var index = scrollView.scrollEvents.length;scrollView.scrollEvents[index] = eventHandler;}/*** 初始化数据和表现* @param {Array} data 所有的列表数据组成的数组* @param {any} param 要传递到item的额外参数*/init(data, param?) {this.initOnce();if (!Array.isArray(data)) {console.error("传进来的数据不为数组!");return;}if (!data.length) {this.recycle();this.content.off(cc.Node.EventType.POSITION_CHANGED, this.scrollEvent, this);return;}param = param || {};this.isInit = true;this.data = data;this.callbackList = [];this.extData = param.extData;if (param.onChanged) {this.onItemChanged(param.onChanged);}this.layout.enabled = false;this.scrollView.stopAutoScroll();if (!NodePool.Instance.hasPool(this.itemName)) {NodePool.Instance.initPool(this.itemPrefab);}/**模板 */var template = this.itemPrefab.data;var paddingLeft = this.layout.paddingLeft;      /**左边距 */var paddingRight = this.layout.paddingRight;    /**右边距 */var paddingTop = this.layout.paddingTop;        /**上边距 */var paddingBottom = this.layout.paddingBottom;  /**下边距 */var spacingX = this.layout.spacingX;            /**水平间隔距离 */var spacingY = this.layout.spacingY;            /**垂直间隔距离 */let firstX = template.x;let firstY = template.y;if (this.scrollView.horizontal) {firstX = -template.width / 2firstX -= paddingLeft;}if (this.scrollView.vertical) {firstY = -template.height / 2;firstY -= paddingTop;}this.firstX = firstX;this.firstY = firstY;this.itemCache = [];this.itemBuffer = this.itemBuffer || [];//初始化itemBufferlet i = 0this.itemBuffer.forEach(buffer => {buffer.index = -1;if (i >= data.length) {buffer.item.x = -9999999;buffer.item.y = -9999999;buffer.item.opacity = 0;}i++;});var initCache = (i) => {this.itemCache[i] = this.itemCache[i] || {};this.itemCache[i].x = firstX;this.itemCache[i].y = firstY;this.itemCache[i].width = template.width;this.itemCache[i].height = template.height;this.itemCache[i].scaleX = template.scaleX;this.itemCache[i].scaleY = template.scaleY;this.itemCache[i].visible = false;}initCache(0);for (let i = 1; i < this.data.length; i++) {initCache(i);if (this.scrollView.horizontal) {this.itemCache[i].x = this.itemCache[i - 1].x - (this.itemCache[i - 1].width / 2 + this.itemCache[i].width / 2 + spacingX);}if (this.scrollView.vertical) {this.itemCache[i].y = this.itemCache[i - 1].y - (this.itemCache[i - 1].height / 2 + this.itemCache[i].height / 2 + spacingY);}}let lastItem = this.itemCache[this.itemCache.length - 1];if (this.scrollView.horizontal) {this.content.width = Math.abs(lastItem.x + lastItem.width / 2 + paddingRight);}if (this.scrollView.vertical) {this.content.height = Math.abs(lastItem.y - lastItem.height / 2 - paddingBottom);}this.content.on(cc.Node.EventType.POSITION_CHANGED, this.scrollEvent, this);this.scheduleOnce(this.updateListView);}updateItemView(item, index) {var js = this.getScript(item);if (js && js.initData) {js.initData(this.data[index], index, this.extData);js.updateView && js.updateView();}}//创建item预制体getItem() {let item = NodePool.Instance.getNode(this.itemName);item.x = this.firstX;item.y = this.firstY;let data = {item: item,index: -1,}//将创建的item预制体放入data数据结构,加入itemBuffer,方便在更新视图时操作this.itemBuffer.push(data);item.on(cc.Node.EventType.SIZE_CHANGED, this.onItemSizeChanged.bind(this, item), this);item.on(cc.Node.EventType.SCALE_CHANGED, this.onItemSizeChanged.bind(this, item), this);return data;}scrollEvent() {if (!this.content || !this.isInit) return;this.updateListView();}/*** 在显示区域的itemx x,y坐标相对于content的位置是固定的,这部分数据存于itemCache. 监听content位置改变时,* 遍历所有itemCache,根据itemCache中每个item相对于content的位置计算出每个item相对于世界坐标的rect区域,与当前mask相对世界坐标* 的rect区域判断是否相交,若相交则该item显示,根据itemCache里面存的坐标给item设置位置,层级和父节点,不显示的item给* 移到下方*/updateListView() {if (!this.itemCache) {return;}var attach = (buffer, i) => {buffer.index = i;buffer.item.x = this.itemCache[i].x;buffer.item.y = this.itemCache[i].y;buffer.item.scaleX = this.itemCache[i].scaleX;buffer.item.scaleY = this.itemCache[i].scaleY;buffer.item.opacity = 255;if (this.scrollView.horizontal) {buffer.item.width = this.itemCache[i].width;}if (this.scrollView.vertical) {buffer.item.height = this.itemCache[i].height;}buffer.item.parent = this.content;this.updateItemView(buffer.item, i);}for (var i = 0; i < this.itemCache.length; i++) {var cache = this.itemCache[i];var visible = this.isItemInView(i);var buffer = this.itemBuffer.find((v) => {return v.index == i;});//创建当前滑动区域内可见的item预制体if (visible) {//当前下标有绑定的item,直接使用该itemif (this.bindIndexList[i]) {buffer = this.itemBuffer.find((v) => {return v.bindIndex == i;});}//创建itemif (!buffer) {//如果itemBuffer里面有初始化缓存的预制体,则使用之buffer = this.itemBuffer.find((v) => {return v.index == -1 && v.bindIndex == undefined;});//没有缓存的话,则使用getIItem创建后,并放入itemBuffer方便后面使用buffer = buffer || this.getItem();}//根据item绑定的序列号重新设置层级以及位置if (buffer.index != i) {attach(buffer, i);}} else if (buffer) { //将不可见item移到非常靠下的位置,使content大小足够大,能够向下滑动buffer.index = -1;buffer.item.x = -9999999;buffer.item.y = -9999999;buffer.item.opacity = 0;  //active会影响layout大小,所以不可见将透明度设置为0}//执行每个item可见变化的回调,可见->不可见 回调, 不可见->可见回调if (cache.visible != visible) {this.runItemChangedCallback(i, visible);}cache.visible = visible;}/**调整层级 */this.itemBuffer.sort((a, b) => {if (a.index < 0 || b.index < 0) {return 1;}return a.index - b.index;});//按照index对item进行排序for (var i = 0; i < this.itemBuffer.length; i++) {this.itemBuffer[i].item.setSiblingIndex(i);}}/**item是否在可视区域内 */isItemInView(index) {//当前滑动列表区域的坐标this._tmpV2 = this._tmpV2 || cc.v2(0, 0);//view为mask裁剪区域this.view.getWorldMatrix(this.mat4);let scale = this.mat4.m[0];let wposx = this.mat4.m[12];let wposy = this.mat4.m[13];let width1 = this.view.width * scale;let height1 = this.view.height * scale;let wpos = this.view.convertToWorldSpaceAR(cc.Vec2.ZERO, this._tmpV2);//rect比那辆没有初始化或则进行了缩放或则横坐标或纵坐标变动了,则将rect变量重赋值if (!this.viewRect || scale != 1 || (this.viewRect.x + width1 / 2) != wposx || (this.viewRect.y + height1 / 2) != wposy) {this.viewRect = new cc.Rect(wpos.x - width1 / 2, wpos.y - height1 / 2, width1, height1);}//获取当前item缓存的数据let data = this.itemCache[index];//转成世界坐标let wpos2 = this.content.convertToWorldSpaceAR(cc.v2(data.x, data.y));//获取item的宽高let width2 = data.width * data.scaleX;let height2 = data.height * data.scaleY;let rect = new cc.Rect(wpos2.x - width2 / 2, wpos2.y - height2 / 2, width2, height2);//判断两个矩形区域是否相交let ret = this.viewRect.intersects(rect);return ret;}onItemSizeChanged(item) {if (!this.itemCache) {return;}let data = this.itemBuffer.find((v) => {return v.item == item;});if (data && data.index >= 0) {var itemData = this.itemCache[data.index];if (this.scrollView.horizontal && itemData.width == item.width && itemData.scaleX == item.scaleX) {return;}if (this.scrollView.vertical && itemData.height == item.height && itemData.scaleY == item.scaleY) {return;}itemData.width = item.width;itemData.scaleX = item.scaleX;itemData.height = item.height;itemData.scaleY = item.scaleY;this.updateBuffer();this.scheduleOnce(this.updateListView);}}setItemProperty(index, property, value) {if (!this.itemCache || !this.itemBuffer) {return;}var buffer = this.itemBuffer.find(v => {return v.index == index;});if (buffer) {buffer.item[property] = value;} else {var itemData = this.itemCache[index];itemData[property] = value;}this.updateBuffer();this.scheduleOnce(this.updateListView);}/**刷新itemCache的缓存里面每个item缓存数据的y坐标,刷新itemBuffer缓存预制体的纵坐标,并更新content的高度* 每个显示的item相对于content的位置不变,只在content容器内的item大小变化时,刷新itemCache数据**/updateBuffer() {let lastItem = this.itemCache[this.itemCache.length - 1];if (this.scrollView.vertical) {this.itemCache[0].y = -this.itemCache[0].height / 2 - this.layout.paddingTop;if (this.itemCache[0].scaleY != 1) {this.itemCache[0].y = -Math.abs(this.itemCache[0].scaleY * this.itemCache[0].height) / 2 - this.layout.paddingTop;}this.itemBuffer.find((v) => {if (v.index == 0) {v.item.y = this.itemCache[0].y;}});for (var i = 1; i < this.data.length; i++) {var data1 = this.itemCache[i - 1];var data2 = this.itemCache[i];var h1 = data1.height;var h2 = data2.height;if (data1.scaleY != 1) {h1 = Math.abs(data1.scaleY * data1.height);}if (data2.scaleY != 1) {h2 = Math.abs(data2.scaleY * data2.height);}data2.y = data1.y - (h1 / 2 + h2 / 2 + this.layout.spacingY);this.itemBuffer.find((v) => {if (v.index == i) {v.item.y = data2.y;}});}var lastRealHeight = lastItem.height / 2;if (lastItem.scaleY != 1) {lastRealHeight = Math.abs(lastItem.scaleY * lastItem.height) / 2;}this.content.height = Math.abs(lastItem.y - lastRealHeight - this.layout.paddingBottom);}}/*** 滑动到指定Item位置* @param {*} index */scrollToItem(index, t?, extParams?) {if (!this.itemCache || !this.itemCache.length) {return;}if (index < 0) index = 0;if (index >= this.itemCache.length) index = this.itemCache.length - 1;var cache = this.itemCache[index];if (!cache) {return;}extParams = extParams || {};t = t || 0;if (this.scrollView) {var toY;if (extParams.customTween) {// toY = -(cache.y + Math.abs(cache.height * cache.scaleY) / 2);// this.content.runAction(cc.moveTo(t, cc.v2(0, toY)));} else {toY = -(cache.y + Math.abs(cache.height * cache.scaleY) / 2);this.scrollView.scrollToOffset(cc.v2(0, toY), t)}}}/*** 获取Item*/getItemByIndex(index) {if (!this.itemBuffer) {return;}var buffer = this.itemBuffer.find(v => {return v.index == index;}) || {};return buffer.item;}removeItemByIndex(index) {if (!this.itemBuffer) {return;}var idx = this.itemBuffer.findIndex(v => {return v.index == index;});if (idx >= 0) {var buffer = this.itemBuffer.splice(idx, 1);buffer.item.destroy();}}/*** 下标和Item绑定* item只能指定index, item将不可复用*/bindItemWithIndex(item, bindIndex, isBind) {if (!this.itemBuffer) {return;}isBind = isBind || true;var buffer = this.itemBuffer.find(v => {return v.item == item;});if (buffer) {buffer.bindIndex = bindIndex;if (isBind) {this.bindIndexList[bindIndex] = isBind;} else {delete this.bindIndexList[bindIndex];delete buffer.bindIndex;}}}onItemChanged(callback) {if (typeof (callback) == "function") {this.callbackList.push(callback);}}runItemChangedCallback(index, visible) {try {for (let i = 0; i < this.callbackList.length; i++) {this.callbackList[i](index, visible);}} catch (e) {console.error(e);}}recycle() {if (this.itemBuffer) {this.itemBuffer.forEach(element => {if (element && cc.isValid(element.item)) {element.item.off(cc.Node.EventType.SIZE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);element.item.off(cc.Node.EventType.SCALE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);NodePool.Instance.putNode(this.itemName, element.item);}});}this.itemCache = null;this.itemBuffer = null;}getScript(node: cc.Node) {if (!node) return null;//@ts-ignorelet arr = node._components;for (let i = 0; i < arr.length; i++) {const element = arr[i];if (element && arr[i].hasOwnProperty("_super")) {return arr[i];}}return null;}
}/*** 获取Item*/getItemByIndex(index) {if (!this.itemBuffer) {return;}var buffer = this.itemBuffer.find(v => {return v.index == index;}) || {};return buffer.item;}removeItemByIndex(index) {if (!this.itemBuffer) {return;}var idx = this.itemBuffer.findIndex(v => {return v.index == index;});if (idx >= 0) {var buffer = this.itemBuffer.splice(idx, 1);EngineUtil.destroyNode(buffer.item);}}/*** 下标和Item绑定* item只能指定index, item将不可复用*/bindItemWithIndex(item, bindIndex, isBind) {if (!this.itemBuffer) {return;}isBind = isBind || true;var buffer = this.itemBuffer.find(v => {return v.item == item;});if (buffer) {buffer.bindIndex = bindIndex;if (isBind) {this.bindIndexList[bindIndex] = isBind;} else {delete this.bindIndexList[bindIndex];delete buffer.bindIndex;}}}onItemChanged(callback) {if (typeof (callback) == "function") {this.callbackList.push(callback);}}runItemChangedCallback(index, visible) {try {for (let i = 0; i < this.callbackList.length; i++) {this.callbackList[i](index, visible);}} catch (e) {console.error(e);}}recycle() {if (this.itemBuffer) {this.itemBuffer.forEach(element => {if (element && cc.isValid(element.item)) {element.item.off(cc.Node.EventType.SIZE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);element.item.off(cc.Node.EventType.SCALE_CHANGED, this.onItemSizeChanged.bind(this, element.item), this);NodePool.Instance.putNode(this.itemName, element.item);}});}this.itemCache = null;this.itemBuffer = null;}getScript(node: cc.Node) {if (!node) return null;//@ts-ignorelet arr = node._components;for (let i = 0; i < arr.length; i++) {const element = arr[i];if (element && arr[i].hasOwnProperty("_super")) {return arr[i];}}return null;}
}

NodePool:


var _nodePool = {};
var _prefabList = {};export default class NodePool {protected static _instance: NodePool = null;public static get Instance(): NodePool {if (NodePool._instance == null) {NodePool._instance = new NodePool();}return NodePool._instance;}private _pathList: {};/*** 添加路径,调用loadPool的时候就不需要传完整路径了* @param {*} path 路径数组或者字符串*/addPath(path) {this._pathList = this._pathList || {};if (!Array.isArray(path)) {path = [path];}for (let i = 0; i < path.length; i++) {let name = cc.path.basename(path[i]);this._pathList[name] = this._pathList[name] || path[i];}cc.log(this._pathList);}/*** 加载预制,保存到节点池中* @param {*} name 预制名字,不需要带路径* @param {*} callback */loadPool(name, callback) {if (!Array.isArray(name)) {name = [name];}var paths = [];for (let i = 0; i < name.length; i++) {paths.push(this._pathList[name[i]]);}cc.resources.load(paths, cc.Prefab, (res: any) => {if (!Array.isArray(res)) {res = [res];}for (let i = 0; i < res.length; i++) {let poolName = res[i].name;if (!this.hasPool(poolName)) {this.initPool(poolName, res[i]);}}if (callback) callback();});}/*** 初始化节点池* @param {*} name 缓存名字,一般和预制体名字一致 * @param {*} object 预制对象* @param {*} count 缓存个数,默认为1*/initPool(object, count = 1, name = "") {if (!name) name = object.name;if (!this.hasPool(name)) {if (_nodePool[name]) {for (const n of _nodePool[name]) {this.destroyNode(n);}}_nodePool[name] = [];_prefabList[name] = object;for (var i = 0; i < count; i++) {var node = cc.instantiate(object);node.active = false;_nodePool[name].push(node);}}}/*** 是否已经缓存在节点池* @param {*} name 预制名字*/hasPool(name) {return _prefabList[name] && _prefabList[name].isValid;}/*** 回收节点* @param {*} name 预制名字 * @param {*} node 预制节点实例*/putNode(name, node) {if (!cc.isValid(node)) {console.error("putNode: node param is invalid");return;}var pool = _nodePool[name];if (!pool) {console.error("putNode: pool %s not found", name);return;}if (pool.findIndex((item) => {return item == node}) >= 0) {return;}node.stopAllActions();node.removeFromParent(true);node.x = 0;node.y = 0;node.scale = 1;node.opacity = 255;node.active = false;pool.push(node);}/*** 获取缓存的节点实例* @param {*} name 预制名字*/getNode(name) {var pool = _nodePool[name];if (!pool) {console.error("getNode: pool %s not found", name);return null;}var node = pool.length > 0 ? pool.pop() : cc.instantiate(_prefabList[name]);node = cc.isValid(node) ? node : cc.instantiate(_prefabList[name]);node.active = true;node.x = 0;node.y = 0;// ant.FontTools.updateFont(node);return node;}/**重置节点池 */reset() {if (!_nodePool) {return;}var list = _nodePool;for (var key in list) {var pool = list[key];while (pool.length > 0) {this.destroyNode(pool.pop());}}}getPool() {return _nodePool;}destroyNode(node) {if (!cc.isValid(node)) {console.error("Tools: destroyNode error, param is invalid");return;}node.removeFromParent(false);node.destroy();}
}

【cocos creator】滑动列表复用,减少drawcall(TS)相关推荐

  1. cocos creator 滚动列表 ListView

    滚动列表,这种东西在游戏中很常见.而cocos creator 中的ScrollView + Layout 只有你想不到,没有它满足不了,   各种分骚布局. 都能实现.  但是, 但是, 它还有一些 ...

  2. 基于Cocos Creator 2.3.0,使用TypeScript(ts)实现微信跳一跳

    在看我这篇文章之前,首先您应该掌握一些基本知识,一是了解和使用cocos creator的常用控件,如Button,Label,Sprite等,脚本事件绑定等;Vscode的基本操作;TypeScri ...

  3. cocos creator 优化之相机渲染 drawcall优化

    相机的渲染对于游戏的优化也很重要,有时候游戏中我们需要多个相机,ui相机,玩家相机,小地图相机等等,多一个相机的渲染就会多一些draw call 相机的设置非常重要,看一下cocos creator  ...

  4. Cocos Creator 性能优化:DrawCall

    Cocos Creator 性能优化:DrawCall(全面!) title: Cocos Creator 性能优化:DrawCall 前言 在游戏开发中,DrawCall 作为一个非常重要的性能指标 ...

  5. Cocos Creator性能优化---DrawCall

    前言 在游戏开发中,DrawCall 作为一个非常重要的性能指标,直接影响游戏的整体性能表现. 无论是 Cocos Creator.Unity.Unreal 还是其他游戏引擎,只要说到游戏性能优化,D ...

  6. Cocos Creator 性能调优:如何减少 2D/3D DrawCall?

    Cocos 中文社区第4期有奖征稿活动火热进行中,iWatch SE.坚果投影仪等丰厚奖品等你来拿,点击文末[阅读原文]进入社区专贴,把你的聪明才智向我们砸来吧! 点击查看活动详情 本文即为此次社区征 ...

  7. 从投篮小游戏入门 Cocos Creator 3D 开发

    一枚小工,多年 Cocos2d-x 和 Cocos Creator 游戏开发工程师.现阶段,主要在 Cocos Creator 环境下用 JS  和 TS 进行项目开发.19 年 7 月份开始,想和其 ...

  8. 手把手教你用 Cocos Creator 实现《大炮英雄》

    个人简介 一枚小工,多年 Cocos2d-x 和 Cocos Creator 游戏开发工程师.现阶段,主要在 Cocos Creator 环境下用 JS  和 TS 进行项目开发.19 年 7 月份开 ...

  9. Creator优化心得:减少脚本文件的大小

    星期2是公众号更新的一个时间点,Shawn这几天情事太多,眼看就快要过12点了,公众号教程看来今天黄呀!但是不发晚上睡不着呀,看来我是中了得到罗胖的毒"死磕自己",一定要把公众号给 ...

最新文章

  1. linux服务器性能监控命令汇总之iostat命令(三)
  2. python 示例_Python中带有示例的关键字除外
  3. 系统备份是对计算机硬件进行维户吗,计算机系统维护毕业论文_精品.doc
  4. linux部署tomcat项目404_一个tomcat下部署多个项目或一个服务器部署多个tomcat
  5. cad方格网高程lisp编程_分享:方格网法土方工程量计算实例例题(疏浚清淤工程)...
  6. 关于我的博客的一些说明
  7. ajax前端取消用户发送重复请求
  8. 计算机组装与维护心得体会作文,《计算机维护与局域网建设》学习心得
  9. unity-光照烘焙GI简单应用
  10. 计算机硬件是外观吗,计算机硬件从外观上看主要有主机箱.doc
  11. 我们要不要和to B“霸王龙”企业交朋友?
  12. POI最新版本 4.1.2 操作 Excel
  13. 极速模式下java无法加载_谷歌和360急速模式 下的XMLHttpRequest 的onprogress事件失效...
  14. 2022年全球与中国超快激光器市场现状及未来发展趋势
  15. 路由dns劫持,路由器DNS劫持简单实现和防范分析
  16. 黑客暴力破解必备的12大逆向工具!设置再复杂的密码也没用!
  17. oracle 分区字段作用,oracle表分区的作用
  18. Robot Framework + Selenium 框架,关键字封装,知识点记录
  19. 【微信小程序】collection.watch实现对云端数据的实时监控
  20. 程序人生 - 数字人民币与微信支付宝有何不同?

热门文章

  1. 使用 JavaScript 漂亮地打印 JSON
  2. R可视乎|三维散点图
  3. MySQL 上亿大表如何优化?
  4. 给动态生成的id标签添加html,vue动态渲染svg、添加点击事件
  5. 中原银行的 Arthas 技术实践分享
  6. js向上向下取整_JS之向上取整、向下取整、四舍五入等
  7. python日志:logging模块使用
  8. Kubernetes网络和集群性能测试
  9. python处理mongodb的ObjectId
  10. 【目标检测】基于yolov5海上船舶目标检测(附代码和数据集)