官網已經寫的比較詳細了,若是你項目中使用 webpack 或 browserify 進行打包,隨着工程項目的增加和大量三方庫的引入,會使你打包後的文件逐漸變大,用戶加載文件時,會花大量時間去加載他們並不關心的內容,而此時,實現異步加載模塊(懶加載) React.lazy
的概念就應運而生。lazy
函數返回的是 Promise 對象,同時爲了效果演示須要搭配 React.Suspense
。而這一功能內部是如何實現的呢?javascript
注:官方提示
React.lazy
並不適合 SSRcss
1.入口文件java
// APP.js
import React from 'react';
import './App.css';
import { connectLazy } from './utils/index.js';
// Logo
const LazyLogo = React.lazy(() => import('./lazy-logo'));
LazyLogo.displayName = 'logo';
const LazyLogoComponent = connectLazy({
loading: <div>Logo 加載中...</div>
})(LazyLogo);
export default () => {
return (
<div className="App"> <LazyLogoComponent /> </div>
);
}
複製代碼
2.組件文件:Logoreact
// lazy-logo/index.js
import React from 'react';
import logo from './logo.svg';
import './index.css';
const LazyLoad = () => {
return (
<img src={logo} className="App-logo" alt="logo" /> ) } export default LazyLoad; 複製代碼
3.方法庫:使用高階組件進行 Suspense 的封裝:webpack
// utils/hoc.js
import React from 'react';
const getDisplayName = WrappedComponent => {
console.log('WrappedComponent', WrappedComponent);
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}
export const connectLazy = params => {
params = {
loading: <div>加載中...</div>,
...params
};
return WrappedComponent => {
return class extends React.Component {
render() {
const displayName = `HOC(${getDisplayName(WrappedComponent)})`;
console.log(displayName);
return (
<React.Suspense fallback={params.loading}>
<WrappedComponent {...this.props} />
</React.Suspense>
)
}
}
}
}
複製代碼
Github 示例代碼git
// react/src/ReactLazy.js
export function lazy<T, R>(ctor: () => Thenable<T, R>): LazyComponent<T> {
let lazyType = {
$$typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// React uses these fields to store the result.
_status: -1,
_result: null,
};
// ...
return lazyType;
}
複製代碼
使用 lazy
後會打包成多個 chunk
文件,進行按需加載。github
屬性說明web
$$typeof
對象類型,可查看文件 shared/ReactSymbols.js
,包括 Symbol.for(react.lazy)
、Symbol.for(react.memo)
、Symbol.for(react.element)
、Symbol.for(react.fragment)
、Symbol.for(react.context)
等等;_ctor
懶加載異步函數,返回 Promise 對象,即 async () => import('./Component')
, 標記傳入的生成 thenable
對象的方法;_result
用來標記加載完成模塊的內容;_status
當前狀態,初始值(-1),其餘狀態 Pending(0) Resolved(1) Rejected(2)
;// react-reconciler/src/ReactFiberBeginWork.js
switch (workInProgress.tag) {
// ...
case LazyComponent: {
const elementType = workInProgress.elementType;
return mountLazyComponent(
current,
workInProgress,
elementType,
updateExpirationTime,
renderExpirationTime,
);
}
}
複製代碼
在 beginWork
函數中,能夠看到對於 LazyComponent
模塊加載方式是調用函數 mountLazyComponent
數組
// react-reconciler/src/ReactFiberBeginWork.js
function mountLazyComponent( _current, workInProgress, elementType, updateExpirationTime, renderExpirationTime, ) {
if (_current !== null) {
_current.alternate = null;
workInProgress.alternate = null;
workInProgress.effectTag |= Placement;
}
// 1.解析 LazyComponent
let Component = readLazyComponentType(elementType);
// 將解析的 LazyComponent 賦值給工做進程類型
workInProgress.type = Component;
// 2.ReactFiber 提供的根據特性決定(判斷)組件類型的方法,ClassComponent、FunctionComponent、ForwardRef、MemoComponent 等內置類型
const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component));
startWorkTimer(workInProgress);
// 3.初始化props
const resolvedProps = resolveDefaultProps(Component, props);
// 4.根據返回的組件類型執行更新
let child;
switch (resolvedTag) {
case FunctionComponent: {
child = updateFunctionComponent(
null,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
break;
}
case ClassComponent: {
child = updateClassComponent(
null,
workInProgress,
Component,
resolvedProps,
renderExpirationTime,
);
break;
}
case ForwardRef: {
child = ...;
break;
}
case MemoComponent: {
child = ...;
break;
}
default: {
// warning
}
}
return child;
}
複製代碼
1.若是 _current
存在值會刪除其的引用,爲何呢? lazy
組件只有在第一次渲染的時纔會調用該方法,等組件加載完成了,就會直接更新組件的流程 const resolvedTag = (workInProgress.tag = resolveLazyComponentTag(Component))
;app
ReactFiber 提供的根據特性決定(判斷)組件類型的方法,ClassComponent、FunctionComponent、ForwardRef、MemoComponent
等內置類型;
// shared/ReactLazyComponent.js
export const Pending = 0;
export const Resolved = 1;
export const Rejected = 2;
// react-reconciler/src/ReactFiberLazyComponent.js
import { Resolved, Rejected, Pending } from 'shared/ReactLazyComponent';
export function readLazyComponentType<T>(lazyComponent: LazyComponent<T>): T {
const status = lazyComponent._status;
const result = lazyComponent._result;
switch (status) {
case Resolved: {
const Component: T = result;
return Component;
}
case Rejected: {
const error: mixed = result;
throw error;
}
case Pending: {
const thenable: Thenable<T, mixed> = result;
throw thenable;
}
default: {
lazyComponent._status = Pending;
const ctor = lazyComponent._ctor;
const thenable = ctor();
thenable.then(
moduleObject => {
if (lazyComponent._status === Pending) {
const defaultExport = moduleObject.default;
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
},
error => {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
},
);
// Handle synchronous thenables.
switch (lazyComponent._status) {
case Resolved:
return lazyComponent._result;
case Rejected:
throw lazyComponent._result;
}
lazyComponent._result = thenable;
throw thenable;
}
}
}
複製代碼
1.readLazyComponentType
函數根據參數 elementType
返回懶加載的組件,thenable
執行 ctor()
異步函數,拿到 import
的組件函數即 f LazyLogo()
(上面示例),拿到後暫存於workInProgress.type
;
2.剛開始 _status
初始值 -1
,因此不符合前三個 case
,而後就進入 default
。這裏面調用了 lazyComponent._ctor()
建立了 thenable
對象,調用 then
方法,resolve
和 reject
分別設置 _status
和 _result
,默認 _status
變成 Pendding
,因此下一次進來會 throw thenable
,這就進入了 Suspense
的階段了。
調用 shouldConstruct
判斷 Component
的原型上是否有 isReactComponent
,若是存在則爲類組件,不然爲函數組件。
// react-reconciler/src/ReactFiber.js
export function resolveLazyComponentTag(Component: Function): WorkTag {
if (typeof Component === 'function') {
return shouldConstruct(Component) ? ClassComponent : FunctionComponent;
} else if (Component !== undefined && Component !== null) {
const $$typeof = Component.$$typeof;
if ($$typeof === REACT_FORWARD_REF_TYPE) {
return ForwardRef;
}
if ($$typeof === REACT_MEMO_TYPE) {
return MemoComponent;
}
}
return IndeterminateComponent;
}
複製代碼
初始化默認的 props
export function resolveDefaultProps(Component: any, baseProps: Object): Object {
if (Component && Component.defaultProps) {
// Resolve default props. Taken from ReactElement
const props = Object.assign({}, baseProps);
const defaultProps = Component.defaultProps;
for (let propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
return props;
}
return baseProps;
}
複製代碼
上面一波操做,懶加載前期工做就完成了,緊接着就是根據 resolvedTag
進行組件刷新。好比類組件 ClassComponent
,其更新方法 updateClassComponent
,下面咱們逐段分析該方法
function updateClassComponent( current: Fiber | null, workInProgress: Fiber, Component: any, nextProps, renderExpirationTime: ExpirationTime, ) {
// `propTypes` 的校驗
if (__DEV__) {
if (workInProgress.type !== workInProgress.elementType) {
// Lazy component props can't be validated in createElement
// because they're only guaranteed to be resolved here.
const innerPropTypes = Component.propTypes;
if (innerPropTypes) {
checkPropTypes(
innerPropTypes,
nextProps, // Resolved props
'prop',
getComponentName(Component),
getCurrentFiberStackInDev,
);
}
}
}
// Push context providers early to prevent context stack mismatches.
// During mounting we don't know the child context yet as the instance doesn't exist.
// We will invalidate the child context in finishClassComponent() right after rendering.
let hasContext;
if (isLegacyContextProvider(Component)) {
hasContext = true;
pushLegacyContextProvider(workInProgress);
} else {
hasContext = false;
}
prepareToReadContext(workInProgress, renderExpirationTime);
const instance = workInProgress.stateNode;
let shouldUpdate;
if (instance === null) {
if (current !== null) {
// An class component without an instance only mounts if it suspended
// inside a non- concurrent tree, in an inconsistent state. We want to
// tree it like a new mount, even though an empty version of it already
// committed. Disconnect the alternate pointers.
current.alternate = null;
workInProgress.alternate = null;
// Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}
// In the initial pass we might need to construct the instance.
constructClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
mountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
shouldUpdate = true;
} else if (current === null) {
// In a resume, we'll already have an instance we can reuse.
shouldUpdate = resumeMountClassInstance(
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
} else {
shouldUpdate = updateClassInstance(
current,
workInProgress,
Component,
nextProps,
renderExpirationTime,
);
}
const nextUnitOfWork = finishClassComponent(
current,
workInProgress,
Component,
shouldUpdate,
hasContext,
renderExpirationTime,
);
if (__DEV__) {
let inst = workInProgress.stateNode;
if (inst.props !== nextProps) {
warning(
didWarnAboutReassigningProps,
'It looks like %s is reassigning its own `this.props` while rendering. ' +
'This is not supported and can lead to confusing bugs.',
getComponentName(workInProgress.type) || 'a component',
);
didWarnAboutReassigningProps = true;
}
}
return nextUnitOfWork;
}
複製代碼
1.首先作了 propTypes
的校驗(若是在組件中設置了的話),注意沒法在 CreateElement
中驗證 lazy
組件的屬性,只能在updateClassComponent
中進行驗證。