React 生命周期方法和杂项
本文档是一系列关于React生命周期方法以及JSX和与React相关的浏览器内存的杂项主题的示例。
引言
本文档包含了一系列关于React生命周期方法以及JSX和与React相关的浏览器内存收集的杂项主题的示例。React支持嵌套组件,但为了简单起见,本文档仅限于非嵌套组件。本文档只关注React在浏览器端的实现。
背景
React有一系列事件风格的方法。要自信地使用React,特别是如果我们想将React与其他JavaScript库一起使用,清晰地理解这些生命周期方法非常重要。对于一个简单的非嵌套React组件,这些方法的列表如下:
- getInitialState() - 在组件挂载前调用一次。返回值将用作组件的初始状态;
- componentWillMount() - 在初始渲染前立即调用一次;
- componentDidMount() - 在初始渲染完成后立即调用一次。如果您想与其他JavaScript框架集成,请在此方法中执行这些操作;
- shouldComponentUpdate(nextProp, nextState) - 在接收到新的props或state并进行渲染之前调用。初始渲染不会调用此方法;
- componentWillUpdate() - 在接收到新的props或state并进行渲染之前立即调用。初始渲染不会调用此方法;
- componentDidUpdate() - 在组件的更新刷新到DOM后立即调用。初始渲染不会调用此方法。您可以在此方法中操作DOM,当组件更新后;
- componentWillUnmount() - 在组件从DOM中卸载前立即调用。您可以在此方法中执行任何必要的清理工作;
- render() - 此方法返回组件的HTML内容。
本文档通过示例演示这些事件风格的方法何时以及按什么顺序触发。在官方的React文档中,“getInitialState()”和“render()”方法没有被列为生命周期方法,但我认为它们在React组件的生命周期中扮演着重要的角色。
附带的是一个Java Maven项目。“index.html”文件包含指向其他示例“html”文件的超链接。
如果您有兴趣探索Maven项目,可以查看这个链接和这个链接。如果您不使用Java,也没关系,因为附带的是静态“html”文件,您可以在任何Web服务器上运行。React相关的JavaScript库是从CDN“https://cdnjs.cloudflare.com/”链接的,因此您需要互联网连接才能运行这些示例。
是否使用Babel?这就是问题所在
在生命周期方法示例之前,我想先谈谈“JSX”。“render()”方法是React返回组件HTML内容的方法。我们可以在“render()”方法中使用两种类型的语法:
- 我们可以使用JavaScript函数“React.createElement()”;
- 我们也可以使用“JSX”语法,它允许我们输入类似于HTML语法的HTML内容。
“0.JSX-or-Not-babel.html”使用了“JSX”语法,而“0.JSX-or-Not-javascripts.html”使用了“React.createElement()”函数。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>0.JSX-or-Not-babel.html</title> <link rel="stylesheet" type="text/css" href="styles/app.css"> </head> <body> <div id="content"></div> <div> </div> <div><a href="index.html">Go back</a></div> </body> <script type="text/javascript"> console.time('execution'); </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script> <script type="text/babel"> var myclass = React.createClass({ componentDidMount: function() { console.timeEnd('execution'); }, render: function() { return <div></div>; } }); ReactDOM.render( React.createElement(myclass, null), document.getElementById('content') ); </script> </html
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>0.JSX-or-Not-javascripts.html</title> <link rel="stylesheet" type="text/css" href="styles/app.css"> </head> <body> <div id="content"></div> <div> </div> <div><a href="index.html">Go back</a></div> </body> <script type="text/javascript"> console.time('execution'); </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script> <script type="text/javascript"> var myclass = React.createClass({ componentDidMount: function() { console.timeEnd('execution'); }, render: function() { return React.createElement('div', null); } }); ReactDOM.render( React.createElement(myclass, null), document.getElementById('content') ); </script> </html>
- 要使用React,我们需要在HTML文件中链接“react.js”和“react-dom.js”文件;
- 如果我们要使用“JSX”语法,我们需要链接“browser.min.js”文件,并将JavaScript类型设置为“text/babel”;
- 这两个示例都在浏览器中渲染了一个空的“div”;
- “console.time('execution')”和“console.timeEnd('execution')”函数用于测量从执行第一个JavaScript代码到渲染空“div”之间的时间。
如果我们至少访问每个网页一次,以确保所有JavaScript文件都被缓存,然后打开Chrome开发者工具中的控制台标签页并重新加载每个页面,我们可以比较渲染空“div”所花费的时间。
- 需要注意的是,时间是在所有JavaScript文件都被缓存后测量的,因此没有进一步的缓存可以提高性能;
- 需要注意的是,我们只是通过React渲染了一个空的“div”。这表明,如果我们使用带JSX的React,我们将不得不容忍0.5秒的启动开销,无论我们希望Web浏览器渲染什么;
- 经过一些反复试验,我注意到0.5秒的大部分时间都花在浏览器评估/运行“browser.min.js”文件中的代码上了。
是否使用Babel?这就是问题所在
JSX/Babel语法由于与HTML语法的相似性,确实提供了很多便利。对于一些真正的单页应用程序,0.5秒的开销可能是可以接受的,因为“browser.min.js”文件只需要为用户会话评估一次。但如果网页需要重新加载,每次加载都会给用户带来额外的0.5秒开销。无论您如何决定,了解这0.5秒的开销总是有益的。当然,我希望在未来的React版本中,这0.5秒的开销可以被移除。
生命周期方法 - 挂载过程
“1.life-cycle-mount.html”用于实验与挂载过程相关的生命周期方法以及它们的触发顺序。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>1.life-cycle-mount.html</title> <link rel="stylesheet" type="text/css" href="styles/app.css"> </head> <body> <div id="content"></div> <div> </div> <div><a href="index.html">Go back</a></div> </body> <script type="text/javascript"> </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script> <script type="text/javascript"> var myclass = React.createClass({ getInitialState: function() { console.log('getInitialState called'); return {}; }, componentWillMount: function() { console.log('componentWillMount called'); }, componentDidMount: function() { console.log('componentDidMount called'); }, render: function() { console.log('render called'); return React.createElement('div', null, "Mount methods example"); } }); window.onload = function() { var element = React.createElement(myclass, null); ReactDOM.render(element, document.getElementById('content')); }; </script> </html>
- “1.life-cycle-mount.html”在浏览器中渲染一个简单的“div”;
- 在每个方法中,都会调用“console.log()”函数,因此我们可以从开发者工具的控制台标签页中检查该方法是否被调用以及它们的调用顺序。
结论
- React组件可以通过“ReactDOM.render()”方法挂载到一个HTML元素上;
- 生命周期方法按以下顺序触发:
- getInitialState
- componentWillMount
- render
- componentDidMount
- 一旦挂载过程开始,就无法阻止这些方法被触发;
- 当“componentDidMount”方法触发时,HTML内容已由React完全渲染到DOM中。
生命周期方法 - 更新过程
“2.life-cycle-update.html”用于实验与更新过程相关的生命周期方法以及它们的触发顺序。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>2.life-cycle-update.html</title> <link rel="stylesheet" type="text/css" href="styles/app.css"> </head> <body> <div id="content"></div> <div> </div> <div><a href="index.html">Go back</a></div> </body> <script type="text/javascript"> </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script> <script type="text/javascript"> var myclass = React.createClass({ getInitialState: function() { return { title: 'Update methods example', updateid: 0 }; }, shouldComponentUpdate: function(nextProp, nextState) { if (! nextState.updateui) { console.log('shouldComponentUpdate called - return false'); return false; } console.log('shouldComponentUpdate called - return true'); return true; }, componentWillUpdate: function() { console.log('componentWillUpdate called'); }, componentDidUpdate: function() { console.log('componentDidUpdate called'); }, updateButtonClick: function() { this.setState({updateid: this.state.updateid + 1, updateui: true}); }, noupdateButtonClick: function() { this.setState({updateid: this.state.updateid + 1, updateui: false}); }, render: function() { console.log('render called'); var s = this.state; var label = s.title + ' - ' + s.updateid; var title = React.createElement('div', null, label); var noupdateButton = React.createElement('button', {onClick: this.noupdateButtonClick}, 'No-update button'); var updateButton = React.createElement('button', {onClick: this.updateButtonClick}, 'Update button'); var buttons = React.createElement('div', null, noupdateButton, updateButton); return React.createElement('div', null, title, buttons); } }); window.onload = function() { var element = React.createElement(myclass, null); ReactDOM.render(element, document.getElementById('content')); }; </script> </html>
- 在这个示例中,将在浏览器中渲染两个按钮。每个按钮都通过“setState()”方法触发更新;
- “No-update button”设置“{updateui: false}”,而“Update button”设置“{updateui: false}”;
- “shouldComponentUpdate()”方法检查“nextState”中的“updateui”属性,并相应地返回true或false。
将页面加载到浏览器中,我们可以看到两个按钮,并且可以看到“updateid”渲染为0,这与初始状态匹配。让我们打开开发者工具中的控制台标签页,然后单击“No-update button”几次。
在我的实验中,我单击了“No-update button”15次,“shouldComponentUpdate()”方法被调用了15次。但是“shouldComponentUpdate()”方法返回false,没有其他生命周期方法被触发。“updateid”在浏览器中保持为0。
如果我们现在单击“Update button”,我们可以看到“updateid”在浏览器中更新为16。在控制台中,我们可以看到所有与更新相关的生命周期方法都被触发了,因为“shouldComponentUpdate()”返回了true。
结论
- UI/DOM的更新可以通过“setState()”方法触发;
- 调用“setState()”后,“shouldComponentUpdate()”会被调用;
- 如果“shouldComponentUpdate()”返回false,则只更新数据状态,不更新UI/DOM;
- 如果“shouldComponentUpdate()”返回true,则同时更新数据状态和UI/DOM。
- 当“shouldComponentUpdate()”返回true后,其他生命周期方法按以下顺序触发:
- componentWillUpdate
- render
- componentDidUpdate
- “componentWillUpdate()”方法给了我们机会在React开始更新DOM之前执行必要的清理工作;
- 当“componentDidUpdate()”方法触发时,DOM的更改已完全准备好供其他JavaScript代码进行操作。
生命周期方法 - 卸载过程
“3.life-cycle-unmount.html”用于实验在卸载过程中“componentWillUnmount()”生命周期方法。
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>3.life-cycle-unmount.html</title> <link rel="stylesheet" type="text/css" href="styles/app.css"> </head> <body> <div id="content"></div> <div> </div> <div><a href="index.html">Go back</a></div> </body> <script type="text/javascript"> </script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.0/react-dom.js"></script> <script type="text/javascript"> var myclass = React.createClass({ getInitialState: function() { var size = 20 * 1024 * 1024; var a = new Array(size); for (var i = 0; i < size; i++) { a[i] = i; } return {a: a}; }, unmountButtonClick: function() { var node = ReactDOM.findDOMNode(this); ReactDOM.unmountComponentAtNode(node.parentNode); }, componentWillUnmount: function() { console.log('componentWillUnmount called'); }, render: function() { var title = React.createElement('div', null, 'Unmount method example'); var unmountButton = React.createElement('button', {onClick: this.unmountButtonClick}, 'Unmount from DOM button'); var buttons = React.createElement('div', null, unmountButton); return React.createElement('div', null, title, buttons); } }); window.onload = function() { var element = React.createElement(myclass, null); ReactDOM.render(element, document.getElementById('content')); }; </script> </html>
- 在这个示例中,一个按钮被添加到DOM中。如果我们点击按钮,组件将被从DOM中卸载;
- 在“getInitialState()”方法中,我人为地向初始状态添加了一个20M的数组。我将使用这块大内存来演示在“React浏览器内存清理”部分卸载React组件时浏览器是否会进行垃圾回收。
如果我们点击“Unmount from DOM button”,组件将从DOM中移除。我们可以在控制台中看到“componentWillUnmount()”方法在组件从DOM中清除之前被调用。
结论
- 已挂载的React组件可以通过“ReactDOM.unmountComponentAtNode()”方法从DOM中卸载;
- 一旦卸载过程开始,就无法停止;
- 在组件从DOM中移除之前,“componentWillUnmount()”会被触发,为我们提供执行一些清理工作的机会。
React浏览器内存清理
在我研究Angular内存泄漏问题时,我通过这个链接学会了检查浏览器内存的技术。在Chrome浏览器中,我们可以使用“Shift + ESC”来启动Chrome任务管理器。
加载“3.life-cycle-unmount.html”页面后,我们可以看到服务于该页面的进程所使用的内存。如果我们点击“Unmount from DOM button”按钮来卸载React组件,我们应该预期垃圾回收会发生,以收集未使用的内存。
垃圾回收可能不会在组件卸载后立即发生。可能需要一段时间。如果它没有发生,我们可以转到开发工具的“Timeline”选项卡,通过点击“trash”图标来触发垃圾回收。
通过这个小测试,我们应该可以放心,如果我们有意识地编写代码,不留下对组件及其状态的死引用,React应该不会有内存泄漏问题。至少对于本文档中使用的React版本来说是如此。
关注点
历史
修订 - 2016/5/12