故事的开头
新东家的merge request比前任要严格好多。
一些平时习以为常的习惯,发现会引起好多问题。
曾经因为组件拆分的不好,merge request卡了两天之久,改了三版。有兴趣的话可以留言,到时候可以开篇讲讲~
也因为被提的次数多了,导致现在在node端看到接口请求就条件反射try catch。
今天这个主题来源自merge request被提出的黑榜。
第一次写技术文章,如果有写的不清楚或者不好的地方,求轻拍~
箭头函数
在只有做setState
操作的时候偷懒写函数,喜欢直接在函数里写() => this.setState({state:value})
class MyButton extends React.Component {
render(){
return <Button onClick={() => this.setState({ visible: true })}>这是个按钮</Button>
}
}
我们知道,在React
中,render
函数在每次props
或者state
更改的时候会触发,而箭头函数使用时都会有一个返回值,我们打印一下它:
// console下() => this.setState({ visible: true })
ƒ () {
return _this.setState({ visible: true });
}
每执行一次render
,都创建一次_this.setState
实例,可以想象,在频繁触发render
时,内存里有多少的实例。
因此,我们应该避免直接在render
内使用箭头函数,纵然只有一行代码,也要定义一个函数,箭头函数不是万能的。
class MyButton extends React.Component {
handleButton = () => {
this.setState({ visible: true })
}
render(){
return <Button onClick={this.handleButton}>这是个按钮</Button>
}
}
只在组件中定义箭头函数一次,对比上面直接使用箭头函数的console
// console下this.handleButton
ƒ () {
_this.setState({ visible: true });
}
// console下() => this.setState({ visible: true })
ƒ () {
return _this.setState({ visible: true });
}
可是,当我们传参的时候,好像没办法避免使用箭头函数传参耶…比如
class MyButton extends React.Component {
state = {
list: [ { id:1 }, { id:2 } ]
}
handleButton = (e) => {
this.setState({ list: [] })
}
render(){
return
list.map((item) =>
<Button onClick={() => this.handleButton(item.id)}>这是个按钮</Button>)
}
}
一切好像又回到了最开始的问题,我们应该就这样向传参认输吗?
社会主义教会我们,不能认输,我们再想(kan)想(kan)问(she)题(fang)。
我们使用社区上另一种流行的写法来试试看。
class MyButton extends React.Component {
state = {
list: [ { id:1 }, { id:2 } ]
}
handleButton = (id) => (e) => {
this.setState({ list: [ { id: id+1 }, { id: 2 } ] })
}
render(){
return
list.map((item) =>
<Button onClick={this.handleButton(item.id)}>这是个按钮</Button>)
}
}
它的原理是高阶函数,什么是高阶函数,JavaScript的函数其实都指向某个变量。既然变量可以指向函数,函数的参数能接收变量,那么一个函数就可以接收另一个函数作为参数,这种函数就称之为高阶函数。 我们可以将它看做:
handleButton = (id) => {
return (e) => {
this.setState({ list: [ { id:id+1 }, { id:2 } ] })
}
}
打印下这个函数
// console下this.handleButton(item.id)
ƒ () {
_this.setState({
list: [{ id: id + 1 }, { id: 2 }]
});
}
它和this.handleButton
(看起来)一样。
扩展一下,如果在父子组件内使用,高阶函数和普通函数是否有差别呢?
React
官方不推荐箭头函数绑定的原因里有一句话:如果该回调函数作为 prop 传入子组件时,这些组件可能会进行额外的重新渲染,怎么理解这句话?
我们来更改一下我们最初的例子,通过父子组件的方式
class MyButton extends React.Component {
state = {
list: [ { id:1 }, { id:2 } ]
}
handleButton = () => {
this.setState({ list: [ { id:3 }, { id:2 } ] }) // 图快先这么写着 只更改第一个组件的id
}
render(){
return list.map((item) => <Test id={item.id} onClick={this.handleButton} />
}
}
Test.js
export default class Test extends React.PureComponent {
render () {
return <p onClick={this.props.onClick}>{this.props.id}</p>
}
}
从PureComponent
的原理来说,当传入的props
不改变时,组件不会重新渲染。
因此,以上代码中,只有第一个组件会进行render函数,而第二个组件由于props
并未改变,所以不会render
。PureComponent
是生效的。
class MyButton extends React.Component {
state = {
list: [ { id:1 }, { id:2 } ]
}
handleButton = (e) => {
this.setState({ list: [ { id:e+1 }, { id:2 } ] }) // 只更改第一个组件的id
}
render(){
return list.map((item) =>
<Test id={item.id} onClick={() => this.handleButton(item.id)} />)
}
}
如果我们使用箭头函数,则会使两个组件都触发重新render
,由于每次箭头函数返回的都是新的实例,每次父组件渲染,传给子组件的 props.onClick
都会变,PureComponent
的 Shallow Compare 基本上就失效了,除非你手动实现 shouldComponentUpdate
.(——@黑猫)。
click
一下第一个组件,得到的结果是,两个组件都进行了render
。
原因是每次父组件render返回的依旧是新的function实例,这个实例被绑定在了onclick
事件上,PureComponent
不生效,渲染浪费,这个很好理解。
那我们来试试上文提到的,看似相同的高阶函数写法
class MyButton extends React.Component {
state = {
list: [ { id:1 }, { id:2 } ]
}
handleButton = (e) => () => {
this.setState({ list: [ { id:e+1 }, { id:2 } ] }) // 只更改第一个组件的id
}
render(){
return list.map((item) =>
<Test id={item.id} onClick={this.handleButton(item.id)} />)
}
}
结果是,PureComponent
也不生效。表现和直接使用箭头函数传参是一样的。
到此,我们不禁困惑?说好的高阶呢?我们来推理下。
影响PureComponent
不生效的原因是props
没有通过浅比较,我们传入Test
组件的props
只有两个属性,id
属性和onClick
事件,可能的就是传入的onClick
事件没有通过比较。
我们在父组件层打印一下两种写法:
// 使用高阶函数写法
handleButton = (e) => () => {
this.setState({ list: [ { id:e+1 }, { id:2 } ] })
}
// 普通写法
handleButton2 = () => {
this.setState({ list: [{id: 3}, {id: 2}] })
}
...
this.handleButton(1) === this.handleButton(1) // false
this.handleButton2 === this.handleButton2 // true
函数是引用类型,引用类型的比较是引用的比较,由此,我们可以知道,高阶函数使得函数的引用变化了。引用类型是按引用访问的,换句话说就是比较两个对象的堆内存中的地址是否相同,那很明显,handleButton(1)
和handleButton(1)
在堆内存中地址是不同的
我们知道,在React中
直接使用this.myFunction()
是会直接执行函数的,所以当我们将高阶函数绑定在了render
内,无疑在每次render
时运行了这个函数:
handleButton = (id) => {
console.log('运行了handleButton')
return () => {
this.setState({
list: [{id: id + 2}, {id: 2}]
})
}
}
以上是只有一个Test
组件绑定了一个事件时,handleButton
运行了两次,每次返回一个箭头函数的引用,可以想象,当有10个组件,绑定了5个以上不同的事件(实际在工程中,列表的组件可能更多,老代码里的绑定事件也不止5个),会有多少的引用。
当我们点击handleButton
事件时,子组件表现为
触发父组件render
,又一次更改了handleButton
的引用,于是之后的所有子组件都重新渲染一遍。
至此,我们发现父子组件里,高阶函数传参与箭头函数传参基本是一样的。
那么回到故事的开始,不管是page
页面还是父子组件,只要传参,不管是高阶函数,还是箭头函数,我们都无法避免性能上的损耗。
只能做到的是,在不需要传参的事件中不使用箭头函数。需要传参的函数,无法避免箭头函数造成的性能浪费。
怎么感觉探索到底,最后的结局让我有点小小的失落呢。
欢迎交流更好的方案。