React Hooks 实现和由来以及解决的问题详解
与React类组件相比,React函数式组件究竟有何不同?
一般的回答都是:
- 类组件比函数式组件多了更多的特性,比如state,那如果有Hooks之后呢?
- 函数组件性能比类组件好,但是在现代浏览器中,闭包和类的原始性能只有在极端场景下才会有明显的差别。
- 性能主要取决于代码的作用,而不是选择函数式还是类组件。尽管优化策略有差别,但性能差异可以忽略不计。
- 参考官网:(https://zh-hans.reactjs.org/docs/hooks-faq.html#are-hooks-slow-because-of-creating-functions-in-render)
- 参考作者github:(https://github.com/ryardley/hooks-perf-issues/pull/2)
而下面会重点讲述:React的函数式组件和类组件之间根本的区别:在心智模型上。
简单的案例
函数式组件以来,它一直存在,但是经常被忽略:函数式组件捕获了渲染所用的值。(Functioncomponentscapturetherenderedvalues.)
思考这个组件:
functionProfilePage(props){ constshowMessage=()=>alert('你好'+props.user); consthandleClick=()=>setTimeout(showMessage,3000); returnFollow }
上述组件:如果props.user是Dan,它会在三秒后显示你好Dan。
如果是类组件我们怎么写?一个简单的重构可能就象这样:
classProfilePageextendsReact.Component{ showMessage=()=>alert('Followed'+this.props.user); handleClick=()=>setTimeout(this.showMessage,3000); render(){ returnFollow; } }
通常我们认为,这两个代码片段是等效的。人们经常在这两种模式中自由的重构代码,但是很少注意到它们的含义:
我们通过React应用程序中的一个常见错误来说明其中的不同。
我们添加一个父组件,用一个下拉框来更改传递给子组件(ProfilePage),的props.user,实例地址:(https://codesandbox.io/s/pjqnl16lm7)
按步骤完成以下操作:
- 点击其中某一个Follow按钮。
- 在3秒内切换选中的账号。
- 查看弹出的文本。
这时会得到一个奇怪的结果:
- 当使用函数式组件实现的ProfilePage,当前账号是Dan时点击Follow按钮,然后立马切换当前账号到Sophie,弹出的文本将依旧是'FollowedDan'。
- 当使用类组件实现的ProfilePage,弹出的文本将是'FollowedSophie':
在这个例子中,函数组件是正确的。如果我关注一个人,然后导航到另一个人的账号,我的组件不应该混淆我关注了谁。,而类组件的实现很明显是错误的。
案例解析
所以为什么我们的例子中类组件会有这样的表现?让我们仔细看看类组件中的showMessage方法:
showMessage=()=>{ alert('Followed'+this.props.user); };
这个类方法从this.props.user中读取数据。
在React中Props是不可变(immutable)的,所以他们永远不会改变。
而this是而且永远是可变(mutable)的。**
这也是类组件this存在的意义:能在渲染方法以及生命周期方法中得到最新的实例。
所以如果在请求已经发出的情况下我们的组件进行了重新渲染,this.props将会改变。showMessage方法从一个"过于新"的props中得到了user。
从this中读取数据的这种行为,调用一个回调函数读取this.props的timeout会让showMessage回调并没有与任何一个特定的渲染"绑定"在一起,所以它"失去"了正确的props。。
如何用类组件解决上述BUG?(假设函数式组件不存在)
我们想要以某种方式"修复"拥有正确props的渲染与读取这些props的showMessage回调之间的联系。在某个地方props被弄丢了。
方法一:在调用事件之前读取this.props,然后显式地传递到timeout回调函数中:
classProfilePageextendsReact.Component{ showMessage=(user)=>alert('Followed'+user); handleClick=()=>{ const{user}=this.props; setTimeout(()=>this.showMessage(user),3000); }; render(){ returnFollowbutton>; } }
然而,这种方法使得代码明显变得更加冗长。如果我们需要的不止是一个props该怎么办?如果我们还需要访问state又该怎么办?如果showMessage调用了另一个方法,然后那个方法中读取了this.props.something或者this.state.something,我们又将遇到同样的问题。然后我们不得不将this.props和this.state以函数参数的形式在被showMessage调用的每个方法中一路传递下去。
这样的做法破坏了类提供的工程学。同时这也很难让人去记住传递的变量或者强制执行,这也是为什么人们总是在解决bugs。
这个问题可以在任何一个将数据放入类似this这样的可变对象中的UI库中重现它(不仅只存在React中)
方法二:如果我们能利用JavaScript闭包的话问题将迎刃而解。*
如果你在一次特定的渲染中捕获那一次渲染所用的props或者state,你会发现他们总是会保持一致,就如同你的预期那样:
classProfilePageextendsReact.Component{ render(){ constprops=this.props; constshowMessage=()=>{ alert('Followed'+props.user); }; consthandleClick=()=>{ setTimeout(showMessage,3000); }; returnFollow; } }
你在渲染的时候就已经"捕获"了props:。这样,在它内部的任何代码(包括showMessage)都保证可以得到这一次特定渲染所使用的props。
Hooks的由来
但是:如果你在render方法中定义各种函数,而不是使用class的方法,那么使用类的意义在哪里?
事实上,我们可以通过删除类的"包裹"来简化代码:
functionProfilePage(props){ constshowMessage=()=>{ alert('Followed'+props.user); }; consthandleClick=()=>{ setTimeout(showMessage,3000); }; return(Follow ); }
就像上面这样,props仍旧被捕获了——React将它们作为参数传递。不同于this,props对象本身永远不会被React改变。
当父组件使用不同的props来渲染ProfilePage时,React会再次调用ProfilePage函数。但是我们点击的事件处理函数,"属于"具有自己的user值的上一次渲染,并且showMessage回调函数也能读取到这个值。它们都保持完好无损。
这就是为什么,在上面那个的函数式版本中,点击关注账号1,然后改变选择为账号2,仍旧会弹出'Followed账号1':
函数式组件捕获了渲染所使用的值。
使用Hooks,同样的原则也适用于state。看这个例子:
functionMessageThread(){ const[message,setMessage]=useState(''); constshowMessage=()=>{ alert('Yousaid:'+message); }; consthandleSendClick=()=>{ setTimeout(showMessage,3000); }; consthandleMessageChange=(e)=>{ setMessage(e.target.value); }; return<>Send >; }
如果我发送一条特定的消息,组件不应该对实际发送的是哪条消息感到困惑。这个函数组件的message变量捕获了"属于"返回了被浏览器调用的单击处理函数的那一次渲染。所以当我点击"发送"时message被设置为那一刻在input中输入的内容。
读取最新的状态
因此我们知道,在默认情况下React中的函数会捕获props和state。但是如果我们想要读取并不属于这一次特定渲染的,最新的props和state呢?如果我们想要["从未来读取他们"]呢?
在类中,你通过读取this.props或者this.state来实现,因为this本身时可变的。React改变了它。在函数式组件中,你也可以拥有一个在所有的组件渲染帧中共享的可变变量。它被成为"ref":
functionMyComponent(){ constref=useRef(null); }
但是,你必须自己管理它。
一个ref与一个实例字段扮演同样的角色。这是进入可变的命令式的世界的后门。你可能熟悉'DOMrefs',但是ref在概念上更为广泛通用。它只是一个你可以放东西进去的盒子。
甚至在视觉上,this.something就像是something.current的一个镜像。他们代表了同样的概念。
默认情况下,React不会在函数式组件中为最新的props和state创造refs。在很多情况下,你并不需要它们,并且分配它们将是一种浪费。但是,如果你愿意,你可以这样手动地来追踪这些值:
functionMessageThread(){ const[message,setMessage]=useState(''); constlatestMessage=useRef(''); constshowMessage=()=>{ alert('Yousaid:'+latestMessage.current);}; consthandleSendClick=()=>{ setTimeout(showMessage,3000); }; consthandleMessageChange=(e)=>{ setMessage(e.target.value); latestMessage.current=e.target.value;};
如果我们在showMessage中读取message,我们将得到在我们按下发送按钮那一刻的信息。但是当我们读取latestMessage.current,我们将得到最新的值——即使我们在按下发送按钮后继续输入。
ref是一种"选择退出"渲染一致性的方法,在某些情况下会十分方便。
通常情况下,你应该避免在渲染期间读取或者设置refs,因为它们是可变得。我们希望保持渲染的可预测性。然而,如果我们想要特定props或者state的最新值,那么手动更新ref会有些烦人。我们可以通过使用一个effect来自动化实现它:
functionMessageThread(){ const[message,setMessage]=useState(''); constlatestMessage=useRef(''); useEffect(()=>{ latestMessage.current=message; }); constshowMessage=()=>{ alert('Yousaid:'+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作者DanAbramov的github
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持毛票票。
声明:本文内容来源于网络,版权归原作者所有,内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎发送邮件至:czq8825#qq.com(发邮件时,请将#更换为@)进行举报,并提供相关证据,一经查实,本站将立刻删除涉嫌侵权内容。