Skip to content

Commit

Permalink
fix(image): lazy load
Browse files Browse the repository at this point in the history
  • Loading branch information
07akioni committed Jun 19, 2022
1 parent 32cf3be commit cd80bcf
Show file tree
Hide file tree
Showing 8 changed files with 119 additions and 32 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### Feats

- 🌟 `n-image` adds `lazy` prop, closes [#3055](https://github.com/TuSimple/naive-ui/issues/3055).
- `n-image` adds `intersection-observer-options` prop.
- Exports `NTooltipInst` type.
- `n-data-table` adds `render-cell` prop, closes [#3095](https://github.com/TuSimple/naive-ui/issues/3095).
- `n-space` adds `wrap-item` prop.
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
### Feats

- 🌟 `n-image` 新增 `lazy` 属性,关闭 [#3055](https://github.com/TuSimple/naive-ui/issues/3055)
- `n-image` 新增 `intersection-observer-options` 属性
- 导出 `NTooltipInst` 类型
- `n-data-table` 新增 `render-cell` 属性,关闭 [#3095](https://github.com/TuSimple/naive-ui/issues/3095)
- `n-space` 新增 `wrap-item` 属性
Expand Down
2 changes: 2 additions & 0 deletions src/image/demos/enUS/index.demo-entry.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ lazy.vue
| fallback-src | `string` | `undefined` | URL to show when the image fails to load. | |
| height | `string \| number` | `undefined` | Image height. | |
| img-props | `object` | `undefined` | The props of the img element inside the component. | |
| lazy | `boolean` | `false` | Whether to show after it enters viewport configured by `intersection-observer-options` | NEXT_VERSION |
| intersection-observer-options | `{ root?: Element \| Document \| string \| null, rootMargin?: string, threshold?: number \| number[]; }` | `undefined` | Intersection observer's config to be applied when `lazy=true`. | NEXT_VERSION |
| object-fit | `'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale-down'` | `fill` | Object-fit type of the image in the container. | |
| preview-src | `string` | `undefined` | Source of preview image. | |
| preview-disabled | `boolean` | `false` | Whether clicking image preview is disabled. | |
Expand Down
51 changes: 41 additions & 10 deletions src/image/demos/enUS/lazy.demo.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
<markdown>
# Lazy load

You can use `lazy` to let image load after it enters viewport.
</markdown>

<template>
<n-image
v-for="(item, index) in Array(10)"
:key="index"
width="100"
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
lazy
:lazy-options="{
root: '.n-layout--static-positioned'
}"
/>
<div
id="image-scroll-container"
style="overflow: auto; height: 100px; display: flex; flex-direction: column"
>
<n-image
v-for="(src, index) in srcList"
:key="index"
width="100"
height="100"
lazy
:src="src"
:intersection-observer-options="{
root: '#image-scroll-container'
}"
/>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
return {
srcList: [
'https://picsum.photos/id/1/100/100',
'https://picsum.photos/id/2/100/100',
'https://picsum.photos/id/3/100/100',
'https://picsum.photos/id/4/100/100',
'https://picsum.photos/id/5/100/100',
'https://picsum.photos/id/6/100/100',
'https://picsum.photos/id/7/100/100',
'https://picsum.photos/id/8/100/100',
'https://picsum.photos/id/9/100/100',
'https://picsum.photos/id/10/100/100'
]
}
}
})
</script>
2 changes: 2 additions & 0 deletions src/image/demos/zhCN/index.demo-entry.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ lazy.vue
| fallback-src | `string` | `undefined` | 图片加载失败时显示的地址 | |
| height | `string \| number` | `undefined` | 图片高度 | |
| img-props | `object` | `undefined` | 组件中 img 元素的属性 | |
| lazy | `boolean` | `false` | 是否在进入 `intersection-observer-options` 配置的视口之后再开始加载 | NEXT_VERSION |
| intersection-observer-options | `{ root?: Element \| Document \| string \| null, rootMargin?: string, threshold?: number \| number[]; }` | `undefined` | `lazy=true` 时 intersection observer 观测的配置 | NEXT_VERSION |
| object-fit | `'fill' \| 'contain' \| 'cover' \| 'none' \| 'scale-down'` | `'fill'` | 图片在容器内的的适应类型 | |
| preview-src | `string` | `undefined` | 预览图片的图片地址 | |
| preview-disabled | `boolean` | `false` | 是否可以点击图片进行预览 | |
Expand Down
51 changes: 41 additions & 10 deletions src/image/demos/zhCN/lazy.demo.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,47 @@
<markdown>
# 懒加载

你可以使用 `lazy` 属性让图片进入视口再加载。
</markdown>

<template>
<n-image
v-for="(item, index) in Array(10)"
:key="index"
width="100"
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
lazy
:lazy-options="{
root: '.n-layout--static-positioned'
}"
/>
<div
id="image-scroll-container"
style="overflow: auto; height: 100px; display: flex; flex-direction: column"
>
<n-image
v-for="(src, index) in srcList"
:key="index"
width="100"
height="100"
lazy
:src="src"
:intersection-observer-options="{
root: '#image-scroll-container'
}"
/>
</div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
setup () {
return {
srcList: [
'https://picsum.photos/id/1/100/100',
'https://picsum.photos/id/2/100/100',
'https://picsum.photos/id/3/100/100',
'https://picsum.photos/id/4/100/100',
'https://picsum.photos/id/5/100/100',
'https://picsum.photos/id/6/100/100',
'https://picsum.photos/id/7/100/100',
'https://picsum.photos/id/8/100/100',
'https://picsum.photos/id/9/100/100',
'https://picsum.photos/id/10/100/100'
]
}
}
})
</script>
14 changes: 9 additions & 5 deletions src/image/src/Image.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ export default defineComponent({
previewInst.toggleShow()
}
}

const shouldStartLoadingRef = ref(!props.lazy)

onMounted(() => {
imageRef.value?.setAttribute(
'data-group-id',
Expand All @@ -91,7 +94,8 @@ export default defineComponent({
if (props.lazy) {
unobserve = observeIntersection(
imageRef.value,
props.intersectionObserverOptions
props.intersectionObserverOptions,
shouldStartLoadingRef
)
}
})
Expand All @@ -113,6 +117,7 @@ export default defineComponent({
imageRef,
imgProps: imgPropsRef,
showError: showErrorRef,
shouldStartLoading: shouldStartLoadingRef,
mergedOnError: (e: Event) => {
showErrorRef.value = true
const { onError, imgProps: { onError: imgPropsOnError } = {} } = props
Expand All @@ -139,9 +144,9 @@ export default defineComponent({
src={
this.showError
? this.fallbackSrc
: this.lazy
? undefined
: this.src || imgProps.src
: this.shouldStartLoading
? this.src || imgProps.src
: undefined
}
alt={this.alt || imgProps.alt}
aria-label={this.alt || imgProps.alt}
Expand All @@ -151,7 +156,6 @@ export default defineComponent({
style={[imgProps.style || '', { objectFit: this.objectFit }]}
data-error={this.showError}
data-preview-src={this.previewSrc || this.src}
data-src={this.src || imgProps.src}
/>
)

Expand Down
29 changes: 22 additions & 7 deletions src/image/src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { Ref } from 'vue'

export type IntersectionObserverOptions = Omit<
IntersectionObserverInit,
'root'
Expand Down Expand Up @@ -33,10 +35,14 @@ Document | Element,
Map<string, [IntersectionObserver, Set<Element | Document>]>
>()

const unobserveHandleMap = new WeakMap<HTMLImageElement, () => void>()
const shouldStartLoadingRefMap = new WeakMap<HTMLImageElement, Ref<boolean>>()

export const observeIntersection: (
el: HTMLImageElement | null,
options: IntersectionObserverOptions | undefined
) => () => void = (el, options) => {
options: IntersectionObserverOptions | undefined,
shouldStartLoadingRef: Ref<boolean>
) => () => void = (el, options, shouldStartLoadingRef) => {
if (!el) return () => {}
const resolvedOptionsAndHash = resolveOptionsAndHash(options)
const { root } = resolvedOptionsAndHash.options
Expand Down Expand Up @@ -69,21 +75,28 @@ export const observeIntersection: (
observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
unobserve()
const img = entry.target as HTMLImageElement
if (!img.src) {
img.src = img.dataset.src || ''
const _unobserve = unobserveHandleMap.get(
entry.target as HTMLImageElement
)
const _shouldStartLoadingRef = shouldStartLoadingRefMap.get(
entry.target as HTMLImageElement
)
if (_unobserve) _unobserve()
if (_shouldStartLoadingRef) {
_shouldStartLoadingRef.value = true
}
}
})
})
}, resolvedOptionsAndHash.options)
observer.observe(el)
observerAndObservedElements = [observer, new Set([el])]
rootObservers.set(resolvedOptionsAndHash.hash, observerAndObservedElements)
}
let unobservered = false
const unobserve = (): void => {
if (unobservered) return
unobserveHandleMap.delete(el)
shouldStartLoadingRefMap.delete(el)
unobservered = true
if (observerAndObservedElements[1].has(el)) {
observerAndObservedElements[0].unobserve(el)
Expand All @@ -96,5 +109,7 @@ export const observeIntersection: (
observers.delete(root)
}
}
unobserveHandleMap.set(el, unobserve)
shouldStartLoadingRefMap.set(el, shouldStartLoadingRef)
return unobserve
}

0 comments on commit cd80bcf

Please sign in to comment.