mirror of
https://github.com/ionic-team/ionic-framework.git
synced 2025-08-15 09:34:19 +08:00
fix(react): first render performance improvements
This commit is contained in:
@ -7,6 +7,7 @@ import { RouteComponentProps, matchPath, withRouter } from 'react-router-dom';
|
||||
import { generateId, isDevMode } from '../utils';
|
||||
import { LocationHistory } from '../utils/LocationHistory';
|
||||
|
||||
import { IonRouteAction } from './IonRouteAction';
|
||||
import { IonRouteData } from './IonRouteData';
|
||||
import { NavManager } from './NavManager';
|
||||
import { RouteManagerContext, RouteManagerContextState } from './RouteManagerContext';
|
||||
@ -15,15 +16,19 @@ import { ViewStack, ViewStacks } from './ViewStacks';
|
||||
|
||||
interface RouteManagerState extends RouteManagerContextState {
|
||||
location?: HistoryLocation;
|
||||
action?: HistoryAction;
|
||||
action?: IonRouteAction;
|
||||
}
|
||||
|
||||
class RouteManager extends React.Component<RouteComponentProps, RouteManagerState> {
|
||||
listenUnregisterCallback: UnregisterCallback | undefined;
|
||||
activeIonPageId?: string;
|
||||
currentDirection?: RouterDirection;
|
||||
currentIonRouteAction?: IonRouteAction;
|
||||
currentRouteDirection?: RouterDirection;
|
||||
locationHistory = new LocationHistory();
|
||||
routes: { [key: string]: any; } = {};
|
||||
routes: { [key: string]: React.ReactElement<any>; } = {};
|
||||
ionPageElements: { [key: string]: HTMLElement; } = {};
|
||||
routerOutlets: { [key: string]: HTMLIonRouterOutletElement; } = {};
|
||||
firstRender = true;
|
||||
|
||||
constructor(props: RouteComponentProps) {
|
||||
super(props);
|
||||
@ -52,7 +57,8 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
componentDidUpdate(_prevProps: RouteComponentProps, prevState: RouteManagerState) {
|
||||
// Trigger a page change if the location or action is different
|
||||
if (this.state.location && prevState.location !== this.state.location || prevState.action !== this.state.action) {
|
||||
this.setActiveView(this.state.location!, this.state.action!);
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
this.setActiveView(this.state.location!, this.state.action!, viewStacks);
|
||||
}
|
||||
}
|
||||
|
||||
@ -71,10 +77,10 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
const { view } = viewStacks.findViewInfoById(viewId);
|
||||
if (view) {
|
||||
view.show = false;
|
||||
view.ionPageElement = undefined;
|
||||
view.isIonRoute = false;
|
||||
view.prevId = undefined;
|
||||
view.key = generateId();
|
||||
delete this.ionPageElements[view.id];
|
||||
this.setState({
|
||||
viewStacks
|
||||
});
|
||||
@ -82,23 +88,29 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
}
|
||||
|
||||
historyChange(location: HistoryLocation, action: HistoryAction) {
|
||||
location.state = location.state || { direction: this.currentDirection };
|
||||
this.currentDirection = undefined;
|
||||
if (action === 'PUSH') {
|
||||
const ionRouteAction = this.currentIonRouteAction === 'pop' ? 'pop' : action.toLowerCase();
|
||||
let direction = this.currentRouteDirection;
|
||||
|
||||
if (ionRouteAction === 'push') {
|
||||
this.locationHistory.add(location);
|
||||
} else if ((action === 'REPLACE' && location.state.direction === 'back') || action === 'POP') {
|
||||
} else if (ionRouteAction === 'pop') {
|
||||
this.locationHistory.pop();
|
||||
} else {
|
||||
direction = direction || 'back';
|
||||
} else if (ionRouteAction === 'replace') {
|
||||
this.locationHistory.replace(location);
|
||||
direction = 'none';
|
||||
}
|
||||
|
||||
location.state = location.state || { direction };
|
||||
this.setState({
|
||||
location,
|
||||
action
|
||||
action: ionRouteAction as IonRouteAction
|
||||
});
|
||||
this.currentRouteDirection = undefined;
|
||||
this.currentIonRouteAction = undefined;
|
||||
}
|
||||
|
||||
setActiveView(location: HistoryLocation, action: HistoryAction) {
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
setActiveView(location: HistoryLocation, action: IonRouteAction, viewStacks: ViewStacks) {
|
||||
let direction: RouterDirection | undefined = (location.state && location.state.direction) || 'forward';
|
||||
let leavingView: ViewItem | undefined;
|
||||
const viewStackKeys = viewStacks.getKeys();
|
||||
@ -122,25 +134,18 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
this.activeIonPageId = enteringView.id;
|
||||
|
||||
if (leavingView) {
|
||||
if (direction === 'forward') {
|
||||
if (action === 'PUSH') {
|
||||
/**
|
||||
* If the page is being pushed into the stack by another view,
|
||||
* record the view that originally directed to the new view for back button purposes.
|
||||
*/
|
||||
enteringView.prevId = leavingView.id;
|
||||
} else if (action === 'POP') {
|
||||
direction = leavingView.prevId === enteringView.id ? 'back' : 'none';
|
||||
} else {
|
||||
direction = direction || 'back';
|
||||
leavingView.mount = false;
|
||||
}
|
||||
}
|
||||
if (direction === 'back' || action === 'REPLACE') {
|
||||
if (action === 'push' && direction === 'forward') {
|
||||
/**
|
||||
* If the page is being pushed into the stack by another view,
|
||||
* record the view that originally directed to the new view for back button purposes.
|
||||
*/
|
||||
enteringView.prevId = leavingView.id;
|
||||
} else if (action === 'pop' || action === 'replace') {
|
||||
leavingView.mount = false;
|
||||
this.removeOrphanedViews(enteringView, enteringViewStack);
|
||||
}
|
||||
leavingViewHtml = enteringView.id === leavingView.id ? leavingView.ionPageElement!.outerHTML : undefined;
|
||||
|
||||
leavingViewHtml = enteringView.id === leavingView.id ? this.ionPageElements[leavingView.id].outerHTML : undefined;
|
||||
} else {
|
||||
// If there is not a leavingView, then we shouldn't provide a direction
|
||||
direction = undefined;
|
||||
@ -167,16 +172,17 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
if (shouldTransitionPage) {
|
||||
const { view: enteringView, viewStack } = this.state.viewStacks.findViewInfoById(this.activeIonPageId);
|
||||
if (enteringView && viewStack) {
|
||||
const enteringEl = enteringView.ionPageElement ? enteringView.ionPageElement : undefined;
|
||||
const leavingEl = leavingView && leavingView.ionPageElement ? leavingView.ionPageElement : undefined;
|
||||
const enteringEl = this.ionPageElements[enteringView.id];
|
||||
const leavingEl = leavingView && this.ionPageElements[leavingView.id];
|
||||
if (enteringEl) {
|
||||
// Don't animate from an empty view
|
||||
const navDirection = leavingEl && leavingEl.innerHTML === '' ? undefined : direction === 'none' ? undefined : direction;
|
||||
const shouldGoBack = !!enteringView.prevId;
|
||||
const routerOutlet = this.routerOutlets[viewStack.id];
|
||||
this.commitView(
|
||||
enteringEl!,
|
||||
leavingEl!,
|
||||
viewStack.routerOutlet,
|
||||
routerOutlet,
|
||||
navDirection,
|
||||
shouldGoBack,
|
||||
leavingViewHtml);
|
||||
@ -211,12 +217,13 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
this.removeOrphanedViews(v, viewStack);
|
||||
|
||||
// If view is not currently visible, go ahead and remove it from DOM
|
||||
if (v.ionPageElement!.classList.contains('ion-page-hidden')) {
|
||||
const page = this.ionPageElements[v.id];
|
||||
if (page.classList.contains('ion-page-hidden')) {
|
||||
v.show = false;
|
||||
v.ionPageElement = undefined;
|
||||
v.isIonRoute = false;
|
||||
v.prevId = undefined;
|
||||
v.key = generateId();
|
||||
delete this.ionPageElements[v.id];
|
||||
}
|
||||
v.mount = false;
|
||||
}
|
||||
@ -270,9 +277,9 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
const prevViewStacks = Object.assign(new ViewStacks(), prevState.viewStacks);
|
||||
const newStack: ViewStack = {
|
||||
id: stack,
|
||||
views: stackItems,
|
||||
routerOutlet
|
||||
views: stackItems
|
||||
};
|
||||
this.routerOutlets[stack] = routerOutlet;
|
||||
if (activeId) {
|
||||
this.activeIonPageId = activeId;
|
||||
}
|
||||
@ -286,18 +293,6 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
}
|
||||
|
||||
async setupRouterOutlet(routerOutlet: HTMLIonRouterOutletElement) {
|
||||
const waitUntilReady = async () => {
|
||||
if (routerOutlet.componentOnReady) {
|
||||
routerOutlet.dispatchEvent(new Event('routerOutletReady'));
|
||||
return;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
waitUntilReady();
|
||||
}, 0);
|
||||
}
|
||||
};
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
const canStart = () => {
|
||||
const config = getConfig();
|
||||
@ -329,20 +324,13 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
}
|
||||
|
||||
syncView(page: HTMLElement, viewId: string) {
|
||||
this.setState(state => {
|
||||
const viewStacks = Object.assign(new ViewStacks(), state.viewStacks);
|
||||
const { view } = viewStacks.findViewInfoById(viewId);
|
||||
|
||||
view!.ionPageElement = page;
|
||||
view!.isIonRoute = true;
|
||||
|
||||
return {
|
||||
viewStacks
|
||||
};
|
||||
|
||||
}, () => {
|
||||
this.setActiveView(this.state.location || this.props.location, this.state.action!);
|
||||
});
|
||||
const viewStacks = Object.assign(new ViewStacks(), this.state.viewStacks);
|
||||
const { view } = viewStacks.findViewInfoById(viewId);
|
||||
if (view) {
|
||||
view.isIonRoute = true;
|
||||
this.ionPageElements[view.id] = page;
|
||||
this.setActiveView(this.state.location || this.props.location, this.state.action!, viewStacks);
|
||||
}
|
||||
}
|
||||
|
||||
syncRoute(_id: string, routerOutlet: any) {
|
||||
@ -359,43 +347,62 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
}
|
||||
|
||||
private async commitView(enteringEl: HTMLElement, leavingEl: HTMLElement, ionRouterOutlet: HTMLIonRouterOutletElement, direction?: NavDirection, showGoBack?: boolean, leavingViewHtml?: string) {
|
||||
if (!this.firstRender) {
|
||||
|
||||
if ((enteringEl === leavingEl) && direction && leavingViewHtml) {
|
||||
// If a page is transitioning to another version of itself
|
||||
// we clone it so we can have an animation to show
|
||||
const newLeavingElement = clonePageElement(leavingViewHtml);
|
||||
ionRouterOutlet.appendChild(newLeavingElement);
|
||||
await ionRouterOutlet.commit(enteringEl, newLeavingElement, {
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
direction,
|
||||
showGoBack,
|
||||
progressAnimation: false
|
||||
});
|
||||
ionRouterOutlet.removeChild(newLeavingElement);
|
||||
if (!('componentOnReady' in ionRouterOutlet)) {
|
||||
await waitUntilRouterOutletReady(ionRouterOutlet);
|
||||
}
|
||||
|
||||
if ((enteringEl === leavingEl) && direction && leavingViewHtml) {
|
||||
// If a page is transitioning to another version of itself
|
||||
// we clone it so we can have an animation to show
|
||||
const newLeavingElement = clonePageElement(leavingViewHtml);
|
||||
ionRouterOutlet.appendChild(newLeavingElement);
|
||||
await ionRouterOutlet.commit(enteringEl, newLeavingElement, {
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
direction,
|
||||
showGoBack,
|
||||
progressAnimation: false
|
||||
});
|
||||
ionRouterOutlet.removeChild(newLeavingElement);
|
||||
} else {
|
||||
await ionRouterOutlet.commit(enteringEl, leavingEl, {
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
direction,
|
||||
showGoBack,
|
||||
progressAnimation: false
|
||||
});
|
||||
}
|
||||
|
||||
if (leavingEl && (enteringEl !== leavingEl)) {
|
||||
/** add hidden attributes */
|
||||
leavingEl.classList.add('ion-page-hidden');
|
||||
leavingEl.setAttribute('aria-hidden', 'true');
|
||||
}
|
||||
} else {
|
||||
await ionRouterOutlet.commit(enteringEl, leavingEl, {
|
||||
deepWait: true,
|
||||
duration: direction === undefined ? 0 : undefined,
|
||||
direction,
|
||||
showGoBack,
|
||||
progressAnimation: false
|
||||
});
|
||||
}
|
||||
|
||||
if (leavingEl && (enteringEl !== leavingEl)) {
|
||||
/** add hidden attributes */
|
||||
leavingEl.classList.add('ion-page-hidden');
|
||||
leavingEl.setAttribute('aria-hidden', 'true');
|
||||
enteringEl.classList.remove('ion-page-invisible');
|
||||
enteringEl.style.zIndex = '101';
|
||||
this.firstRender = false;
|
||||
}
|
||||
}
|
||||
|
||||
handleNavigate(type: 'push' | 'replace', path: string, direction?: RouterDirection) {
|
||||
this.currentDirection = direction;
|
||||
if (type === 'push') {
|
||||
this.props.history.push(path);
|
||||
} else {
|
||||
this.props.history.replace(path);
|
||||
handleNavigate(ionRouteAction: IonRouteAction, path: string, direction?: RouterDirection) {
|
||||
this.currentIonRouteAction = ionRouteAction;
|
||||
switch (ionRouteAction) {
|
||||
case 'push':
|
||||
this.currentRouteDirection = direction;
|
||||
this.props.history.push(path);
|
||||
break;
|
||||
case 'pop':
|
||||
this.currentRouteDirection = direction || 'back';
|
||||
this.props.history.replace(path);
|
||||
break;
|
||||
case 'replace':
|
||||
this.currentRouteDirection = 'none';
|
||||
this.props.history.replace(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@ -405,26 +412,26 @@ class RouteManager extends React.Component<RouteComponentProps, RouteManagerStat
|
||||
if (leavingView.id === leavingView.prevId) {
|
||||
const previousLocation = this.locationHistory.previous();
|
||||
if (previousLocation) {
|
||||
this.handleNavigate('replace', previousLocation.pathname + previousLocation.search, 'back');
|
||||
this.handleNavigate('pop', previousLocation.pathname + previousLocation.search);
|
||||
} else {
|
||||
defaultHref && this.handleNavigate('replace', defaultHref, 'back');
|
||||
defaultHref && this.handleNavigate('pop', defaultHref);
|
||||
}
|
||||
} else {
|
||||
const { view: enteringView } = this.state.viewStacks.findViewInfoById(leavingView.prevId);
|
||||
if (enteringView) {
|
||||
const lastLocation = this.locationHistory.findLastLocationByUrl(enteringView.routeData.match!.url);
|
||||
if (lastLocation) {
|
||||
this.handleNavigate('replace', lastLocation.pathname + lastLocation.search, 'back');
|
||||
this.handleNavigate('pop', lastLocation.pathname + lastLocation.search);
|
||||
} else {
|
||||
this.handleNavigate('replace', enteringView.routeData.match!.url, 'back');
|
||||
this.handleNavigate('pop', enteringView.routeData.match!.url);
|
||||
}
|
||||
} else {
|
||||
const currentLocation = this.locationHistory.previous();
|
||||
if (currentLocation) {
|
||||
this.handleNavigate('replace', currentLocation.pathname + currentLocation.search, 'back');
|
||||
this.handleNavigate('pop', currentLocation.pathname + currentLocation.search);
|
||||
} else {
|
||||
if (defaultHref) {
|
||||
this.handleNavigate('replace', defaultHref, 'back');
|
||||
this.handleNavigate('pop', defaultHref);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -464,5 +471,15 @@ function clonePageElement(leavingViewHtml: string) {
|
||||
return newEl.firstChild as HTMLElement;
|
||||
}
|
||||
|
||||
async function waitUntilRouterOutletReady(ionRouterOutlet: HTMLIonRouterElement) {
|
||||
if ('componentOnReady' in ionRouterOutlet) {
|
||||
return;
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
waitUntilRouterOutletReady(ionRouterOutlet);
|
||||
}, 0);
|
||||
}
|
||||
}
|
||||
|
||||
export const RouteManagerWithRouter = withRouter(RouteManager);
|
||||
RouteManagerWithRouter.displayName = 'RouteManager';
|
||||
|
Reference in New Issue
Block a user