官网已经写的比较详细了,若是你项目中使用 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
中进行验证。