ReactNative-HMR原理探索
前言
在开始本文前,先简单说下我们在开发RN项目中,本地的node服务究竟扮演的是什么样的角色。在我们的RN APP中有配置本地开发的地方,只要我们输入我们本地的IP和端口号8081就可以开始调试本地代码,其实质是APP发起了一个请求bundle文件的HTTP请求,而我们的node server在接到request后,开始对本地项目文件进行babel,pack,最后返回一个bundle.js。而本地的node服务扮演的角色还不止如此,比如启动基础服务dev tool,HMR等
什么是HMR
HMR(Hot Module Replacement)模块热替换,可以类比成Webpack的Hot Reload。可以让你在代码变动后不用reload app,代码直接生效,且当前路由栈不会发生改变
名词说明
- 逆向依赖:如上图 对于D模块来说,A,B文件就是D的逆向依赖
- 浅层依赖:如上图 对于index.js来说,A,B模块就是index.js的浅层依赖(直属依赖),C,D,E跟index没有直接依赖关系
实现原理
先贴上个人整理的的一个HMR热更新的过程
我们来逐步按流程对应相应的源码分析
启动Packerage&HMR server
run packager server
# react-native/local-cli/server/runServer.js
const serverInstance = http.createServer(app).listen(
args.port,
args.host,
() => {
attachHMRServer({
httpServer: serverInstance,
path: '/hot',
packagerServer,
});
wsProxy = webSocketProxy.attachToServer(serverInstance, '/debugger-proxy');
ms = messageSocket.attachToServer(serverInstance, '/message');
webSocketProxy.attachToServer(serverInstance, '/devtools');
readyCallback();
}
);
本地启动在8081启动HTTP服务的同时,也初始化了本地HMR的服务,这里在初始化的时候注入了packagerServer,为的是能订阅packagerServer提供的watchman回调,同时也为了能拿到packagerServer提供的getDependencies方法,这样能在HMR内部拿到文件的依赖关系(相互require的关系)
#react-native/local-cli/server/util/attachHMRServer.js
// 略微简化下代码
function attachHMRServer({httpServer, path, packagerServer}) {
...
const WebSocketServer = require('ws').Server;
const wss = new WebSocketServer({
server: httpServer,
path: path,
});
wss.on('connection', ws => {
...
getDependencies(params.platform, params.bundleEntry)
.then((arg) => {
client = {
...
};
packagerServer.setHMRFileChangeListener((filename, stat) => {
...
client.ws.send(JSON.stringify({type: 'update-start'}));
stat.then(() => {
return packagerServer.getShallowDependencies({
entryFile: filename,
platform: client.platform,
dev: true,
hot: true,
})
.then(deps => {
if (!client) {
return [];
}
const oldDependencies = client.shallowDependencies[filename];
// 分析当前文件的require关系是否与之前一致,如果require关系有变动,需要重新对文件的dependence进行分析
if (arrayEquals(deps, oldDependencies)) {
return packagerServer.getDependencies({
platform: client.platform,
dev: true,
hot: true,
entryFile: filename,
recursive: true,
}).then(response => {
const module = packagerServer.getModuleForPath(filename);
return response.copy({dependencies: [module]});
});
}
return getDependencies(client.platform, client.bundleEntry)
.then(({
dependenciesCache: depsCache,
dependenciesModulesCache: depsModulesCache,
shallowDependencies: shallowDeps,
inverseDependenciesCache: inverseDepsCache,
resolutionResponse,
}) => {
if (!client) {
return {};
}
return packagerServer.buildBundleForHMR({
entryFile: client.bundleEntry,
platform: client.platform,
resolutionResponse,
}, packagerHost, httpServerAddress.port);
})
.then(bundle => {
if (!client || !bundle || bundle.isEmpty()) {
return;
}
return JSON.stringify({
type: 'update',
body: {
modules: bundle.getModulesIdsAndCode(),
inverseDependencies: client.inverseDependenciesCache,
sourceURLs: bundle.getSourceURLs(),
sourceMappingURLs: bundle.getSourceMappingURLs(),
},
});
})
.then(update => {
client.ws.send(update);
});
}
).then(() => {
client.ws.send(JSON.stringify({type: 'update-done'}));
});
});
client.ws.on('close', () => disconnect());
})
}
RN最舒服的地方就是命名规范,基本看到函数名就能知道他的职能,我们来看上面这段代码,attachHMRServer这个总共做了以下几件事:
- 起一个socket服务,这样在监听到文件变动的时候能够将处理完的code通过socket层扔给App端
- 订阅packager server提供fileChange方法
- 拿到packager server提供的getDependence方法,对变动文件进行简单的依赖分析。如果说发现变动文件A之前require了B,C文件,但是这次只require了B文件,oldDependencies!==currentDep(这里HMRServer为了优化性能,对浅层依赖关系,逆向依赖关系,依赖缓存时间都做了cache),那么HMR server会让Packager Server重新梳理一遍项目文件的依赖关系(因为可能存在增删文件的可能),同时对它局部维护的一些cache Map做更新
HMRClient
注册
我们已经看到了socket的发送方,那么必定存在一个接收方,也就是这里要讲的HMRClient,首先先来看这边注册函数
#react-native/Libraries/BatchedBridge/BatchedBridge.js
const MessageQueue = require('MessageQueue');
const BatchedBridge = new MessageQueue(
() => global.__fbBatchedBridgeConfig,
serializeNativeParams
);
const Systrace = require('Systrace');
const JSTimersExecution = require('JSTimersExecution');
BatchedBridge.registerCallableModule('Systrace', Systrace);
BatchedBridge.registerCallableModule('JSTimersExecution', JSTimersExecution);
BatchedBridge.registerCallableModule('HeapCapture', require('HeapCapture'));
if (__DEV__) {
BatchedBridge.registerCallableModule('HMRClient', require('HMRClient'));
}
这边就是HMRClient注册阶段,贴这段代码其实是因为发现RN里的JS->Native,Native->JS通信是通过MQ(MessageQueue)实现的,而追溯到最里层发现竟然是一套setTimeout,setImmediate的异步队列...扯远了,有空的话,可以专门分享一下。
HMRClient
#react-native/Libraries/Utilities/HMRClient.js
activeWS.onmessage = ({ data }) => {
...
modules.forEach(({ id, code }, i) => {
...
const injectFunction = typeof global.nativeInjectHMRUpdate === 'function'
? global.nativeInjectHMRUpdate
: eval;
code = [
'__accept(',
`${id},`,
'function(global,require,module,exports){',
`${code}`,
'\n},',
`${JSON.stringify(inverseDependencies)}`,
');',
].join('');
injectFunction(code, sourceURLs[i]);
});
}
};
HMRClient做的事就很简单了,接到socket传入的String,直接eval运行,这边的code形如下图
我们可以看到这边是一个__accept函数在接受这个变更后的HMR bundle
真正的热更新过程
#react-native/packager/react-packager/src/Resolver/polyfills/require.js
const accept = function(id, factory, inverseDependencies) {
//在当前模块映射表里查找,如果找的到将其Code进行替换,并执行,若没有,重新进行声明
const mod = modules[id];
if (!mod) {
//重新申明
define(id, factory);
return true; // new modules don't need to be accepted
}
const {hot} = mod;
if (!hot) {
console.warn(
'Cannot accept module because Hot Module Replacement ' +
'API was not installed.'
);
return false;
}
// replace and initialize factory
if (factory) {
mod.factory = factory;
}
mod.hasError = false;
mod.isInitialized = false;
//真正进行热替换的地方
require(id);
//当前模块热更新后需要执行的回调,一般用来解决循环引用
if (hot.acceptCallback) {
hot.acceptCallback();
return true;
} else {
// need to have inverseDependencies to bubble up accept
if (!inverseDependencies) {
throw new Error('Undefined `inverseDependencies`');
}
//将当前moduleId的逆向依赖传入,热更新他的逆向依赖,递归执行
return acceptAll(inverseDependencies[id], inverseDependencies);
}
};
global.__accept = accept;
这边的代码就不进行删减了,accept函数接受三个参数,moduleId,factory,inverseDependencies。
- moduleId:需要热更新的ID,对于每个模块,都会被赋予一个模块ID,RN 30之前的版本使用的是filePath作为key,而后使用的是一个递增的整型
- factory:babel后实际的需要热替换的code
- inverseDependencies:当前所有的逆向依赖Map
简单来说accept做的事情就是判断变动当前模块是新加的需要define,还是说直接更新内存里已存在的module,同时沿着他的逆向依赖树,全部load一遍,一直到最顶级的AppResigterElement,这样热替换的过程就完成了,形如下图
那么问题就来了,react的View展现对state是强依赖的,重新load一遍,state不会丢失么,实际上在load的过程中,RN把老的ref传入了,所以继承了之前的state
讲到这还略过了最重要的一点,为什么说我这边热替换了内存中module,并执行了一遍,我的App就能拿到这个更新后的代码,我们依旧拿代码来说
#react-native/packager/react-packager/src/Resolver/polyfills/require.js
global.require = require;
global.__d = define;
const modules = Object.create(null);
function define(moduleId, factory) {
if (moduleId in modules) {
// prevent repeated calls to `global.nativeRequire` to overwrite modules
// that are already loaded
return;
}
modules[moduleId] = {
factory,
hasError: false,
isInitialized: false,
exports: undefined,
};
if (__DEV__) {
// HMR
modules[moduleId].hot = createHotReloadingObject();
// DEBUGGABLE MODULES NAMES
// avoid unnecessary parameter in prod
const verboseName = modules[moduleId].verboseName = arguments[2];
verboseNamesToModuleIds[verboseName] = moduleId;
}
}
function require(moduleId) {
const module = __DEV__
? modules[moduleId] || modules[verboseNamesToModuleIds[moduleId]]
: modules[moduleId];
return module && module.isInitialized
? module.exports
: guardedLoadModule(moduleId, module);
}
RN复写了require,这样所有模块其实拿到的是这里
HMR存在的问题
- 由于其原理是逆向load其依赖树,如果说项目的技术方法破坏了其树状依赖结构,那么HMR也没法生效。例如通过global挂载包装了AppResigter这样的方法。
- 由于Ctrl+s会立即触发watchMan的回调,导致可能代码改了一半就走进了HMR的逻辑,在transfrom Code或者require的时候就直接红屏了
- 由于其HMR原理是逆向执行依赖树,如果项目中存在文件循环引用,也会导致栈溢出,可以通过文件增加module.hot.accept这样的方法解决,但是如果项目公用方法存在这样的问题,就只能强行把HMR的逆向加载这块代码注释了。这无疑是阉割了HMR一大部分功能
- 综上,HMR如果仅仅用于切图,可能不会有那么多的问题
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。