react-loadable源码解析

1,094 阅读3分钟

react-loadable组件加上import(),可以轻松实现以组件为中心代码的分割。

主体流程

源码截图: export出来的是Loadable,而Loadable里面调用的是createLoadableComponent函数。 createLoadableComponent函数有两个参数,loadFn即是load函数,options是传入的参数。

在createLoadableComponent中,先判断了options.loading是否存在,不存在则扔出一个错误。

if (!options.loading) {
    throw new Error("react-loadable requires a `loading` component");
}

后定义了一个opts、res变量,声明init函数等,最后返回一个LoadableComponent。LoadableComponent先运行了init函数。

function init() {
    if (!res) {
      res = loadFn(opts.loader);
    }
    return res.promise;
}

init函数中,调用load函数,并传入入参opts.loader(loader即类似() => import("./pages/Data/DataSource"),)。import()函数运行时加载模块。import()返回一个 Promise 对象。import()加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。

function load(loader) {
  // 运行import()
  let promise = loader();

  let state = {
    loading: true, // 是否加载中
    loaded: null, // loaded即已加载成功的模块
    error: null  // 加载错误
  };

  state.promise = promise
    .then(loaded => {
      state.loading = false;
      state.loaded = loaded;
      return loaded;
    })
    .catch(err => {
      state.loading = false;
      state.error = err;
      throw err;
    });

  return state;
}

然后声明state

this.state = {
        error: res.error,
        pastDelay: false,
        timedOut: false,
        loading: res.loading,
        loaded: res.loaded
};

componentWillMount函数里面运行了_loadModule函数。 主要操作:当组件加载成功或失败时,更新state的error、loaded和loading

_loadModule() {
      if (this.context.loadable && Array.isArray(opts.modules)) {
        opts.modules.forEach(moduleName => {
          this.context.loadable.report(moduleName);
        });
      }

      // 判断是否还在加载,已加载完成,则返回
      if (!res.loading) {
        return;
      }

      // 用于更新state
      let setStateWithMountCheck = (newState) => {
        if (!this._mounted) {
          return;
        }

        this.setState(newState);
      }
      
      // delay延时时间,加定时器,当超过delay设置的值,将pastDelay设置为true
      if (typeof opts.delay === 'number') {
        if (opts.delay === 0) {
          this.setState({ pastDelay: true });
        } else {
          this._delay = setTimeout(() => {
            setStateWithMountCheck({ pastDelay: true });
          }, opts.delay);
        }
      }
      // timeout超时时间,加定时器,当超过timeout设置的值,将timeout设置为true
      if (typeof opts.timeout === "number") {
        this._timeout = setTimeout(() => {
          setStateWithMountCheck({ timedOut: true });
        }, opts.timeout);
      }
      
      // 更新error、loaded和loading,并清除定时器
      let update = () => {
        setStateWithMountCheck({
          error: res.error,
          loaded: res.loaded,
          loading: res.loading
        });

        this._clearTimeouts();
      };

      // 当组件加载成功或失败,则更新state
      res.promise
        .then(() => {
          update();
          return null;
        })
        .catch(err => {
          update();
          return null;
        });
    }

componentDidMount函数里设置this._mountedtrue

componentWillUnmount() {
      this._mounted = false;
      this._clearTimeouts();
}

组件卸载前设置_mountedfalse,并取消定时器。 最后是render。

render() {
    if (this.state.loading || this.state.error) {
        return React.createElement(opts.loading, {
          isLoading: this.state.loading,
          pastDelay: this.state.pastDelay,
          timedOut: this.state.timedOut,
          error: this.state.error,
          retry: this.retry
      });
    } else if (this.state.loaded) {
        return opts.render(this.state.loaded, this.props);
    } else {
        return null;
    }
}

若组件还在加载,或加载出错,则显示传入的loading过渡组件。若组件已加载成功,则调用render函数进行渲染。

// 若__esModule为true,则表明是es模块,那么返回obj.default,否则返回obj。
function resolve(obj) {
  return obj && obj.__esModule ? obj.default : obj;
}

// props为原组件的props
function render(loaded, props) {
  return React.createElement(resolve(loaded), props);
}

Loadable.Map

用于并行加载多个模块。Loadable.Map实质是调用了LoadableMap函数。

function LoadableMap(opts) {
  if (typeof opts.render !== "function") {
    throw new Error("LoadableMap requires a `render(loaded, props)` function");
  }

  return createLoadableComponent(loadMap, opts);
}

LoadableMap函数与主流程的运行并无不同,除了所用的loadFn和render不一样。Loadable.Map使用的loadMap函数,并且render函数需要自己传入

function loadMap(obj) {
  let state = {
    loading: false,
    loaded: {},// 存放所有加载成功的模块
    error: null
  };

  let promises = [];

  try {
    // 依次遍历传入的loader
    Object.keys(obj).forEach(key => {
      let result = load(obj[key]);

      if (!result.loading) {
        state.loaded[key] = result.loaded;
        state.error = result.error;
      } else {
        state.loading = true;
      }

      promises.push(result.promise);

      result.promise
        .then(res => {
          state.loaded[key] = res;
        })
        .catch(err => {
          state.error = err;
        });
    });
  } catch (err) {
    state.error = err;
  }

  // 当所有模块都加载成功,或者有一个模块加载失败的时候,设置state.loading的状态。
  // 这个代码里面的res和err看着应该是没值的
  state.promise = Promise.all(promises)
    .then(res => {
      state.loading = false;
      return res;
    })
    .catch(err => {
      state.loading = false;
      throw err;
    });

  return state;
}

Loadable.Capture

Loadable.Capture用于收集已呈现的所有模块。源码很简单,就不显示了。 主要是定义了一个loadableloadable是一个contextloadablereportprops.report赋值。这个report哪里会用到呢?在前面的主体流程的_loadModule里面有这样一段代码:

if (this.context.loadable && Array.isArray(opts.modules)) {
        opts.modules.forEach(moduleName => {
          this.context.loadable.report(moduleName);
        });
}

当loadable和opts.modules均存在时,将opts.modules里面的数据取出来传入report函数。

用法

let modules = [];
......
<Loadable.Capture report={moduleName => modules.push(moduleName)}>
   <App/>
</Loadable.Capture>

Loadable.preloadAll和Loadable.preloadReady

Loadable.preloadAll用于预加载模块,ALL_INITIALIZERS里面放的是所有模块的init函数。

Loadable.preloadAll = () => {
  return new Promise((resolve, reject) => {
    flushInitializers(ALL_INITIALIZERS).then(resolve, reject);
  });
};

Loadable.preloadReady用于检查在浏览器上模块是否已经加载完成了。READY_INITIALIZERS里面放的是init函数运行返回的promise

Loadable.preloadReady = () => {
  return new Promise((resolve, reject) => {
    // We always will resolve, errors should be handled within loading UIs.
    flushInitializers(READY_INITIALIZERS).then(resolve, resolve);
  });
};

参考自:

react-loadable