阅读 255

React 源码解析之 lazy

背景

官网已经写的比较详细了,如果你项目中使用 webpack 或 browserify 进行打包,随着工程项目的增长和大量三方库的引入,会使你打包后的文件逐渐变大,用户加载文件时,会花大量时间去加载他们并不关心的内容,而此时,实现异步加载模块(懒加载) React.lazy 的概念就应运而生。lazy 函数返回的是 Promise 对象,同时为了效果演示需要搭配 React.Suspense。而这一功能内部是如何实现的呢?

注:官方提示 React.lazy 并不适合 SSR

示例的使用

1.入口文件

// 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.组件文件:Logo

// 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 的封装:

// 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 示例代码

源码实现

// 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 文件,进行按需加载。

属性说明

  • $$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)

更新流程

mountLazyComponent

// 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))

readLazyComponentType

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 方法,resolvereject 分别设置 _status_result,默认 _status 变成 Pendding,所以下一次进来会 throw thenable,这就进入了 Suspense 的阶段了。

resolveLazyComponentTag

调用 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;
}
复制代码

resolveDefaultProps

初始化默认的 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;
}
复制代码

updateClassComponent

上面一波操作,懒加载前期工作就完成了,紧接着就是根据 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中进行验证。

关注下面的标签,发现更多相似文章
评论