Skip to content

Commit 4d2b9af

Browse files
theniceangeldolymood
authored andcommitted
Recycle list fixbug (#410)
* fix(recycle-list): fix stop scroll bug * docs(recycle-list): optimize docs * fix(recycle-list): improve coverage * test(recycle-list): add some test suites * fix(recycle-list): normalize function name * refactor(recycle-list): change ele class * fix(recycle-list): set list vari to normal prop * fix(recycle-list): hide loading-spinner when stop scroll * fix(recycle-list): remove stopFetch variable * test(recycle-list): modify unit case * fix(recycle-list): clean unused whitespace and ajust position of noMore * fix(recycle-list): make noMore var reactive
1 parent 0389bfc commit 4d2b9af

File tree

4 files changed

+137
-94
lines changed

4 files changed

+137
-94
lines changed

document/components/docs/en-US/recycle-list.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ A recyclable scrolling list that always keeps the number of DOMs at a very low r
7878

7979
The vast majority of list interactions are when the user scrolls to the bottom and requests the next page of data. The default implementation of the component is based on this interaction.
8080

81-
The component accepts `size` as props and controls how many counts of data are rendered at a time. `offset` is the distance to configure the bottom pull data, `onFetch` is a function, which is mandatory, and the return value of the function must be a Promise, and the `items` ( **Array** ) must be the first parameter when calling `resolve` function, so the component can get `items`. Of course, if you want to stop scrolling, pass `false`.
81+
The component accepts `size` as props and controls how many counts of data are rendered at a time. `offset` is the distance to configure the bottom pull data, `onFetch` is a function, which is mandatory, and the return value of the function must be a Promise, and the `items` ( **Array** ) must be the first parameter when calling `resolve` function, so the component can get `items`. Of course, if you want to stop scrolling, pass `false` or an array whose length is smaller than `size` props.
8282

8383
The component supports the scope slot. You can use the destructuring assignment of the above example to get the `data` (each data item of item) that the component passes to the caller.
8484

document/components/docs/zh-CN/recycle-list.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@
134134
| offset | 底部拉取更多数据的距离 | Number | - | 200 |
135135
| onFetch | 获取更多数据 | Function | 必传 | - |
136136

137-
`onFetch` 函数必须返回一个 Promise,同时 Promise 的 resolve 函数的第一个参数必须是数组或者 `false`这样组件内部能拿到对应的数据来决定是否加载更多还是停止滚动
137+
`onFetch` 函数必须返回一个 Promise,同时 Promise 的 resolve 函数的第一个参数必须是数组或者 `false`如果是数组并且长度小于 size,那么组件会停止无限滚动,同理,如果是 `false`,也会停止
138138

139139
### 插槽
140140

src/components/recycle-list/recycle-list.vue

Lines changed: 90 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
<div class="cube-recycle-list-pool">
2727
<div
2828
class="cube-recycle-list-item cube-recycle-list-invisible"
29-
v-if="!item.isTombstone && !item.height"
29+
v-if="item && !item.isTombstone && !item.height"
3030
:ref="'preloads'+index"
3131
v-for="(item, index) in items"
3232
>
@@ -38,13 +38,13 @@
3838
</div>
3939
</div>
4040
<div
41-
v-if="!infinite"
41+
v-if="!infinite && !noMore"
4242
class="cube-recycle-list-loading"
4343
:style="{visibility: loading ? 'visible' : 'hidden'}"
4444
>
4545
<slot name="spinner">
4646
<div class="cube-recycle-list-loading-content">
47-
<cube-loading class="spinner"></cube-loading>
47+
<cube-loading class="cube-recycle-list-spinner"></cube-loading>
4848
</div>
4949
</slot>
5050
</div>
@@ -72,11 +72,9 @@
7272
data() {
7373
return {
7474
items: [],
75-
list: [],
7675
heights: 0,
7776
startIndex: 0,
7877
loadings: [],
79-
startOffset: 0,
8078
noMore: false
8179
}
8280
},
@@ -109,69 +107,94 @@
109107
return this.loadings.length
110108
}
111109
},
112-
watch: {
113-
list (newV) {
114-
if (newV.length) {
115-
this.loadings.pop()
116-
if (!this.loading) {
117-
this.loadItems()
118-
}
119-
}
120-
},
121-
items (newV) {
122-
if (newV.length > this.list.length) {
123-
this.getItems()
124-
}
125-
}
110+
created() {
111+
this.list = []
112+
this.promiseStack = []
126113
},
127114
mounted() {
128115
this.checkPromiseCompatibility()
129116
this.$el.addEventListener(EVENT_SCROLL, this._onScroll)
130117
window.addEventListener(EVENT_RESIZE, this._onResize)
131-
this.init()
118+
this.load()
132119
},
133-
beforeDestroy () {
120+
beforeDestroy() {
134121
this.$el.removeEventListener(EVENT_SCROLL, this._onScroll)
135122
window.removeEventListener(EVENT_RESIZE, this._onResize)
136123
},
137124
methods: {
138-
checkPromiseCompatibility () {
125+
checkPromiseCompatibility() {
139126
/* istanbul ignore if */
140127
if (isUndef(window.Promise)) {
141128
warn(PROMISE_ERROR)
142129
}
143130
},
144-
init() {
145-
this.load()
146-
},
147131
load() {
148132
if (this.infinite) {
133+
const items = this.items
134+
const start = items.length
149135
// increase capacity of items to display tombstone
150-
this.items.length += this.size
151-
this.loadItems()
136+
items.length += this.size
137+
const end = items.length
138+
this.loadItems(start, end)
139+
this.getItems()
152140
} else if (!this.loading) {
153141
this.getItems()
154142
}
155143
},
156144
getItems() {
145+
const index = this.promiseStack.length
146+
const promiseFetch = this.onFetch()
157147
this.loadings.push('pending')
158-
this.onFetch().then((res) => {
148+
this.promiseStack.push(promiseFetch)
149+
promiseFetch.then((res) => {
150+
this.loadings.pop()
159151
/* istanbul ignore if */
160152
if (!res) {
161-
this.noMore = true
162-
this.loadings.pop()
153+
this.stopScroll(index)
163154
} else {
164-
this.list = this.list.concat(res)
155+
this.setList(index, res)
156+
this.loadItemsByIndex(index)
157+
if (res.length < this.size) {
158+
this.stopScroll(index)
159+
}
165160
}
166161
})
167162
},
168-
loadItems(isResize) {
163+
removeUnusedTombs(copy, index) {
164+
let cursor
165+
let size = this.size
166+
let start = index * size
167+
let end = (index + 1) * size
168+
for (cursor = start; cursor < end; cursor++) {
169+
if (copy[cursor] && copy[cursor].isTombstone) break
170+
}
171+
this.items = copy.slice(0, cursor)
172+
},
173+
stopScroll(index) {
174+
this.noMore = true
175+
this.removeUnusedTombs(this.items.slice(0), index)
176+
this.updateItemTop()
177+
this.updateStartIndex()
178+
},
179+
setList(index, res) {
180+
const list = this.list
181+
const baseIndex = index * this.size
182+
for (let i = 0; i < res.length; i++) {
183+
list[baseIndex + i] = res[i]
184+
}
185+
},
186+
loadItemsByIndex(index) {
187+
const size = this.size
188+
const start = index * size
189+
const end = (index + 1) * size
190+
this.loadItems(start, end)
191+
},
192+
loadItems(start, end) {
193+
const items = this.items
169194
let promiseTasks = []
170-
let start = 0
171-
let end = this.infinite ? this.items.length : this.list.length
172195
let item
173196
for (let i = start; i < end; i++) {
174-
item = this.items[i]
197+
item = items[i]
175198
/* istanbul ignore if */
176199
if (item && item.loaded) {
177200
continue
@@ -185,6 +208,7 @@
185208
// update items top and full list height
186209
window.Promise.all(promiseTasks).then(() => {
187210
this.updateItemTop()
211+
this.updateStartIndex()
188212
})
189213
},
190214
setItem(index, data) {
@@ -202,60 +226,56 @@
202226
let dom = this.$refs['preloads' + index]
203227
if (dom && dom[0]) {
204228
cur.height = dom[0].offsetHeight
205-
} else {
206-
// tombstone
229+
} else if (cur) {
207230
cur.height = this.tombHeight
208231
}
209232
},
210233
updateItemTop() {
234+
let heights = 0
235+
const items = this.items
236+
let pre
237+
let current
211238
// loop all items to update item top and list height
212-
this.heights = 0
213-
for (let i = 0; i < this.items.length; i++) {
214-
let pre = this.items[i - 1]
215-
this.items[i].top = pre ? pre.top + pre.height : 0
216-
this.heights += this.items[i].height
217-
}
218-
// update scroll top when needed
219-
if (this.startOffset) {
220-
this.setScrollTop()
239+
for (let i = 0; i < items.length; i++) {
240+
pre = items[i - 1]
241+
current = items[i]
242+
// it is empty in array
243+
/* istanbul ignore if */
244+
if (!items[i]) {
245+
heights += 0
246+
} else {
247+
current.top = pre ? pre.top + pre.height : 0
248+
heights += current.height
249+
}
221250
}
222-
this.updateIndex()
251+
this.heights = heights
223252
},
224-
updateIndex() {
253+
updateStartIndex() {
225254
// update visible items start index
226255
let top = this.$el.scrollTop
227-
for (let i = 0; i < this.items.length; i++) {
228-
if (this.items[i].top > top) {
256+
let item
257+
const items = this.items
258+
for (let i = 0; i < items.length; i++) {
259+
item = items[i]
260+
if (!item || item.top > top) {
229261
this.startIndex = Math.max(0, i - 1)
230262
break
231263
}
232264
}
233265
},
234-
getStartItemOffset() {
235-
if (this.items[this.startIndex]) {
236-
this.startOffset = this.items[this.startIndex].top - this.$el.scrollTop
237-
}
238-
},
239-
setScrollTop() {
240-
if (this.items[this.startIndex]) {
241-
this.$el.scrollTop = this.items[this.startIndex].top - this.startOffset
242-
// reset start item offset
243-
this.startOffset = 0
244-
}
245-
},
246266
_onScroll() {
247267
// trigger load
248-
if (this.$el.scrollTop + this.$el.offsetHeight > this.heights - this.offset) {
268+
if (!this.noMore && this.$el.scrollTop + this.$el.offsetHeight > this.heights - this.offset) {
249269
this.load()
250270
}
251-
this.updateIndex()
271+
this.updateStartIndex()
252272
},
253273
_onResize() {
254-
this.getStartItemOffset()
255-
this.items.forEach((item) => {
274+
const items = this.items
275+
items.forEach((item) => {
256276
item.loaded = false
257277
})
258-
this.loadItems(true)
278+
this.loadItems(0, items.length)
259279
}
260280
},
261281
components: {
@@ -298,15 +318,8 @@
298318
299319
.cube-recycle-list-loading-content
300320
text-align: center
301-
.spinner
302-
margin: 10px auto
303-
display: flex
304-
justify-content: center
305-
306-
.cube-recycle-list-noMore
307-
overflow: hidden
321+
.cube-recycle-list-spinner
308322
margin: 10px auto
309-
height: 20px
310-
text-align: center
323+
display: flex
324+
justify-content: center
311325
</style>
312-

test/unit/specs/recycle-list.spec.js

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,23 @@ describe('RecycleList', () => {
2626
setTimeout(() => {
2727
const length = vm.items.length
2828
expect(length)
29-
.to.equal(50)
29+
.to.equal(10)
3030
vm.$el.scrollTop = 1000
3131
setTimeout(() => {
3232
const length = vm.items.length
3333
expect(length)
34-
.to.equal(100)
34+
.to.equal(15)
35+
done()
36+
}, 500)
37+
}, 500)
38+
})
39+
it('should stop scroll', (done) => {
40+
vm = createRecycleList(true)
41+
setTimeout(() => {
42+
vm.$el.scrollTop = 2000
43+
setTimeout(() => {
44+
expect(vm.noMore)
45+
.to.equal(true)
3546
done()
3647
}, 500)
3748
}, 500)
@@ -65,25 +76,44 @@ function createRecycleList (infinite) {
6576
`,
6677
data () {
6778
return {
68-
size: 50,
69-
infinite
79+
size: 10,
80+
infinite,
81+
pid: 0
7082
}
7183
},
7284
methods: {
7385
onFetch () {
7486
let items = []
87+
this.pid += 1
88+
const pid = this.pid
7589
return new Promise((resolve) => {
76-
setTimeout(() => {
77-
for (let i = 0; i < 50; i++) {
78-
items.push({
79-
id: i,
80-
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/danpliego/128.jpg',
81-
msg: '123',
82-
time: 'Thu Oct 25 2018 15:02:12 GMT+0800 (中国标准时间)'
83-
})
84-
}
85-
resolve(items)
86-
}, 100)
90+
if (pid > 2) {
91+
resolve(false)
92+
} else if (pid === 2) {
93+
setTimeout(() => {
94+
for (let i = 0; i < 5; i++) {
95+
items.push({
96+
id: i,
97+
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/danpliego/128.jpg',
98+
msg: '123',
99+
time: 'Thu Oct 25 2018 15:02:12 GMT+0800 (中国标准时间)'
100+
})
101+
}
102+
resolve(items)
103+
}, 100)
104+
} else {
105+
setTimeout(() => {
106+
for (let i = 0; i < 10; i++) {
107+
items.push({
108+
id: i,
109+
avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/danpliego/128.jpg',
110+
msg: '123',
111+
time: 'Thu Oct 25 2018 15:02:12 GMT+0800 (中国标准时间)'
112+
})
113+
}
114+
resolve(items)
115+
}, 100)
116+
}
87117
})
88118
}
89119
}

0 commit comments

Comments
 (0)