React 源碼解析之 lazy

背景

官網已經寫的比較詳細了,若是你項目中使用 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)

更新流程

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

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中進行驗證。

相關文章
相關標籤/搜索