干货 | Taro虚拟列表最佳实践

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、背景"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最近组内小程序项目从Taro1迁移到了Taro3,紧跟凹凸实验室的步伐,开发体验确实比版本1好了很多,完全支持React语法,没有了那么多鸡肋的限制,项目的可配置程度也大大放开,充分给予了开发者自由发挥的空间。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是由于Taro3是运行时架构,是以牺牲页面部分性能为代价的,这也间接导致了我们的列表页异常卡顿,由于我们的列表页是一次性请求所有数据,然后进行渲染,所以页面节点初始化渲染的时候会渲染很多节点,再加上一些筛选项,不用说用户,卡顿已经让我们自己都忍受不了。此为背景。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文我们会先分析页面卡顿的原因,然后寻找对应的一些解决方案,分析其可行性,最后结合前期的问题解析,给出一套最优的解决方案。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、原因分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)页面节点过多,渲染时间变长,阻碍了用户快速操作的需求;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)列表setState数据量太大,造成逻辑层与渲染层的通讯时间变长;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)修改state,例如点击列表筛选项,列表数据需要重新大量渲染,造成页面卡顿;"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、解决方案"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"方案一:后端分页"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们第一时间想到让接口分页,这样初始化渲染的时候就不会渲染大量节点,然后监听下拉到底时机,再依次渲染数据;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是该方案第一时间被毙掉,原因:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"列表页接口不只有小程序在用,app客户端也在共用同一套接口,如果想让接口变更,那么app客户端也跟着去修改逻辑(列表页的逻辑也挺复杂的),因为我们去尝试给客户端增加需求量,不太厚道; "}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"就算说服了客户端和服务端一起去修改,我们页面的初始化速度提升了,但是随着页面上拉,数据加载越来越多,当加载到一定数量之后,再操作页面的筛选项,依然会导致操作卡顿;"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"总结:想让页面初始化以及数据全部加载完成之后不卡顿,除非减少setState的数据量以及减少页面总的渲染节点数量,因此只能采用虚拟列表。"}]}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"方案二:官方虚拟列表(3.2.1版本)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"官方文档:"},{"type":"link","attrs":{"href":"https:\/\/docs.taro.zone\/docs\/virtual-list","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/docs.taro.zone\/docs\/virtual-list"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原理:只渲染当前可视区域内的数据节点,监听页面可视区域,不在可视区域的节点不再渲染,这样一来就大大减少了页面节点渲染数量。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用效果:团队第一时间尝试了虚拟列表,但是效果并不是非常理想,主要问题有以下几点:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"由于我们的列表内容不是所有的Item都是等高的,所以虚拟列表每次渲染的时候都会去动态计算每个Item的高度,造成列表高度变换抖动;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上拉加载过程中偶尔会出现无限上滑加载的问题,造成页面紊乱;"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"滑动速度太快会导致页面很长一段时间的白屏,体验不佳;"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"总结:已知问题需要官方团队去解决,但是要等,而且Item不等高,需要频繁动态计算Item高度的问题并不好解决,目前市面上也没有什么特别好的方案,因此该方案也被搁浅了。"}]}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四、方案分析"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)减少页面节点数量:只能采用虚拟列表,只渲染当前可视区域内的节点;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)减少setState的数据量:能不能不每次都去全量setState;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)动态计算Item高度:每次都重新计算每个Item高度,计算量太大,也会阻碍页面渲染;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基于以上问题,我们团队最终出品了更佳(没有最佳,只有更佳)虚拟列表方案。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"五、终极(更佳)方案"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.1 效果概览"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"动图预览:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/aa\/aadc25e58606776ac78e18de9cf0e5f1.gif","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要看一下虚拟列表节点组成:"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/6b\/6bede3cd81c4bf5f1e0dba54449b01e5.jpeg","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.2 前期思考"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)继续采用监听可视区域,只渲染可视区域内的节点。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)由于Item不等高问题,需要动态计算每个Item的高度,效果不佳,我们放弃。因为只渲染当前可视区域内的数据,那么能不能以每一屏的数据为一个维度(界限),当一屏数据渲染完成之后,记录一下该屏幕节点所占的整体高度,当该屏幕的节点再次进入可视区域,我们将记录下的高度重新赋予这一屏幕,这样是不是就减少了大量计算的工作?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)为了减少setState的数据量,不在可视区域内的那些屏幕的数据,可否用该屏幕的高度(一个简单的对象数据结构)去占位?好像思路都能说的过去,那到底可不可行呢,下面我们来一探究竟吧。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.3 Coding"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"格式化数据"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我们需要外部传入列表数据list,然后在组件内部加工一下,按照一屏一屏渲染的思路,暂且把list改为二维数组,一个维度就是一屏的数据;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nexport default class VirtialList extends Component {\n constructor(props) {\n super(props)\n this.state = {\n twoList: [], \/\/ 二维数组\n }\n }\n componentDidMount() {\n \/\/ 接收外部传入的列表数据\n const { list } = this.props\n \/\/ 将list格式化为二维数组\n this.formatList(list)\n }\n initList = [] \/\/ 承载初始化的二维数组,该数组初始化完成之后就不会再变了,除非外部list变化\n \/**\n * 将列表格式化为二维\n * @param list 列表\n *\/\n formatList(list) {\n \/\/ 用户可自定义二维数组每一个维度的数据量\n const { segmentNum } = this.props\n \n let arr = []\n const _list = [] \/\/ 二维数组副本\n list.forEach((item, index) => {\n arr.push(item)\n if ((index + 1) % segmentNum === 0) {\n \/\/ 够一个维度的量就装进_list\n _list.push(arr)\n arr = []\n }\n })\n \/\/ 将分段不足segmentNum的剩余数据装入_list\n const restList = list.slice(_list.length * segmentNum)\n if (restList?.length) {\n _list.push(restList)\n }\n this.initList = _list\n this.setState({\n twoList: _list.slice(0, 1), \/\/ 第一次渲染,只取第一个维度的数据\n })\n }\n render() {\n const {\n twoList,\n } = this.state\n \/\/ 渲染回调\n const { onRender } = this.props\n return (\n \n \n {\n twoList?.map((item, pageIndex) => {\n return (\n \/\/ 每一个屏幕都用一个节点包裹着\n \n {\n item.map((el, index) => {\n return onRender?.(el, (pageIndex * segmentNum + index), pageIndex)\n })\n }\n \n )\n })\n }\n \n \n )\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"设置屏幕高度"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我们已将数据格式化为二维数组了,初始化渲染的时候只会渲染数组的第一维度,那么在该维度节点渲染完成之后,需要记录下该维度节点所占屏幕的一个高度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nstate = {\n wholePageIndex: 0, \/\/ 每一屏为一个单位,屏幕索引\n}\nformatList(list) {\n \/\/ ...\n this.setState({\n twoList: _list.slice(0, 1),\n }, () => {\n \/\/ 注意:放在下一个事件循环去获取节点,更有保障\n Taro.nextTick(() => {\n this.setHeight()\n })\n })\n}\npageHeightArr = [] \/\/ 用来装每一屏的高度\nsetHeight():void {\n const { wholePageIndex } = this.state\n const query = Taro.createSelectorQuery()\n query.select(`.wrap_${wholePageIndex}`).boundingClientRect()\n query.exec((res) => {\n this.pageHeightArr.push(res?.[0]?.height)\n })\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"上拉加载"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"利用ScrollView的onScrollToLower属性,监听列表上拉至底部,加载下一个维度的数据,塞入二维数组列表。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n\n\/\/...\n\n\nrenderNext = () => {\n \/\/ 每次加载下一屏幕的数据,修改屏幕索引\n const page_index = this.state.wholePageIndex + 1\n\n this.setState({\n wholePageIndex: page_index,\n }, () => {\n const { wholePageIndex, twoList } = this.state\n \/\/ 找到当前屏幕的对应的数据,塞入二维数组\n twoList[wholePageIndex] = this.initList[wholePageIndex]\n this.setState({\n twoList: [...twoList],\n }, () => {\n Taro.nextTick(() => {\n this.setHeight()\n })\n })\n })\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"监听可视区域"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"利用observer对象的监听方法observe,监听当前可视区域,渲染对应维度的数据,那么不在可视区域内的数据要怎么处理呢?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"这也是该组件最重要的一环,当不在可视区域内的数据,因为我们之前已经记录了该维度节点渲染之后的一个高度,那么我们就利用一个节点赋予对应的高度,进行占位!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nsetHeight() {\n \/\/...\n this.observe()\n}\nobserve = () => {\n const { wholePageIndex } = this.state\n \/\/ 外界用户传入的组件高度\n const { scrollViewProps } = this.props\n \/\/ 以传入的scrollView的高度为相交区域的参考边界,若没传,则默认使用屏幕高度\n const scrollHeight = scrollViewProps?.style?.height || this.windowHeight\n \/\/ 设定监听的范围,我们这里默认监听上下两个屏幕的高度\n const observer = Taro.createIntersectionObserver(this.currentPage.page).relativeToViewport({\n top: 2 * scrollHeight,\n bottom: 2 * scrollHeight,\n })\n observer.observe(`.wrap_${wholePageIndex}`, (res) => {\n const { twoList } = this.state\n if (res?.intersectionRatio <= 0) {\n \/\/ 当没有与当前视口有相交区域,则将该屏的数据置为该屏的高度占位\n twoList[wholePageIndex] = { height: this.pageHeightArr[wholePageIndex] }\n this.setState({\n twoList: [...twoList],\n })\n } else if (!twoList[wholePageIndex]?.length) {\n \/\/ 如果有相交区域,则将对应的维度的数据塞入二维数组\n twoList[wholePageIndex] = this.initList[wholePageIndex]\n this.setState({\n twoList: [...twoList],\n })\n }\n })\n}\nrender() {\n return (\n \n \n {\n twoList?.map((item, pageIndex) => {\n return (\n \n {\n item?.length > 0 ? (\n \n {\n item.map((el, index) => {\n return onRender?.(el, (pageIndex * segmentNum + index), pageIndex)\n })\n }\n \n ) : (\n \n )\n }\n \n )\n })\n }\n \n \n )\n}"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"六、性能提升"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接下来是智行小程序机票列表页优化前跟优化后的几组数据对比:"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"列表页渲染时长"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要指的是页面航线列表的渲染总时间。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/0c\/0c6692c47428bf6e8e898c6e267bfa1b.png","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"筛选项响应时间"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"主要指的是从点击页面下方筛选按钮时间开始算起,到底部浮层弹出的时间间隔,单位毫秒。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/60\/600df7585f05b1c360253fac68ec3b83.jpeg","alt":"图片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"性能提升总结"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可以看出在使用虚拟列表对页面进行优化之后,页面总的渲染性能会有一个质的提升,页面列表渲染速度提升了将近45%,按钮点击响应速度提升了将近50%。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前我们只是针对航班列表使用了虚拟列表进行优化,页面中还有一个比较损耗性能的点是上方的日历列表,后期我们将把日历列表也改成虚拟列表,相信性能会更进一步提升。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"七、总结"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"组件的实现比较简单,关键点就在于:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)将列表数据格式化为二维数组; "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)不在可视区域内的数据用{height: xx px}填充,减少了列表数据setState的量;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)动态计算每一个屏幕的高度并记录,减少计算量;"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"八、最后"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"该组件支持列表内部节点不等高的棘手问题,目前已经用于生产环境,运行稳定 。如果这篇文章对你现在的开发有一些帮助,或者说给你带来了一些更好的思考,欢迎一起来讨论。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"github"},{"type":"text","text":":"},{"type":"link","attrs":{"href":"https:\/\/github.com\/tingyuxuan2302\/taro3-virtual-list","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/github.com\/tingyuxuan2302\/taro3-virtual-list"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"npm包"},{"type":"text","text":":"},{"type":"link","attrs":{"href":"https:\/\/www.npmjs.com\/package\/taro-virtual-list","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/www.npmjs.com\/package\/taro-virtual-list"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"Taro物料市场"},{"type":"text","text":":"},{"type":"link","attrs":{"href":"https:\/\/taro-ext.jd.com\/plugin\/view\/60bf31e23ac107d9df4685cb","title":null,"type":null},"content":[{"type":"text","text":"https:\/\/taro-ext.jd.com\/plugin\/view\/60bf31e23ac107d9df4685cb"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者简介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不浪,携程高级前端开发工程师,关注前端热门技术,目前从事小程序的相关开发与优化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文转载自:携程技术(ID:ctriptech)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文链接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/XHJ40fgg-bES4B-gqEKOMQ","title":"xxx","type":null},"content":[{"type":"text","text":"干货 | Taro虚拟列表最佳实践"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章