H5W3
当前位置:H5W3 > 其他技术问题 > 正文

react-hot-loader原理

什么是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翻译

源代码

本文地址:H5W3 » react-hot-loader原理

评论 0

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址