React在前端界一直很流行,并且学起来也不是很难,只须要学会JSX、理解State
和Props
,而后就能够愉快的玩耍了,但想要成为React的专家你还须要对React有一些更深刻的理解,但愿本文对你有用。html
这是Choerodon的一个前端页面前端
在复杂的前端项目中一个页面可能包含上百个状态,对React框架理解得更精细一些对前端优化很重要。曾经这个页面点击一条记录展现详情会卡顿数秒,而这仅仅是前端渲染形成的。react
为了可以解决这些问题,开发者须要了解React组件从定义到在页面上呈现(而后更新)的整个过程。git
React在编写组件时使用混合HTML
和JavaScript
的一种语法(称为JSX)。 可是,浏览器对JSX及其语法一无所知,浏览器只能理解纯JavaScript
,所以必须将JSX转换为HTML
。 这是一个div的JSX代码,它有一个类和一些内容:github
<div className='cn'> 文本 </div> 复制代码
在React中将这段jsx变成普通的js以后它就是一个带有许多参数的函数调用:算法
React.createElement( 'div', { className: 'cn' }, '文本' ); 复制代码
它的第一个参数是一个字符串,对应html中的标签名,第二个参数是它的全部属性所构成的对象,固然,它也有多是个空对象,剩下的参数都是这个元素下的子元素,这里的文本也会被看成一个子元素,因此第三个参数是 “文本”
。npm
到这里你应该就能想象这个元素下有更多children
的时候会发生什么。api
<div className='cn'> 文本1 <br /> 文本2 </div> 复制代码
React.createElement( 'div', { className: 'cn' }, '文本1', // 1st child React.createElement('br'), // 2nd child '文本1' // 3rd child ) 复制代码
目前的函数有五个参数:元素的类型,所有属性的对象和三个子元素。 因为一个child
也是React已知的HTML
标签,所以它也将被解释成函数调用。数组
到目前为止,本文已经介绍了两种类型的child
参数,一种是string
纯文本,一种是调用其余的React.createElement
函数。其实,其余值也能够做为参数,好比:浏览器
使用数组是由于能够将子组件分组并做为一个参数传递:
React.createElement( 'div', { className: 'cn' }, ['Content 1!', React.createElement('br'), 'Content 2!'] ) 复制代码
固然,React的强大功能不是来自HTML
规范中描述的标签,而是来自用户建立的组件,例如:
function Table({ rows }) { return ( <table> {rows.map(row => ( <tr key={row.id}> <td>{row.title}</td> </tr> ))} </table> ); } 复制代码
组件容许开发者将模板分解为可重用的块。在上面的“纯函数”组件的示例中,组件接受一个包含表行数据的对象数组,并返回React.createElement
对
每当开发者将组件放入JSX布局中时它看上去是这样的:
<Table rows={rows} />
复制代码
但从浏览器角度,它看到的是这样的:
React.createElement(Table, { rows: rows });
复制代码
请注意,此次的第一个参数不是以string
描述的HTML元素,而是组件的引用(即函数名)。第二个参数是传入该组件的props
对象。
如今,浏览器已经将全部JSX组件转换为纯JavaScript
,如今浏览器得到了一堆函数调用,其参数是其余函数调用,还有其余函数调用......如何将它们转换为构成网页的DOM元素?
为此,开发者须要使用ReactDOM
库及其render
方法:
function Table({ rows }) { /* ... */ } // 组件定义 // 渲染一个组件 ReactDOM.render( React.createElement(Table, { rows: rows }), // "建立" 一个 component document.getElementById('#root') // 将它放入DOM中 ); 复制代码
当ReactDOM.render
被调用时,React.createElement
最终也会被调用,它返回如下对象:
// 这个对象里还有不少其余的字段,但如今对开发者来讲重要的是这些。 { type: Table, props: { rows: rows }, // ... } 复制代码
这些对象构成了React意义上的Virtual DOM
它们将在全部进一步渲染中相互比较,并最终转换为真正的DOM(与Virtual DOM对比)。
这是另外一个例子:此次有一个div具备class属性和几个子节点:
React.createElement( 'div', { className: 'cn' }, 'Content 1!', 'Content 2!', ); 复制代码
变成:
{ type: 'div', props: { className: 'cn', children: [ 'Content 1!', 'Content 2!' ] } } 复制代码
全部的传入的展开函数,也就是React.createElement
除了第一第二个参数剩下的参数都会在props
对象中的children
属性中,无论传入的是什么函数,他们最终都会做为children
传入props
中。
并且,开发者能够直接在JSX代码中添加children
属性,将子项直接放在children
中,结果仍然是相同的:
<div className='cn' children={['Content 1!', 'Content 2!']} /> 复制代码
在Virtual DOM对象被创建出来以后ReactDOM.render
会尝试按如下规则把它翻译成浏览器可以看得懂的DOM节点:
若是
Virtual DOM对象中的type属性是一个string类型的tag名称,就
建立一个tag,包含props里的所有属性。若是
Virtual DOM对象中的type属性是一个函数或者class,就
调用它,它返回的可能仍是一个Virtual DOM而后将结果继续递归调用此过程。若是
props中有children属性,就
对children中的每一个元素进行以上过程,并将返回的结果放到父DOM节点中。最后,浏览器得到了如下HTML(对于上述table的例子):
<table> <tr> <td>Title</td> </tr> ... </table> 复制代码
接下浏览器要“重建”一个DOM节点,若是浏览器要更新一个页面,显然,开发者并不但愿替换页面中的所有元素,这就是React真正的魔法了。如何才能实现它?先从最简单的方法开始,从新调用这个节点的ReactDOM.render
方法。
// 第二次调用 ReactDOM.render( React.createElement(Table, { rows: rows }), document.getElementById('#root') ); 复制代码
这一次,上面的代码执行逻辑将与看到的代码不一样。React不是从头开始建立全部DOM节点并将它们放在页面上,React将使用“diff”算法,以肯定节点树的哪些部分必须更新,哪些部分能够保持不变。
那么它是怎样工做的?只有少数几个简单的状况,理解它们将对React程序的优化有很大帮助。请记住,接下来看到的对象是用做表示React Virtual DOM中节点的对象。
▌Case 1:type是一个字符串,type在调用之间保持不变,props也没有改变。
// before update { type: 'div', props: { className: 'cn' } } // after update { type: 'div', props: { className: 'cn' } } 复制代码
这是最简单的状况:DOM保持不变。
▌Case 2:type仍然是相同的字符串,props是不一样的。
// before update: { type: 'div', props: { className: 'cn' } } // after update: { type: 'div', props: { className: 'cnn' } } 复制代码
因为type仍然表明一个HTML元素,React知道如何经过标准的DOM API调用更改其属性,而无需从DOM树中删除节点。
▌Case 3:type已更改成不一样的组件String或从String组件更改成组件。
// before update: { type: 'div', props: { className: 'cn' } } // after update: { type: 'span', props: { className: 'cn' } } 复制代码
因为React如今看到类型不一样,它甚至不会尝试更新DOM节点:旧元素将与其全部子节点一块儿被删除(unmount)。所以,在DOM树上替换彻底不一样的元素的代价会很是之高。幸运的是,这在实际状况中不多发生。
重要的是要记住React使用===(三等)来比较type值,所以它们必须是同一个类或相同函数的相同实例。
下一个场景更有趣,由于这是开发者最常使用React的方式。
▌Case 4:type是一个组件。
// before update: { type: Table, props: { rows: rows } } // after update: { type: Table, props: { rows: rows } } 复制代码
你可能会说,“这好像没有任何变化”,但这是不对的。
若是type是对函数或类的引用(即常规React组件),而且启动了树diff比较过程,那么React将始终尝试查看组件内部的全部child
以确保render
的返回值没有更改。即在树下比较每一个组件 - 是的,复杂的渲染也可能变得昂贵!
除了上面描述的四种常见场景以外,当元素有多个子元素时,开发者还须要考虑React的行为。假设有这样一个元素:
// ... props: { children: [ { type: 'div' }, { type: 'span' }, { type: 'br' } ] }, // ... 复制代码
开发者开发者想将它从新渲染成这样(span
和div
交换了位置):
// ... props: { children: [ { type: 'span' }, { type: 'div' }, { type: 'br' } ] }, // ... 复制代码
那么会发生什么?
当React看到里面的任何数组类型的props.children
,它会开始将它中的元素与以前看到的数组中的元素按顺序进行比较:index 0将与index 0,index 1与index 1进行比较,对于每对子元素,React将应用上述规则集进行比较更新。在以上的例子中,它看到div
变成一个span
这是一个情景3中的状况。但这有一个问题:假设开发者想要从1000行表中删除第一行。React必须“更新”剩余的999个孩子,由于若是与先前的逐个索引表示相比,他们的内容如今将不相等。
幸运的是,React有一种内置的方法来解决这个问题。若是元素具备key
属性,则元素将经过key
而不是索引进行比较。只要key
是惟一的,React就会移动元素而不将它们从DOM树中移除,而后将它们放回(React中称为挂载/卸载的过程)。
// ... props: { children: [ // 如今react就是根据key,而不是索引来比较了 { type: 'div', key: 'div' }, { type: 'span', key: 'span' }, { type: 'br', key: 'bt' } ] }, // ... 复制代码
到目前为止,本文只触及了props
,React哲学的一部分,但忽略了state
。这是一个简单的“有状态”组件:
class App extends Component { state = { counter: 0 } increment = () => this.setState({ counter: this.state.counter + 1, }) render = () => (<button onClick={this.increment}> {'Counter: ' + this.state.counter} </button>) } 复制代码
如今,上述例子中的state
对象有一个counter
属性。单击按钮会增长其值并更改按钮文本。可是当用户点击时,DOM会发生什么?它的哪一部分将被从新计算和更新?
调用this.setState
也会致使从新渲染,但不会致使整个页面重渲染,而只会致使组件自己及其子项。父母和兄弟姐妹均可以幸免于难。
本文准备了一个DEMO,这是修复问题前的样子。你能够在这里查看其源代码。不过在此以前,你还须要安装React Developer Tools。
打开demo要看的第一件事是哪些元素以及什么时候致使Virtual DOM更新。导航到浏览器的Dev Tools中的React面板,点击设置而后选择“Highlight Updates”复选框:
如今尝试在表中添加一行。如你所见,页面上的每一个元素周围都会出现边框。这意味着每次添加行时,React都会计算并比较整个Virtual DOM树。如今尝试按一行内的计数器按钮。你将看到Virtual DOM如何更新 (state仅相关元素及其子元素更新)。
React DevTools暗示了问题可能出现的地方,但没有告诉开发者任何细节:特别是有问题的更新是指元素“diff”以后有不一样,仍是组件被unmount/mount了。要了解更多信息,开发者须要使用React的内置分析器(请注意,它不能在生产模式下工做)。
转到Chrome DevTools中的“Performance”标签。点击record按钮,而后点击表格。添加一些行,更改一些计数器,而后点击“Stop”按钮。稍等一下子以后开发者会看到:
在结果输出中,开发者须要关注“Timing”。缩放时间轴,直到看到“React Tree Reconciliation”组及其子项。这些都是组件的名称,旁边有[update]或[mount]。能够看到有一个TableRow被mount了,其余全部的TableRow都在update,这并非开发者想要的。
大多数性能问题都由[update]或[mount]引发
一个组件(以及组件下的全部东西)因为某种缘由在每次更新时从新挂载,开发者不想让它发生(从新挂载很慢),或者在大型分支上执行代价过大的重绘,即便组件彷佛没有发生任何改变。
如今,当开发者了解React如何决定更新Virtual DOM并知道幕后发生的事情时,终于准备好解决问题了!修复性能问题首先要解决 mount/unmount。
若是开发者将任何元素/组件的多个子元素在内部表示为数组,那么程序能够得到很是明显的速度提高。
考虑一下:
<div> <Message /> <Table /> <Footer /> </div> 复制代码
在虚拟DOM中,将表示为:
// ... props: { children: [ { type: Message }, { type: Table }, { type: Footer } ] } // ... 复制代码
一个简单的Message
组件(是一个div
带有一些文本,像是猪齿鱼的顶部通知)和一个很长的Table
,比方说1000多行。它们都是div
元素的child
,所以它们被放置在父节点的props.children
之下,而且它们没有key
。React甚至不会经过控制台警告来提醒开发者分配key,由于子节点React.createElement
做为参数列表而不是数组传递给父节点。
如今,用户已经关闭了顶部通知,因此Message
从树中删除。Table
、Footer
是剩下的child。
// ... props: { children: [ { type: Table }, { type: Footer } ] } // ... 复制代码
React如何看待它?它将它视为一系列改变了type的child:children[0]的type原本是Message
,但如今他是Table
。由于它们都是对函数(和不一样函数)的引用,它会卸载整个Table并再次安装它,渲染它的全部子代:1000多行!
所以,你能够添加惟一键(但在这种特殊状况下使用key
不是最佳选择)或者采用更智能的trick:使用 && 的布尔短路运算,这是JavaScript
和许多其余现代语言的一个特性。像这样:
<div> {isShowMessage && <Message />} <Table /> <Footer /> </div> 复制代码
即便Message
被关闭了(再也不显示),props.children
父母div仍将拥有三个元素,children[0]具备一个值false
(布尔类型)。还记得true
/false
, null
甚至undefined
都是Virtual DOM对象type属性的容许值吗?浏览器最终获得相似这样的东西:
// ... props: { children: [ false, // isShowMessage && <Message /> 短路成了false { type: Table }, { type: Footer } ] } // ... 复制代码
因此,无论Message
是否被显示,索引都不会改变,Table
仍然会和Table
比较,但仅仅比较Virtual DOM一般比删除DOM节点并从中建立它们要快得多。
如今来看看更高级的东西。开发者喜欢HOC。高阶组件是一个函数,它将一个组件做为一个参数,添加一些行为,并返回一个不一样的组件(函数):
function withName(SomeComponent) { return function(props) { return <SomeComponent {...props} name={name} />; } } 复制代码
开发者在父render
方法中建立了一个HOC 。当React
须要从新渲染树时,React
的Virtual DOM将以下所示:
// On first render: { type: ComponentWithName, props: {}, } // On second render: { type: ComponentWithName, // Same name, but different instance props: {}, } 复制代码
如今,React只会在ComponentWithName上运行一个diff算法,可是此次同名引用了一个不一样的实例,三等于比较失败,必须进行彻底从新挂载。注意它也会致使状态丢失,幸运的是,它很容易修复:只要返回的实例都是同一个就行了:
// 单例 const ComponentWithName = withName(Component); class App extends React.Component() { render() { return <ComponentWithName />; } } 复制代码
如今浏览器已经确保不会从新装载东西了,除非必要。可是,对位于DOM树根目录附近的组件所作的任何更改都将致使其全部子项的进行对比重绘。结构复杂,价格昂贵且常常能够避免。
若是有办法告诉React不要查看某个分支,那将是很好的,由于它没有任何变化。
这种方式存在,它涉及一个叫shouldComponentUpdate
的组件生命周期函数。React会在每次调用组件以前调用此方法,并接收props
和state
的新值。而后开发者能够自由地比较新值和旧值之间的区别,并决定是否应该更新组件(返回true
或false
)。若是函数返回false
,React将不会从新渲染有问题的组件,也不会查看其子组件。
一般比较两组props
和state
一个简单的浅层比较就足够了:若是顶层属性的值相同,浏览器就没必要更新了。浅比较不是JavaScript
的一个特性,但开发者不少方法来本身实现它,为了避免重复造轮子,也可使用别人写好的方法。
在引入浅层比较的npm包后,开发者能够编写以下代码:
class TableRow extends React.Component { shouldComponentUpdate(nextProps, nextState) { const { props, state } = this; return !shallowequal(props, nextProps) && !shallowequal(state, nextState); } render() { /* ... */ } } 复制代码
可是你甚至没必要本身编写代码,由于React在一个名为React.PureComponent的类中内置了这个功能,它相似于React.Component
,只是shouldComponentUpdate
已经为你实现了浅层props/state比较。
或许你会有这样的想法,能替换Component
为PureComponent
就去替换。但开发者若是错误地使用PureComponent
一样会有从新渲染的问题存在,须要考虑下面三种状况:
<Table // map每次都会返回一个新的数组实例,因此每次比较都是不一样的 rows={rows.map(/* ... */)} // 每一次传入的对象都是新的对象,引用是不一样的。 style={ { color: 'red' } } // 箭头函数也同样,每次都是不一样的引用。 onUpdate={() => { /* ... */ }} /> 复制代码
上面的代码片断演示了三种最多见的反模式,请尽可能避免它们!
正确地使用PureComponent
,你能够在这里看到全部的TableRow都被“纯化”后渲染的效果。
可是,若是你火烧眉毛想要所有使用纯函数组件,这样是不对的。比较两组props
和state
不是免费的,对于大多数基本组件来讲甚至都不值得:运行shallowCompare
比diff算法须要更多时间。
可使用此经验法则:纯组件适用于复杂的表单和表格,但它们一般会使按钮或图标等简单元素变慢。
如今,你已经熟悉了React的渲染模式,接下来就开始前端优化之旅吧。
Choerodon猪齿鱼是一个开源企业服务平台,基于Kubernetes的容器编排和管理能力,整合DevOps工具链、微服务和移动应用框架,来帮助企业实现敏捷化的应用交付和自动化的运营管理的开源平台,同时提供IoT、支付、数据、智能洞察、企业应用市场等业务组件,致力帮助企业聚焦于业务,加速数字化转型。
你们也能够经过如下社区途径了解猪齿鱼的最新动态、产品特性,以及参与社区贡献: