fi3ework / blog

📝
861 stars 51 forks source link

在React中通过反模式获取props中函数的更新 #20

Open fi3ework opened 6 years ago

fi3ework commented 6 years ago

前言

在看 React 的内联函数和性能 看到了一段很有意思的代码段,乍一看挺简单的代码,但是弄懂还是认真的想了一下,在这里分享一下思考的过程。

代码

// 1. App 会传递一个 prop 给 From 表单
// 2. Form 将向下传递一个函数给 button
//    这个函数与它从 App 得到的 prop 相接近
// 3. App 会在 mounting 之后 setState,并传递
//    一个**新**的 prop 给 Form
// 4. Form 传递一个新的函数给 Button,这个函数与
//    新的 prop 相接近
// 5. Button 会忽略新的函数, 并无法
//    更新点击处理程序,从而提交陈旧的数据

class App extends React.Component {
  state = { val: "one" }

  componentDidMount() {
    this.setState({ val: "two" })
  }

  render() {
    return <Form value={this.state.val} />
  }
}

const Form = props => (
  <Button
    onClick={() => {
      submit(props.value)
    }}
  />
)

class Button extends React.Component {
  shouldComponentUpdate() {
    // 让我们假装比较了除函数以外的一切东西
    return false
  }

  handleClick = () => this.props.onClick()

  render() {
    return (
      <div>
        <button onClick={this.props.onClick}>这个的数据是旧的</button>
        <button onClick={() => this.props.onClick()}>这个工作正常</button>
        <button onClick={this.handleClick}>这个也工作正常</button>
      </div>
    )
  }
}

在线把玩地址:这是一个运行该应用程序的沙箱

上面的三个 button。

第一个会打印 "one"。

第一个会打印 "two"。

第一个会打印 "two"。

奇怪,明明长得都差不多为什么会有区别呢?

解释

第一个

首先,从上到下看,App 的 state 更新,导致 re-render。Form 是一个 stateless component,接受一个新的 prop 必然会 re-render。然后是关键的 Button,Button 将 shouldComponent 给直接 return false 了,这会导致 render 不会被再次调用,在 Button 第一次 render 后(事实上也只有一个 render,因为 shouldComponentUpdate 直接 return false 了),onClick 指向的是 prevProps 的 this.props.onClick。

在这里还需要将 JSX 还原一下方便理解,JSX 调用 React.createElemennt 生成的 VDOM 的简化版可以表达为

{
    type: button,
    onClick: this.props.onClick,
    children: "这个的数据是旧的",
    ...
}

此时,onClick 已经被赋值为了 prevProps.onClick 了,之后都不会再有任何改变。

prevProps.onClick 又是个什么样的函数呢?是这个样子的:

() => { submit(props.value) }

在第一次 render 传递给 Button 时,props.value 值为 "one",之后 Form re-render,会生成新的 onClick 函数传递给 Button,但是很遗憾,Button 内的 onClick 已经定死了,无法改变,所以总是会输出 "one"

第二次 & 第三次

第二次和第三次是一个道理,这里只说第二次。

第二次相比第一次,区别就是不是直接去执行 props.onClick,而是每次都包一个新的箭头函数,在每一次执行的时候都会去获取一个新的 this.props.onClick,这就是一切的关键,虽然 shouldComponentUpdate 为 false,但是新的 props 还是已经来了,可以通过 this.props 引用。

思考

这段代码对我们有什么启发吗?

文章中作者说可以写一个 PureComponentMinusHandlers 高阶组件,作用类似高阶组件,但是对类似 onClick 的 props 的更新函数不会触发 update(因为它们基本也不会变化),而只对数据类的 props 的变化进行 PureComponent 的 shallowCompare,这是一种 react 的优化方法。

通过之前的分析还可以玩出下面的花样: 主动拉取更新的子组件来进行性能优化:像上例中的第二种和第三种方法,将子组件的 shouldComponentUpdate 返回 false,然后在传入的 props 的 handler 外面包一层匿名函数,这样每次调用 handler 都会去访问最新的 this.props.handler 等“非计划更新的 props”(函数的 props),这些函数的 props 可以返回父组件的一些内部状态传递给子组件。如此一来,子组件就从单向状态流变成了子组件向父组件主动拉取。但这与 React 的单向数据理念相左,是属奇技淫巧。

wd2010 commented 6 years ago

写了这么久react发现有这样的行为,但大神,我对你后面的思考方面的知识不是很理解,可以看个具体的例子吗?

wd2010 commented 6 years ago

@fi3ework

fi3ework commented 6 years ago

之前写的比较乱,又整理了一下哈