mirror of
https://github.com/zoriya/react-native-web.git
synced 2026-05-23 14:57:13 +00:00
[change] render Image 'source' immediately if previously loaded
Maintain a record of loaded images. If an image has already been loaded, bypass the JS loading logic and render it immediately. This prevents flashes of placeholder state when moving between screens or items in a virtualized list.
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
class ImageUriCache {
|
||||
static _maximumEntries: number = 256;
|
||||
static _entries = {};
|
||||
|
||||
static has(uri: string) {
|
||||
const entries = ImageUriCache._entries;
|
||||
const isDataUri = /^data:/.test(uri);
|
||||
return isDataUri || Boolean(entries[uri]);
|
||||
}
|
||||
|
||||
static add(uri: string) {
|
||||
const entries = ImageUriCache._entries;
|
||||
const lastUsedTimestamp = Date.now();
|
||||
if (entries[uri]) {
|
||||
entries[uri].lastUsedTimestamp = lastUsedTimestamp;
|
||||
entries[uri].refCount += 1;
|
||||
} else {
|
||||
entries[uri] = {
|
||||
lastUsedTimestamp,
|
||||
refCount: 1
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
static remove(uri: string) {
|
||||
const entries = ImageUriCache._entries;
|
||||
if (entries[uri]) {
|
||||
entries[uri].refCount -= 1;
|
||||
}
|
||||
// Free up entries when the cache is "full"
|
||||
ImageUriCache._cleanUpIfNeeded();
|
||||
}
|
||||
|
||||
static _cleanUpIfNeeded() {
|
||||
const entries = ImageUriCache._entries;
|
||||
const imageUris = Object.keys(entries);
|
||||
|
||||
if (imageUris.length + 1 > ImageUriCache._maximumEntries) {
|
||||
let leastRecentlyUsedKey;
|
||||
let leastRecentlyUsedEntry;
|
||||
|
||||
imageUris.forEach(uri => {
|
||||
const entry = entries[uri];
|
||||
if (
|
||||
(!leastRecentlyUsedEntry ||
|
||||
entry.lastUsedTimestamp < leastRecentlyUsedEntry.lastUsedTimestamp) &&
|
||||
entry.refCount === 0
|
||||
) {
|
||||
leastRecentlyUsedKey = uri;
|
||||
leastRecentlyUsedEntry = entry;
|
||||
}
|
||||
});
|
||||
|
||||
if (leastRecentlyUsedKey) {
|
||||
delete entries[leastRecentlyUsedKey];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = ImageUriCache;
|
||||
@@ -1,8 +1,9 @@
|
||||
/* eslint-env jasmine, jest */
|
||||
|
||||
import Image from '../';
|
||||
import ImageUriCache from '../ImageUriCache';
|
||||
import React from 'react';
|
||||
import { render } from 'enzyme';
|
||||
import { mount, render } from 'enzyme';
|
||||
|
||||
const originalImage = window.Image;
|
||||
|
||||
@@ -88,6 +89,32 @@ describe('components/Image', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('prop "source"', () => {
|
||||
test('is not set immediately if the image has not already been loaded', () => {
|
||||
const uri = 'https://google.com/favicon.ico';
|
||||
const source = { uri };
|
||||
const component = render(<Image source={source} />);
|
||||
expect(component.find('img')).toBeUndefined;
|
||||
});
|
||||
|
||||
test('is set immediately if the image has already been loaded', () => {
|
||||
const uriOne = 'https://google.com/favicon.ico';
|
||||
const uriTwo = 'https://twitter.com/favicon.ico';
|
||||
ImageUriCache.add(uriOne);
|
||||
ImageUriCache.add(uriTwo);
|
||||
|
||||
// initial render
|
||||
const component = mount(<Image source={{ uri: uriOne }} />);
|
||||
ImageUriCache.remove(uriOne);
|
||||
expect(component.render().find('img').attr('src')).toBe(uriOne);
|
||||
|
||||
// props update
|
||||
component.setProps({ source: { uri: uriTwo } });
|
||||
ImageUriCache.remove(uriTwo);
|
||||
expect(component.render().find('img').attr('src')).toBe(uriTwo);
|
||||
});
|
||||
});
|
||||
|
||||
describe('prop "style"', () => {
|
||||
test('correctly supports "resizeMode" property', () => {
|
||||
const component = render(<Image style={{ resizeMode: Image.resizeMode.contain }} />);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
/* global window */
|
||||
import applyNativeMethods from '../../modules/applyNativeMethods';
|
||||
import createDOMElement from '../../modules/createDOMElement';
|
||||
import ImageResizeMode from './ImageResizeMode';
|
||||
import ImageLoader from '../../modules/ImageLoader';
|
||||
import ImageResizeMode from './ImageResizeMode';
|
||||
import ImageStylePropTypes from './ImageStylePropTypes';
|
||||
import ImageUriCache from './ImageUriCache';
|
||||
import requestIdleCallback, { cancelIdleCallback } from '../../modules/requestIdleCallback';
|
||||
import StyleSheet from '../../apis/StyleSheet';
|
||||
import StyleSheetPropType from '../../propTypes/StyleSheetPropType';
|
||||
@@ -29,6 +30,10 @@ const ImageSourcePropType = oneOfType([
|
||||
string
|
||||
]);
|
||||
|
||||
const getImageState = (uri, isPreviouslyLoaded) => {
|
||||
return isPreviouslyLoaded ? STATUS_LOADED : uri ? STATUS_PENDING : STATUS_IDLE;
|
||||
};
|
||||
|
||||
const resolveAssetDimensions = source => {
|
||||
if (typeof source === 'object') {
|
||||
const { height, width } = source;
|
||||
@@ -73,17 +78,20 @@ class Image extends Component {
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = { shouldDisplaySource: false };
|
||||
// If an image has been loaded before, render it immediately
|
||||
const uri = resolveAssetSource(props.source);
|
||||
this._imageState = uri ? STATUS_PENDING : STATUS_IDLE;
|
||||
const isPreviouslyLoaded = ImageUriCache.has(uri);
|
||||
this.state = { shouldDisplaySource: isPreviouslyLoaded };
|
||||
this._imageState = getImageState(uri, isPreviouslyLoaded);
|
||||
isPreviouslyLoaded && ImageUriCache.add(uri);
|
||||
this._isMounted = false;
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this._isMounted = true;
|
||||
if (this._imageState === STATUS_PENDING) {
|
||||
this._createImageLoader();
|
||||
}
|
||||
this._isMounted = true;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
@@ -93,13 +101,18 @@ class Image extends Component {
|
||||
}
|
||||
|
||||
componentWillReceiveProps(nextProps) {
|
||||
const uri = resolveAssetSource(this.props.source);
|
||||
const nextUri = resolveAssetSource(nextProps.source);
|
||||
if (resolveAssetSource(this.props.source) !== nextUri) {
|
||||
this._updateImageState(nextUri ? STATUS_PENDING : STATUS_IDLE);
|
||||
if (uri !== nextUri) {
|
||||
ImageUriCache.remove(uri);
|
||||
const isPreviouslyLoaded = ImageUriCache.has(nextUri);
|
||||
isPreviouslyLoaded && ImageUriCache.add(uri);
|
||||
this._updateImageState(getImageState(uri, isPreviouslyLoaded));
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
ImageUriCache.remove(resolveAssetSource(this.props.source));
|
||||
this._destroyImageLoader();
|
||||
this._isMounted = false;
|
||||
}
|
||||
@@ -164,8 +177,8 @@ class Image extends Component {
|
||||
}
|
||||
|
||||
_createImageLoader() {
|
||||
this._destroyImageLoader();
|
||||
this._loadRequest = requestIdleCallback(() => {
|
||||
this._destroyImageLoader();
|
||||
const uri = resolveAssetSource(this.props.source);
|
||||
this._imageRequestId = ImageLoader.load(uri, this._onLoad, this._onError);
|
||||
this._onLoadStart();
|
||||
@@ -198,9 +211,9 @@ class Image extends Component {
|
||||
};
|
||||
|
||||
_onLoad = e => {
|
||||
const { onLoad } = this.props;
|
||||
const { onLoad, source } = this.props;
|
||||
const event = { nativeEvent: e };
|
||||
|
||||
ImageUriCache.add(resolveAssetSource(source));
|
||||
this._updateImageState(STATUS_LOADED);
|
||||
if (onLoad) {
|
||||
onLoad(event);
|
||||
|
||||
Reference in New Issue
Block a user