又是个class和fn组件心智模型上的区别

749 阅读9分钟

性能主要取决于代码的作用,而不是选择函数式还是类式

本篇文章不是对函数式或者类式组件的价值判断,只是在阐述React中这两种编程模型之间的区别,关于更多的函数式问题,请查看Hooks FAQ

函数式组件捕捉了渲染所用的值


思考以下组件

const FnCpm: FC<Props> = (props) => {
  const myAlert = () => {
    alert("look here!" + props.user);
  };
  const handler = () => {
    setTimeout(myAlert, 3000);
  };
  return <button onClick={handler}>function Click</button>;
};

它渲染了一个利用setTimeout来模拟网络请求,然后显示一个确认警告的按钮。例如,如果props.userQsj ,它会在三秒后显示 look here! Qsj ,非常简单

如果是一个class component

class ClassComponent extends Component<Props> {
  myAlert = () => {
    alert("look here!" + this.props.user);
  };
  handler = () => {
    setTimeout(this.myAlert, 3000);
  };
  render() {
    return <button onClick={this.handler}>Class Click</button>;
  }
}

通常我们认为,这两段代码是等价的,人们经常在这两种模式中自由的重构代码,但是很少注意到他们的含义

然而这两个代码片段还是有略微的不同,仔细的看看他们。

尝试按照以下顺序分别使用这两个按钮

  1. 点击 其中某一个按钮
  2. 在三秒内切换选中的select
  3. 查看弹出的文本

你将看到一个奇特的区别

  • 当使用function component ,当前user张三 时点击按钮后,马上切换到李四 ,弹出的文本将依旧是look here 张
  • 当使用class component ,当前user李四时点击按钮,马上切换到张三,弹出的确实look here 张三

funcClass.gif


在这个例子中,第一个行为是正确的。如果我关注一个人,然后切换到另外一个人的账号,我的组件不应该混淆我关注了谁。在这里,class组件的实现很明显是错误的


why?

class ClassComponent extends Component<Props> {
  myAlert = () => {
    alert("look here!" + this.props.user) // look here
  };

这个class方法从this.props.user 中读取数据。在reactprops是不可变的(immutable)的,所以他们永远不会改变。然而,this是,而且永远是,可变的。

事实上,这就是class组件this存在的意义。**react本身会随着时间的推移而改变,以便你可以在渲染方法以及生命周期方法中得到最新的实例。**

所以如果在请求已经发出的情况下我们的组件进行了重新渲染,this.props将会改变。myAlert方法从一个“过于新”的props中得到了user

这暴露了一个关于用户界面性质的一个有趣观察。如果我们说ui在概念上是当前应用状态的一个函数,那么事件处理程序是渲染结果的一部分———就像视觉输出一样。我们的事件处理程序“属于”一个拥有特定propsstate的特定渲染。

然而,调用一个回调函数读取this.propstimeout会打断这种关联。我们的myAlert函数并没有与任何一个特定的渲染“绑定”在一起,所以它失去了正确的props。从this中读取数据这种行为,切断了这种联系


让我们假设函数组件不存在。我们该如何解决类组件这个问题?

我们想要从某种关系“修复”拥有正确的props的渲染与读取这些propsmyAlert回调之间的关系。在某个地方props会被弄丢。

一种方法是在调用事件之前读取this.props,然后将它们显示的传递给timeout回调函数中去:

class ClassComponent extends Component<Props> {
  myAlert = (user: string) => {
    alert("look here!" + user);
  };
  handler = () => {
    const { user } = this.props;
    setTimeout(() => this.myAlert(user), 3000);
  };
}

这种方法会起作用。然而,这种方法使得代码明显变的更加冗长,并且随着时间推移容易出错。如果我们需要的不止是一个props怎么办?如果我们还需要访问一个state怎么办?如果myAlert中调用了另一个方法,然后那个方法读取了this.props.something或者this.state.something,我们又将会遇到同样的问题,然后我们不得不将this.propsthis.state以函数参数的形式传递下去。

这样破坏了class提供的工程学。同时这也很难让人去记住传递的变量,这就是为什么人们总是在解决bug。

同样的,在handler中内联alert代码也无法解决问题,我们希望以允许将拆分多个方法的方式去构造组织代码,但同时也能读取与某次组件调用形成的渲染结果对应的propsstate。这种问题并不是react所独有的————你可以在任何一个将数据放入类似this这样的可变对象中的ui库中重现它。

我们面对的问题是我们从this.props中读取数据太迟了——读取时已经不是我们所需要使用的上下文了!然而如果我们利用javascript函数中的闭包问题将迎刃而解

class ClassComponent extends Component<Props> {
  render() {
    const { user } = this.props;
    const myAlert = (user: string) => {
      alert("look here!" + user);
    };
    const handler = () => {
      setTimeout(() => myAlert(user), 3000);
    };
    return <button onClick={handler}>Class Click</button>;
  }
}

通常来说我们会避免使用闭包,因为它会让我们难以想象一个可能会随着时间推移而变化的变量。(但是在react中。propsstate是不可变的)或者说,react强烈推荐是不可变的。这就消除了闭包的主要缺陷

这就意味着如果你在一次特定的渲染中捕捉那一次渲染所用的props或者state,你会发现她们总是保持一致,就如同你预期的那样

你在渲染的时候就已经”捕捉“了props

https://overreacted.io/fa483dd5699aac1350c57591770a49be/pokemon.gif

这样,在它内部的任何代码(包括myAlert)都保证可以得到这一次特定渲染所使用的propsreact再也不会动我们的奶酪

用闭包是正确的,但是看起来很奇怪,如果你在render方法中定义各种函数,而不是使用class的方法,那么class的意义又在哪里?

事实上,我们可以通过删除class的”包裹“来简化代码:

const FnCpm: FC<Props> = (props) => {
  const myAlert = () => {
    alert("look here!" + props.user);
  };
  const handler = () => {
    setTimeout(myAlert, 3000);
  };
  return <button onClick={handler}>function Click</button>;
};

就像上面这样,props仍旧被捕获—— react会将它们作为参数传递。不同于thisprops对象本身永远都不会被react改变

如果你在函数定义中解构props,那更加明显

const FnCpm: FC<Props> = ({ user }) => {
  const myAlert = () => {
    alert("look here!" + user);
  };
};

当父组件使用不同的props来渲染function component时,react会再次调用functiom component,但是我们点击的事件处理函数,”属于“具有自己的user值的上一次渲染,并且myAlert回调函数也会读取到这个值,它们都会完好无损。

这就是为什么在function component中,当前user为张三,点击后改变为李四,三秒后alert仍然会弹出张三的原因,这个行为是正确的

现在明白了reactfunction componentclass component之间的区别:

函数式组件捕捉了渲染所使用的值

使用hooks,同样的原则也适用于state

function MessageThread() {
  const [message, setMessage] = useState("");

  const showMessage = () => {
    alert("You said: " + message);
  };

  const handleSendClick = () => {
    setTimeout(showMessage, 3000);
  };

  const handleMessageChange = (e: any) => {
    setMessage(e.target.value);
  };

  return (
    <>
      <input value={message} onChange={handleMessageChange} />
      <button onClick={handleSendClick}>Send</button>
    </>
  );
}

尽管这不是一个非常好的fn component,但它说明了同样的观点:如果我发送一条特定的消息,组件不应该对实际发送的是哪条消息感到困惑。这个函数组件的message变量捕获了“属于”返回了被浏览器调用的单击处理函数的那一次渲染。所以但我点击“发送”的时候,message被设置为那一刻在input输入的内容


因此我们知道,在默认情况下react中的函数会捕获propsstate,但是如果我们不想要读取并不属于这一次特定渲染的,最新的propsstate呢?如果我们想要从未来读取它们呢?

class中,你通过读取this.props或者this.state来实现,因为this本身就是可变的,react改变了它,在fn component中,你也可以拥有一个在所有的渲染帧中共享的可变变量,它被称为“ref

function MyComponent() {
  const ref = useRef(null);
}

但是,你必须自己管理它

你可能知道“dom ref”,但是ref在概念上更为广泛通用。它只是一个你可以放东西进去的盒子。

甚至在视觉上,this.something就像是something.current的一个镜像,它们代表同样的概念

默认情况下,react不会在fn component中为最新propsstate创造ref,在很多情况下,你并不需要它们,并且分配它们将是一种浪费,但是如果你愿意,你可以这样手动的追踪这些值:

function MessageThread() {
  const [message, setMessage] = useState("");
  const latestMessage = useRef("");
  const showMessage = () => {
    alert("You said: " + latestMessage.current); // edit
  };

  const handleMessageChange = (e: any) => {
    setMessage(e.target.value);
    latestMessage.current = e.target.value; // edit
  };
}

当我们读取latestMessage.current,我们将会得到最新的值,即使我们在按下按钮后继续输入

通常情况下,应该避免在渲染期间读取或者设置ref,因为它们是可变的,so,尽量应该保持渲染的可预测性,然而,如果我们想要特定propsstate的最新值,那么手动更新ref会有些烦人,我们可以通过effect来自动化实现它:

const latestMessage = useRef('');
useEffect(() => {
    latestMessage.current = message;
});

我们在一个effect内部执行赋值操作以便让ref的值只会在dom被更新后才会改变。这确保了我们的变量突变不会破坏依赖于中断渲染的时间切片和Suspense等特性

通常来说使用这样的ref并不是非常的必要,捕捉propsstate通常是更好的默认值。然而在处理类似于intervals命令式api时,ref会非常便利。记住你可以像这样追踪任何一个值——一个props一个state,又或者甚至是一个函数


当用函数来编写大部分react代码时,我们需要调整关于优化代码什么变量会随着时间改变的认知和直觉。

正如Fredrik所说

目前为止,我发现的有关于hooks的最好的心智模型是“写代码时要认为任何值都可以随时改变”

函数也不例外。这需要一段时间才能成为react学习资料中的常识,它需要一些从class的思维方式中进行一些调整。但我希望这篇文章能够帮助你以新的目光来看待它。