探秘 React Hot Loader
自從 webpack 成為前端構建的主流工具後,有一個很重要的開發調試模式開始被前端熟悉 — Hot Module Replacement。
這項技術在一定程度上改變了我們之前改一行代碼就要刷新瀏覽器看效果的開發方式,這種方式無法保存我們頁面上的狀態,比如頁面中 Tab 切換的標籤、Modal 框打開的狀態、表單選中和填寫的狀態。但是 HMR 能夠完美解決這個問題
簡單的 HMR 例子
我們來看一個 webpack 官方提供的最簡單的 demo
index.js
import _ from lodash; import printMe from ./print.js; function component() { var element = document.createElement(div); var btn = document.createElement(button); element.innerHTML = _.join([Hello, webpack], ); btn.innerHTML = Click me and check the console!; btn.onclick = printMe; // onclick 事件綁定原始的 printMe 函數上 element.appendChild(btn); return element; }- document.body.appendChild(component());+ let element = component(); // 當 print.js 改變導致頁面重新渲染時,重新獲取渲染的元素+ document.body.appendChild(element); if (module.hot) { module.hot.accept(./print.js, function() { console.log(Accepting the updated printMe module!);- printMe();+ document.body.removeChild(element);+ element = component(); // 重新渲染頁面後,component 更新 click 事件處理+ document.body.appendChild(element); }) }
print.js
export default function printMe() {- console.log(I get called from print.js!);+ console.log(Updating print.js...) }
在 index.js 中我們為了實現熱更新不僅改變正常的代碼邏輯,並且額外寫了很多代碼來應用更新,因此如果我們的 App 是不引入任何 MVVM 級別的框架時,HMR只能像上面的 Demo 一樣在某些點上應用上熱更新(這也是很有意義的,比如我們將頁面的一些全局 config,一些主要的渲染點都應用熱更新,都是能一定程度上幫助開發的),但是正如 webpack 文檔中說的
這只是一個例子,但還有很多其他地方可以輕鬆地讓人犯錯。幸運的是,存在很多 loader(其中一些在下面提到),使得模塊熱替換的過程變得更容易。
簡單的 React HMR 的例子
ReactDOM.render 多次調用是沒有副作用的,除了在第一次會將 mount node 的子節點全部清空,剩下的時候都會調用 React 的 diff 演算法,高效更新
ReactDOM.render()
controls the contents of the container node you pass in. Any existing DOM elements inside are replaced when first called. Later calls use React』s DOM diffing algorithm for efficient updates.
我們可以利用這一特性很簡單的就實現 React App 的無刷新 HMR
entry.jsx
import React from reactimport {render} from react-domimport App from ./Appconst Render = () => { render(<App />, document.getElementById(root))}Render()if (module.hot) { module.hot.accept(./App, () => { Render() })}
App.jsx
import React, { PureComponent, Component } from reactexport default class App extends PureComponent { state = { counter: 0 } handleIncrease = () => { this.setState({ counter: this.state.counter + 1 }) } handleDecrease = () => { this.setState({ counter: this.state.counter - 1 }) } render() { return ( <div> <button onClick={this.handleIncrease}>+</button> <span>counter: {this.state.counter}</span> <button onClick={this.handleDecrease}>-</button> </div> ) }}
無需額外的引入任何其他的庫和工具,我們已經能夠實現 HMR,但是每次調用 ReactDOM.render 後,每個組件內部的 state 會被完全初始化,上面的例子中,我們的counter 會被重置,這種 HMR 的方式如果我們的 App 是 stateless 的,比如由 Redux 接管所有的狀態管理,這樣我們就已經能夠愉快的開發了。
但是如果我們如果想享受 HMR 帶來的開發便利,但是又不想丟失 component state,我們就需要用到 React Hot Loader。
React Hot Loader 的原理
React Hot Loader 的實現依賴兩個主要的庫 gaearon/react-proxy 和 gaearon/react-deep-force-update 這兩個庫在 babel 編譯過程中侵入 React Comeponent 的編譯結果和通過 React 的隱藏 api 強制刷新組件樹來完美的實現保持狀態的 React HMR,下面我們通過源碼來研究一下這一切是如何做到的
react-proxy 的功能
我們通過一個例子來了解一下這個庫的功能
import React from reactimport {render} from react-domimport createProxy from react-proxyimport deepForceUpdate from react-deep-force-updateimport App from ./Appclass App1 extends App { handleIncrease() { this.setState({ counter: this.state.counter + 2 }) }}const proxy = createProxy(App)const ProxyApp = proxy.get()const Instance = render(<ProxyApp />, document.getElementById(root))const proxyUpdate = () => { proxy.update(App1) deepForceUpdate(Instance)}proxyUpdate()
當proxyUpdate調用時,App 不會刷新,當前 App 狀態保持,但是 handleIncrease 會被更新
react-proxy 的大致原理是創建一個 ProxyComponent, 保持這個 ProxyComponent 不變,update 介面會根據更新前後 Component 的差異,對 ProxyComponent 原型鏈上的方法做更新
react-deep-force-update 的功能
通過 React.Component - forceUpdate Api 來強制更新整個 component tree
react-hot-loader/patch,react-hot-loader/babel
if (typeof global.__REACT_HOT_LOADER__ === undefined) { React.createElement = patchedCreateElement React.createFactory = patchedCreateFactory global.__REACT_HOT_LOADER__ = hooks}
這個庫 hack 掉基礎的 createElement ,這樣我們在整個渲染的中使用的 Componet 都是經過 react-proxy 包裝過的
每一次 webpack 推送的更新 chunk,因為 react-hot-loader/babel 的處理,都會帶上
__REACT_HOT_LOADER__.register(type, uniqueLocalName, fileName)
用於通知 react-hot-loader/patch 里的 proxy map,調用 proxy.update, 更新對應的 proxy
AppContainer in react-hot-loader
AppContainer 是實現 hmr 的最後一步
import deepForceUpdate from react-deep-force-updateclass AppContainer extends Component { componentWillReceiveProps() { ... deepForceUpdate(this) }}
當我們在入口文件中監控文件變化然後不斷調用 ReactDOM.render 時會不斷觸發 AppContainer.componentWillReceiveProps, 接著會強制更新整個component tree
最後...
最初只是因為在項目中經常配置不好 react-hot-loader 才動了心思仔細研究了源碼,看源碼的過程中驚嘆於 gaearon (Dan Abramov) 的腦洞和大神對開發工具的重視,本文涉及到的項目源代碼中還有很多設計的很好的細節後續會和大家一起分享
涉及 repo
gaearon/react-proxy
gaearon/react-deep-force-update
GilbertSun/react-hot-loader-demo
推薦閱讀: