乾貨 | 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虛擬列表最佳實踐"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章