react-hot-loader原理

4,803 阅读4分钟

什么是react-hot-loader?

react-hot-loader是一个结合webpack HotModuleReplacementPlugin插件实现的react热更新库,可以实现保留react状态的动态热更新。

模块热更新

在讲解react-hot-loader前,必须先对HotReplacementPlugin(之后简称HRP)有初步 的了解。
webpack会根据入口文件构建一颗依赖树,咱们可以把它想象成和dom树差不多,针对这个依赖树,HRP会给每个模块注入一个module对象,表示该模块的一些信息,对象结构如下:

module对象

{
    id: string, //路径
    loaded: boolean,
    exports: boolean,
    webpackPolyfill: number,
    exports: 导出,
    __proto__: {
        parents: string[], // 父级引用
        children: string[], // 依赖
        hot: 下面会介绍
        exports: 导出
        i: 路径,
        l: 不清楚,
    }
}

module.hot是最主要的部分,我们可以通过调用hot方法,自定义一些热更新的实现,比如阻止热更新,又或者在热更新清除一些全局变量,可以在模块热替换了解更详细的信息,下面只介绍我们用到的两个方法:

module.hot方法(只介绍部分)

在这只介绍acceptaddStatusHandler两个方法

  • accept: (dependencies: string | string[], cb: () => void) => void

接受指定的依赖模块更新,并进行处理,依赖树和dom树类似,每次热更新时,从更新的模块处会像冒泡一样往上传递自己更新的消息,当碰到有accept处理方法时就会停止继续冒泡,而执行accept的处理,如果冒泡到顶还是没有accept处理,就是重载一遍,所有的状态都会消失

module.hot.accept('./App.js', () => {
  ...dosomething    
})
  • addStatusHandler: (status => void) => void

注册一个函数来监听 status的变化,可以根据状态来控制我们的热更新

module.hot.addStatusHandler(status => {
  // 响应当前状态……
  // idle: 该进程正在等待调用 check(见下文)
  // check: 该进程正在检查以更新
  // prepare: 该进程正在准备更新(例如,下载已更新的模块)
  // ready: 此更新已准备并可用
  // dispose: 该进程正在调用将被替换模块的 dispose 处理函数
  // apply: 该进程正在调用 accept 处理函数,并重新执行自我接受(self-accepted)的模块
  // abort: 更新已中止,但系统仍处于之前的状态
  // fail: 更新已抛出异常,系统状态已被破坏
})

原理及实现

  • root.js

root.js中是获取到引用hot模块的父模块,也就是咱们处理热更新的react组件,并对该组件进行HOC包裹

var hot = require("./hot").hot;
let parent;
if (module.hot) {
  // 获取require所有的模块缓存,与nodejs中类似
  const cache = require.cache;
  if (!module.parents || module.parents.length === 0) {
    throw new Error("no parents!!");
  }
  // 获取咱们的组件模块
  parent = cache[module.parents[0]];
  // 删除root.js模块,以便每次调用都会进行重新加载
  delete cache[module.id];
}
export default hot(parent);
  • hot.js

HOC实现,通过HOC包裹我们的组件,并在componentDidMount中保存this实例,以便更新时不重载,直接forceUpdate

const requireIndirect =
  typeof __webpack_require__ !== "undefined" ? __webpack_require__ : require;
reactHotLoader.patch(React, ReactDOM); // 对React、ReactDOM做一些改动
...
// 更新队列
const runInRenderQueue = createQueue((cb) => {
  if (ReactDOM.unstable_batchedUpdates) {
    ReactDOM.unstable_batchedUpdates(cb);
  } else {
    cb();
  }
});
// 热更新的处理
const makeHotExport = (sourceModule, moduleId) => {
  const updateInstances = () => {
    // 获取该模块的实例对象
    const module = hotModule(moduleId);
    const deepUpdate = () => {
      // forceUpdate每个实例
      runInRenderQueue(() => {
        module.instances.forEach((inst) => inst.forceUpdate());
      });
    };
    deepUpdate();
  };
  if (sourceModule.hot) {
    // 传入的参数不正确,但是可以阻塞热更新冒泡传递(只针对webpack)
    sourceModule.hot.accept(updateInstances);
    if (sourceModule.hot.addStatusHandler) {
      if (sourceModule.hot.status() === "idle") {
        sourceModule.hot.addStatusHandler((status) => {
          if (status === "apply") {
            // 当接受到热更新时开始更新实例
            updateInstances();
          }
        });
      }
    }
  }
};
// 生成HOC
export const hot = (sourceModule) => {
  const moduleId = sourceModule.id || sourceModule.i;
  // 保存实例
  const module = hotModule(moduleId);
  let firstHotRegistered = false;
  makeHotExport(sourceModule, moduleId);
  return (WrappedComponent) => {
    const Hoc = createHoc(
      WrappedComponent,
      class Hoc extends React.Component {
        componentDidMount() {
          // 保存我们的react实例
          module.instances.push(this);
        }
        componentWillUnmount() {
          module.instances = module.instances.filter((a) => a !== this);
        }
        render() {
          return <WrappedComponent {...this.props} />;
        }
      }
    );
    if (!firstHotRegistered) {
      firstHotRegistered = true;
      // 对模块进行保存,下面会介绍
      reactHotLoader.register(
        WrappedComponent,
        WrappedComponent.displayName || WrappedComponent.name,
        moduleId
      );
    }
    return Hoc;
  };
};

  • reactHotLoader.js

通过上面的处理,基本上已经有了热更新的雏形了,但是还是有问题

1.那就是因为闭包的原因,我们的实例其实上还是指向原来的方法,forceUpdate还是不会应用上新的代码
2. 即使我们想办法让它指向新的代码,但是react tree diff时不是同一个Component,react还是会重新render

这里的解决方式是,热更新时保存新的代码,在第一次加载的时候创建一个Proxy,每次forceUpdate,让Proxy去找到最新的代码,然后执行,这样就解决了上面两个问题

const proxies = new Map();
const types = new Map();

const resolveType = (type) => {
  if (type["PROXY_KEY"]) {
    // 获取proxy
    return proxies.get(type["PROXY_KEY"]);
  }
  return type;
};

const reactHotLoader = {
  register(type, name, id) {
    if (!type["PROXY_KEY"]) {
      const key = `${id}-${name}`;
      // 给组件加上一个标志
      type["PROXY_KEY"] = key;
      // 通过key保留最新的组件代码
      types.set(key, type);
      if (!proxies.get(key)) {
        // 创建该组件的proxy,
        proxies.set(
          key,
          new Proxy(type, {
            apply: function (target, thisBinding, args) {
              const id = target["PROXY_KEY"];
              // 获取最新的代码
              const latestTarget = types.get(id);
              return latestTarget(...args);
            },
          })
        );
      }
    }
  },
  // 代理React.createElement方法,以便我们找到新的模块代码
  patch(React, ReactDOM) {
    if (!React.createElement.isPatchd) {
      const origin = React.createElement;
      React.createElement = (type, ...args) =>
        origin(resolveType(type), ...args);
      React.createElement.isPatchd = true;
    }
  },
};
export default reactHotLoader;

参考

webpack HotReplacementPlugin

github react-hot-loader

Hot Reloading in React(Dan的文章)

Hot Reloading in React翻译

源代码