我从merge request获得了什么之箭头函数

268 阅读6分钟

故事的开头

新东家的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并未改变,所以不会renderPureComponent是生效的。

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页面还是父子组件,只要传参,不管是高阶函数,还是箭头函数,我们都无法避免性能上的损耗。

只能做到的是,在不需要传参的事件中不使用箭头函数。需要传参的函数,无法避免箭头函数造成的性能浪费。

怎么感觉探索到底,最后的结局让我有点小小的失落呢。

欢迎交流更好的方案。