与React类组件相比,React函数式组件究竟有何不一样?html
通常的回答都是:前端
state
,那若是有 Hooks
以后呢?而下面会重点讲述:React的函数式组件和类组件之间根本的区别: 在心智模型上。react
函数式组件常常被忽略的点:函数式组件捕获了渲染所用的值。(Function components capture the rendered values.)git
思考这个组件:github
function ProfilePage(props) { const showMessage = () => alert('你好 ' + props.user); const handleClick = () => setTimeout(showMessage, 3000); return <button onClick={handleClick}>Follow</button> } 复制代码
上述组件:若是 props.user
是 Dan
,它会在三秒后显示 你好 Dan
。数组
若是是类组件咱们怎么写?一个简单的重构可能就象这样:浏览器
class ProfilePage extends React.Component { showMessage = () => alert('Followed ' + this.props.user); handleClick = () => setTimeout(this.showMessage, 3000); render() { return <button onClick={this.handleClick}>Follow</button>; } } 复制代码
一般咱们认为,这两个代码片断是等效的。人们常常在这两种模式中自由的重构代码,可是不多注意到它们的含义:markdown
咱们经过 React 应用程序中的一个常见错误来讲明其中的不一样。闭包
咱们添加一个父组件,用一个下拉框来更改传递给子组件(ProfilePage
),的 props.user
,实例地址:(codesandbox.io/s/pjqnl16lm…) 。并发
按步骤完成如下操做:
这时会获得一个奇怪的结果:
ProfilePage
, 当前帐号是 Dan 时点击 Follow 按钮,而后立马切换当前帐号到 Sophie,弹出的文本将依旧是 'Followed Dan'
。ProfilePage
, 弹出的文本将是 'Followed Sophie'
:在这个例子中,函数组件是正确的。 若是我关注一我的,而后导航到另外一我的的帐号,个人组件不该该混淆我关注了谁。 ,而类组件的实现很明显是错误的。
因此为何咱们的例子中类组件会有这样的表现? 让咱们仔细看看类组件中的 showMessage
方法:
showMessage = () => { alert('Followed ' + this.props.user); }; 复制代码
这个类方法从 this.props.user
中读取数据。
this
是并且永远是 可变(mutable)的。**这也是类组件 this
存在的意义:能在渲染方法以及生命周期方法中获得最新的实例。
因此若是在请求已经发出的状况下咱们的组件进行了从新渲染, this.props
将会改变。 showMessage
方法从一个"过于新"的 props
中获得了 user
。
从 this 中读取数据的这种行为,调用一个回调函数读取 this.props
的 timeout 会让 showMessage
回调并无与任何一个特定的渲染"绑定"在一块儿,因此它"失去"了正确的 props。。
咱们想要以某种方式"修复"拥有正确 props 的渲染与读取这些 props 的 showMessage
回调之间的联系。在某个地方 props
被弄丢了。
this.props
,而后显式地传递到timeout回调函数中:class ProfilePage extends React.Component { showMessage = (user) => alert('Followed ' + user); handleClick = () => { const {user} = this.props; setTimeout(() => this.showMessage(user), 3000); }; render() { return <button onClick={this.handleClick}>Followbutton>; } } 复制代码
然而,这种方法使得代码明显变得更加冗长。若是咱们须要的不止是一个props 该怎么办? 若是咱们还须要访问state 又该怎么办? **若是 showMessage
调用了另外一个方法,而后那个方法中读取了 this.props.something
或者 this.state.something
,咱们又将遇到一样的问题。**而后咱们不得不将 this.props
和 this.state
以函数参数的形式在被 showMessage
调用的每一个方法中一路传递下去。
这样的作法破坏了类提供的工程学。同时这也很难让人去记住传递的变量或者强制执行,这也是为何人们老是在解决bugs。
这个问题能够在任何一个将数据放入相似 this
这样的可变对象中的UI库中重现它(不只只存在 React 中)
若是你在一次特定的渲染中捕获那一次渲染所用的props或者state,你会发现他们老是会保持一致,就如同你的预期那样:
class ProfilePage extends React.Component { render() { const props = this.props; const showMessage = () => { alert('Followed ' + props.user); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return <button onClick={handleClick}>Follow</button>; } } 复制代码
你在渲染的时候就已经"捕获"了props:。这样,在它内部的任何代码(包括 showMessage
)都保证能够获得这一次特定渲染所使用的props。
可是:若是你在 render
方法中定义各类函数,而不是使用class的方法,那么使用类的意义在哪里?
事实上,咱们能够经过删除类的"包裹"来简化代码:
function ProfilePage(props) { const showMessage = () => { alert('Followed ' + props.user); }; const handleClick = () => { setTimeout(showMessage, 3000); }; return ( <button onClick={handleClick}>Follow</button> ); } 复制代码
就像上面这样, props
仍旧被捕获了 —— React将它们做为参数传递。 不一样于 this
, props
对象自己永远不会被React改变。
当父组件使用不一样的props来渲染 ProfilePage
时,React会再次调用 ProfilePage
函数。可是咱们点击的事件处理函数,"属于"具备本身的 user
值的上一次渲染,而且 showMessage
回调函数也能读取到这个值。它们都保持无缺无损。
这就是为何,在上面那个的函数式版本中,点击关注帐号1,而后改变选择为帐号2,仍旧会弹出 'Followed 帐号1'
:
函数式组件捕获了渲染所使用的值。
使用Hooks,一样的原则也适用于state。 看这个例子:
function MessageThread() { const [message, setMessage] = useState(''); const showMessage = () => { alert('You said: ' + message); }; const handleSendClick = () => { setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => { setMessage(e.target.value); }; return <> <input value={message} onChange={handleMessageChange} /> <button onClick={handleSendClick}>Send</button> </>; } 复制代码
若是我发送一条特定的消息,组件不该该对实际发送的是哪条消息感到困惑。这个函数组件的 message
变量捕获了"属于"返回了被浏览器调用的单击处理函数的那一次渲染。因此当我点击"发送"时 message
被设置为那一刻在input中输入的内容。
所以咱们知道,在默认状况下React中的函数会捕获props和state。 **可是若是咱们想要读取并不属于这一次特定渲染的,最新的props和state呢?**若是咱们想要["从将来读取他们"]呢?
在类中,你经过读取 this.props
或者 this.state
来实现,由于 this
自己时可变的。React改变了它。在函数式组件中,你也能够拥有一个在全部的组件渲染帧中共享的可变变量。它被成为"ref":
function MyComponent() { const ref = useRef(null); } 复制代码
可是,你必须本身管理它。
一个ref与一个实例字段扮演一样的角色。这是进入可变的命令式的世界的后门。你可能熟悉'DOM refs',可是ref在概念上更为普遍通用。它只是一个你能够放东西进去的盒子。
甚至在视觉上, this.something
就像是 something.current
的一个镜像。他们表明了一样的概念。
默认状况下,React不会在函数式组件中为最新的props和state创造refs。在不少状况下,你并不须要它们,而且分配它们将是一种浪费。可是,若是你愿意,你能够这样手动地来追踪这些值:
function MessageThread() { const [message, setMessage] = useState(''); const latestMessage = useRef(''); const showMessage = () => { alert('You said: ' + latestMessage.current); }; const handleSendClick = () => { setTimeout(showMessage, 3000); }; const handleMessageChange = (e) => { setMessage(e.target.value); latestMessage.current = e.target.value; }; 复制代码
若是咱们在 showMessage
中读取 message
,咱们将获得在咱们按下发送按钮那一刻的信息。可是当咱们读取 latestMessage.current
,咱们将获得最新的值 —— 即便咱们在按下发送按钮后继续输入。
ref是一种"选择退出"渲染一致性的方法,在某些状况下会十分方便。
一般状况下,你应该避免在渲染期间读取或者设置refs,由于它们是可变得。咱们但愿保持渲染的可预测性。 **然而,若是咱们想要特定props或者state的最新值,那么手动更新ref会有些烦人。**咱们能够经过使用一个effect来自动化实现它:
function MessageThread() { const [message, setMessage] = useState(''); const latestMessage = useRef(''); useEffect(() => { latestMessage.current = message; }); const showMessage = () => { alert('You said: ' + latestMessage.current); }; 复制代码
咱们在一个effect 内部执行赋值操做以便让ref的值只会在DOM被更新后才会改变。这确保了咱们的变量突变不会破坏依赖于可中断渲染的时间切片和 Suspense 等特性。
一般来讲使用这样的ref并非很是地必要。 捕获props和state一般是更好的默认值。 然而,在处理相似于intervals和订阅这样的命令式API时,ref会十分便利。你能够像这样跟踪 任何值 —— 一个prop,一个state变量,整个props对象,或者甚至一个函数。
这种模式对于优化来讲也很方便 —— 例如当 useCallback
自己常常改变时。然而,使用一个reducer 一般是一个更好的解决方式
闭包帮咱们解决了很难注意到的细微问题。一样,它们也使得在并发模式下能更轻松地编写可以正确运行的代码。这是可行的,由于组件内部的逻辑在渲染它时捕获并包含了正确的props和state。
函数捕获了他们的props和state —— 所以它们的标识也一样重要。这不是一个bug,而是一个函数式组件的特性。例如,对于 useEffect
或者 useCallback
来讲,函数不该该被排除在"依赖数组"以外。(正确的解决方案一般是使用上面说过的 useReducer
或者 useRef
)
当咱们用函数来编写大部分的React代码时,咱们须要调整关于优化代码和什么变量会随着时间改变的认知与直觉。
到目前为止,我发现的有关于hooks的最好的内心规则是"写代码时要认为任何值均可以随时更改"。
React函数老是捕获他们的值 —— 如今咱们也知道这是为何了。
文章参考:React做者 Dan Abramov 的github