Hot Reloading
React Native(下文簡稱RN)致力于提供最好的開發(fā)體驗,亮點之一就是極大縮短了修改文件到頁面刷新的時間。在文件修改之后,頁面可以直接獲取到這一變動并更新邏輯,這一技術(shù)被稱為Hot Reloading。
RN這一技術(shù)的實現(xiàn)依賴以下三種特性:
- 使用JavaScript作為開發(fā)語言,規(guī)避了長時間編譯的問題。
- 使用Packager的工具將es6/flow/jsx文件轉(zhuǎn)為Virtual Dom可以理解的普遍JS文件。Packager以服務(wù)器的形式將中間狀態(tài)保存在內(nèi)存中,這一處理使得對快速更新變動提供了強效支持,并且使用多內(nèi)核處理。
- 使用一個稱為Live Reload的特性在項目保存后刷新。
通過以上特性,開發(fā)瓶頸由編譯時間變?yōu)槿绾伪3諥PP的state。
Hot Reloading實現(xiàn)
Hot Reloading的設(shè)計是在文件發(fā)生變化時,實時去刷新應(yīng)用的狀態(tài)。Hot Reloading是在Hot Module Replacement特性的基礎(chǔ)上實現(xiàn)的,簡稱HMR(熱組件替換)。這個特性由Webpack首次提出,現(xiàn)在應(yīng)用在RN Packager。
HMR包含了JS組件改變后的最新代碼,在HMR Runtime收到時,會將舊的代碼替換為新的:
下面通過代碼示例進行講解:
// log.js
function log(message) {
const time = require('./time');
console.log(`[${time()}] ${message}`);
}
module.exports = log;
// time.js
function time() {
return new Date().getTime();
}
module.exports = time;
可以看到在log.js內(nèi)依賴了time。
在應(yīng)用打包時,RN會在組件系統(tǒng)內(nèi)通過__d
方法注冊每個組件。對于以上示例來說,在眾多的__d
定義中,log
的定義如下:
__d('log', function() {
... // module's code
});
RN將每個組件的代碼通過匿名函數(shù)的方式封裝,類似于我們知道的工廠方法。組件系統(tǒng)的運行時會追蹤每個組件的工廠方法。
組件在被獲取后會進行緩存,緩存與否的處理方式是不一樣的。
未緩存:在time的代碼發(fā)生改變時,Packager會將time的最新代碼發(fā)送給runtime。
存在緩存:先清除緩存 -> 替換time -> 在log使用time時重新建立緩存
HMR API
RN中通過引入hot
對象的方式擴展了組件系統(tǒng)。hot
對象提供了accept
方法用于接受組件被修改后的回調(diào)方法。
下面這個示例,是說明了HMR中accept的用法,并沒有使用React Hot Loader,沒有React Transform等其他RN Hot Reloading的特性。HMR并不會改變React組件的語法,HMR只是一個用于處理幾個步驟的優(yōu)秀框架:獲取更新,將更新更新的模塊注入到腳本,調(diào)用回調(diào)。
var React = require('react')
var ReactDOM = require('react-dom')
// Render the root component normally
var rootEl = document.getElementById('root')
ReactDOM.render(<App />, rootEl)
// Are we in development mode?
if (module.hot) {
// Whenever a new version of App.js is available
module.hot.accept('./App', function () {
// Require the new version and render it instead
var NextApp = require('./App')
ReactDOM.render(<NextApp />, rootEl)
})
}
WebPack設(shè)計HMR的內(nèi)部細節(jié)
上面我們說過,React Native的Hot Reloading是在HMR的基礎(chǔ)上實現(xiàn)的,而HMR是WebPack的重要特性,我們下面分析下,HRM的實現(xiàn)細節(jié)。
對于應(yīng)用來說更新步驟如下:
- 應(yīng)用使用HRM runtime去檢測更新。
- runtime異步下載更新,通知應(yīng)用。
- 告知runtime去應(yīng)用更新
- 同步去應(yīng)用更新
對于編譯器的更新步驟如下:
manifest內(nèi)包含新的編譯哈希和已更新的chunks列表。
對于Runtime的更新步驟:
這里我們先說一下Runtime:對于manifest來講,runtime主要是指在瀏覽器運行時,webpack 用來連接模塊化的應(yīng)用程序的所有代碼。runtime 包含在模塊交互時連接模塊所需的加載和解析邏輯。包括瀏覽器中的已加載模塊的連接,以及懶加載模塊的執(zhí)行邏輯。
對于組件系統(tǒng)運行時,runtime提供了兩個方法:check
和apply
。
check
會發(fā)起HTTP請求來更新manifest,如果請求失敗,則更新失敗。請求成功后會對比新的chunks和已經(jīng)加載的chunks列表。當(dāng)所有更新的chunks下載完成并且可以應(yīng)用時,runtime會切換到ready
狀態(tài)。
apply
方法會將所有更新的組件標記為不可用。對于不可用組件,需要handler去處理,如果沒有處理,會逐級向上尋找,知道找到handler或者程序入口,如果直至入口都沒有處理,就會退出程序。
HMR Runtime
如果需要更新的組件,已經(jīng)被緩存了,就不能單純的只是完成替換,需要先接觸緩存的綁定關(guān)系。清除依賴關(guān)系的方式是遞歸進行的。
對于已經(jīng)緩存的組件,會查找依賴關(guān)系,并逐級解除。例如上圖,log的上級有MovieScreen和MovieSearch,MovieScreen沒有緩存log,所以遞歸結(jié)束。MovieSearch依賴log,而MovieRouter依賴MovieSearch,他們之間的緩存也需要解除。
為了遍歷依賴樹,會創(chuàng)建一個逆序依賴文件,如下:
{
modules: [
{
name: 'time',
code: /* time's new code */
}
],
inverseDependencies: {
MovieRouter: [],
MovieScreen: ['MovieRouter'],
MovieSearch: ['MovieRouter'],
log: ['MovieScreen', 'MovieSearch'],
time: ['log'],
}
}
React 組件
React組件使用Hot Reloading機制要更復(fù)雜些,因為不能單純的替換代碼,這樣會導(dǎo)致DOM和組件的狀態(tài)丟失。
因為模塊被重新計算了,內(nèi)部組件的ID和過去的不同,對于React而言,你想要重新渲染一個全心的組件,所以React會卸載過去組件。所以,如上所說,React會摧毀組件的DOM和本地state。
解決這種問題有幾種方法:
將state存于外部
類似于Redux這種數(shù)據(jù)流管理框架,每個組件并不管理自己的狀態(tài),而是整個APP公用一個state,每個組件獲取這個state內(nèi)的數(shù)據(jù),這樣在更新組件時,就不需要擔(dān)心組件更新的問題。
保存DOM和本地state
為了解決DOM和本地狀態(tài)被銷毀的問題,有兩種不同的做法:
- 找到一種方法,將React實例從DOM和狀態(tài)中分離出來,只更新這個實例,完成后將其“粘回”DOM和state。
- 使用代理組件類型。這樣對于React來說,類型沒有改變,不需要重新去卸載和裝載,內(nèi)部的實際實現(xiàn),會根據(jù)熱更新發(fā)生變化。
失敗方案:React實例從DOM和狀態(tài)中分離
第一種方法聽上去更好,但是目前React并沒有提供可以分離/聚合React實例和DOM、還有運行的生命周期鉤子。哪怕我們可以使用React的私有API,這個方案也未必可行。
例如React組件可能會訂閱一些生命周期方法(componentDidMount
等),即使我們可以在不摧毀DOM和狀態(tài)的情況下靜默替換舊的實例,但是因為過去的實例沒有取消對生命周期的訂閱,新的實例也無法訂閱成功。
成功方案:Proxy Component代理組件
Proxy Component就是使用在React Hot Loader和React Transform中的方案。這種方案會改變代碼的語法,但是目前在React中的應(yīng)用還算成功。
Proxy就是一個類,內(nèi)部封裝了關(guān)于顯示和狀態(tài)的實現(xiàn)。
React Hot Loader將通過module.exports
內(nèi)的React組件通過代理進行封裝,輸出封裝后的代理類。
可以這么理解:當(dāng)調(diào)用<App>render<NavBar>時,實際是在render<NavbarProxy>。
轉(zhuǎn)換機制:在轉(zhuǎn)換時為每個React組件創(chuàng)建代理,代理在組件真正的生命周期中持有它們的狀態(tài)和其他代理方法。
除了創(chuàng)建代理組件,轉(zhuǎn)換還定義了
accept
方法,用于對組件進行強制刷新。這樣組件的熱更新就不會丟失任何狀態(tài)了。