对不起,我抛弃了Vue,转入React阵营。不因为其它,就因为在我这边使用React的工资比使用Vue的工资高。
在六月前,我硬背了几百道的React面试题,入职一家使用React的公司,薪资增幅120%;
入职就马上进入开发阶段,完全是从零开始,随着时间的推移,发现React入门也不是网传的那么难。难道是自己天生就适合吃这碗饭…………
到今天已经六个月了,在这里想把这段时间的收获跟掘友们分享一下,请掘友们多多指教,一起进步哈。
另外我是用React16.8来入门,故在开发过程中大多使用函数式组件和React Hooks来开发。
重磅提醒,文末有抽奖噢。
一、关于函数式组件的收获
函数式组件可以理解为一个能返回React元素的函数,其接收一个代表组件属性的参数props
。
在React16.8之前,也就是没有React Hooks之前,函数式组件只作为UI组件,其输出完全由参数props
控制,没有自身的状态没有业务逻辑代码,是一个纯函数。函数式组件没有实例,没有生命周期,称为无状态组件。
在React Hooks出现后,可以用Hook赋予函数式组件状态和生命周期,于是函数式组件也可以作为业务组件。
开发过程中,类组件和函数式组件都有使用,经过六个月的开发,感觉还是函数式组件比类组件好用一些,感受最深的是以下两点:
- 不用去学习class,不用去管烦人的this指向问题;
- 复用性高,很容易就把共同的抽取出来,写出自定义Hook,来替代高阶组件。
函数式组件和类组件之间有一个非常重要的区别:函数式组件捕获了渲染所使用的值。
我是遇到一个BUG才知道有这个区别,在解决这个BUG的过程理解了这个区别的含义。
那个BUG的场景是这样的,一个输入框,输入完内容,点击按钮搜索,搜索时先请求一个接口,获取一个类型,再用类型和输入框值去请求搜索接口。用代码简单描述一下。
import React, { Component } from "react";
import * as API from 'api/list';
class SearchComponent extends Component {
constructor() {
super();
this.state = {
inpValue: ""
};
}
getType () {
const param = {
val:this.state.inpValue
}
return API.getType(param);
}
getList(type){
const param = {
val:this.state.inpValue,
type,
}
return API.getList(param);
}
async handleSearch() {
const res = await this.getType();
const type = res?.data?.type;
const res1 = await this.getList(type);
console.log(res1);
}
render() {
return (
<div>
<input
type="text"
value={this.state.inpValue}
onChange={(e) => {
this.setState({ inpValue: e.target.value });
}}
/>
<button
onClick={() => {
this.handleSearch();
}}
>
搜索
</button>
</div>
);
}
}
export default SearchComponent;
以上代码逻辑看上去都没什么毛病,但是QA给我挑了一个BUG,在输入框输入要搜索的内容后,点击搜索按钮开始搜索,然后很快在输入框中又输入内容,结果搜索接口getList
报错。查一下原因,发现是获取类型接口getType
和搜索接口getList
接受的参数val
不一致。
在排查过程中,我非常纳闷,明明两次请求中val
都是读取this.state.inpValue
的值。当时同事指导我改成函数式组件就可解决这个BUG。
import React, { useState } from "react";
import * as API from 'api/list';
export const SearchComponent = () =>{
const [inpValue,setInpValue] = useState('');
const getType = () =>{
const param = {
val:inpValue
}
return API.getType(param);
}
const getList = (type) =>{
const param = {
val:inpValue,
type:type,
}
return API.getList(param);
}
const handleSearch = async ()=>{
const res = await getType();
const type = res?.data?.type;
const res1 = await getList(type);
console.log(res1);
}
return (
<div>
<input
type="text"
value={inpValue}
onChange={(e) => {
setInpValue(e.target.value);
}}
/>
<button
onClick={() => {
handleSearch();
}}
>
搜索
</button>
</div>
);
}
export default SearchComponent;
改成函数式组件后,再试一下,不报错了,BUG修复了。后面我查阅资料后才知道在函数式组件中的事件的state和props所获取的值是事件触发那一刻页面渲染所用的state和props的值。当点击搜索按钮后,val
的值就是那一刻输入框中的值,无论输入框后面的值在怎么改变,不会捕获最新的值。
那为啥类组件中,能获取到最新的state值呢?关键在于类组件中是通过this
去获取state的,而this
永远是最新的组件实例。
其实在类组件中改一下,也可以解决这个BUG,改动的地方如下所示:
getType (val) {
const param = {
val,
}
return API.getType(param);
}
getList(val,type){
const param = {
val,
type,
}
return API.getList(param);
}
async handleSearch() {
const inpValue = this.state.inpValue;
const res = await this.getType(inpValue);
const type = res?.data?.type;
const res1 = await this.getList(inpValue,type);
console.log(res1);
}
在搜索事件handleSearch
触发时,就把输入框的值this.state.inpValue
存在inpValue
变量中,后续执行的事件用到输入框的值都去inpValue
变量取,后续再往输入框输入内容也不会影响到inpValue
变量的值,除非再次触发搜索事件handleSearch
。这样修改也可以解决这个BUG。
二、关于受控和非受控组件的收获
在React中正确理解受控和非受控组件的概念和作用至关重要,因为太多地方用到了。
在Vue中有v-model
指令可以很轻松把组件和数据关联起来,而在React中没有这种指令,那怎么让组件和数据联系起来,这时候就要用到受控组件的概念。
受控组件,我理解为组件的状态由数据来控制,改变这个数据的方法却不是组件的,这里所说的组件不仅仅是组件,也可以表示一个原生DOM。比如一个input输入框。
input输入框的状态(输入框的值)由数据value
控制,改变数据value
的方法setValue
不是input输入框自身的。
import React, { useState } from "react";
const Input = () =>{
const [value,setValue] = useState('');
return (
<div>
<input
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</div>
);
}
export default Input;
再比如在Ant Design UI中的Form组件自定义表单控件时,要求自定义表单控件接受属性value
和onChange
,其中value
来作为自定义表单控件的值,onChange
事件来改变属性value
。
import React from 'react';
import { Form, Input } from 'antd';
const MyInput = (props) => {
const { value, onChange } = props;
const onNameChange = (e) => {
onChange?.(e.target.value)
}
return (
<Input
type="text"
value={value || ''}
onChange={onNameChange}
/>
)
}
const MyForm = () => {
const onValuesChange = (values) => {
console.log(values);
};
return (
<Form
name="demoFrom"
layout="inline"
onValuesChange={onValuesChange}
>
<Form.Item name="name">
<MyInput />
</Form.Item>
</Form>
)
}
export default MyForm;
我认为受控组件最大的作用是在第三方组件的状态经常莫名奇妙的改变时,用一个父组件将其包裹起来,传入一个要控制的状态和改变状态方法的props,把组件的状态提到父组件来控制,这样当组件的状态改变时就很清晰的知道哪个地方改变了组件的状态。
非受控组件就是组件自身状态完全由自身来控制,是相对某个组件而言,比如input输入框受Input组件控制,而Input组件不受Demo组件控制,那么Input相对Demo组件是非受控组件。
import React, { useState } from "react";
const Input = () =>{
const [value,setValue] = useState('');
return (
<div>
<input
type="text"
value={value}
onChange={(e) => {
setValue(e.target.value);
}}
/>
</div>
);
}
const Demo = () =>{
return (
<Input/>
)
}
export default Demo;
也可以把非受控组件理解为组件的值只能由用户设置,而不能通过代码控制。此外要记住一个非常特殊的DOM元素<input type="file" />
,其无法通过代码设置所上传的文件。
三、关于useState的收获
useState
可谓是使用频率最高的一个Hook,下面分享一下使用的3个心得。
useState
可以接收一个函数作为参数,把这个函数称为state初始化函数
一般场景下useState
只要传入一个值就可以作为这个state的初始值,但是如果这个值要通过一定计算才能得出的呢?那么此时就可以传入一个函数,在函数中计算完成后,返回一个初始值。
import React, { useState } from 'react';
export default (props) => {
const { a } = props;
const [b] = useState(() => {
return a + 1;
})
return (
<div>{b}</div>
)
};
state更新如何使用旧的state
刚开始时,我这么使用的
const [a , setA] = useState(1);
const changeA = () =>{
setA(a + 1);
}
后来遇到一个错误,代码如下
const [a , setA] = useState(1);
const changeA = () =>{
setA(a + 1);
setA(a + 1);
}
在函数中连续调用两次setA
会发现a
还是等于2,并不是等于3。正确要这么调用
const [a , setA] = useState(1);
const changeA = () =>{
setA( a => a+1 );
setA( a => a+1 );
}
如何拆分state
在函数式组件中,只要一个state改变都会引起组件的重新渲染。而在React中只要父组件重新渲染,其子组件都会被重新渲染。
那么问题就来了,如果把state拆分成多个,当依次改变这些state,则会多次触发组件重新渲染。
若不拆分state,改变state时,就只会触发一次组件重新渲染。但是要注意,函数式组件不像类组件那样,改变其中state中的一个数据,会自动更新state中对应的数据。要这么处理
const [data,setData] = useState({a:1,b:2,c:3});
const changeData = () =>{
setData(data =>{
...data,
a:2,
})
}
当然也不能不拆分state,这样代码复用性会大大降低。我的经验是:
四、关于useMemo和usecallback的收获
useCallback(fn,[a, b]);
useMemo(fn, [a, b]);
如上所示,useMemo
和usecallback
参数中fn
是一个函数,参数中[a,b]
的a
和b
可以是state或props。
useMemo
和usecallback
首次执行时,执行fn
后创建一个缓存并返回这个缓存,监听[a,b]
中的表示state或props的a
和b
,若值未发生变化直接返回缓存,若值发生变化则重新执行fn
再创建一个缓存并返回这个缓存。
useMemo
和usecallback
都会返回一个缓存,但是这个缓存各不相同,useMemo
返回一个值,这个值可以认为是执行fn
返回的。usecallback
返回一个函数,这个函数可以认为是fn
。那么注意了传给useMemo
的fn
必须返回一个值。
用React.memo
包裹一个组件,当组件的props发生改变时才会重新渲染。
若包裹的组件是个函数式组件,在其中拥有useState
、useReducer
、useContext
的 Hook,当state 或context发生变化时,它仍会重新渲染,不过这些影响不大,使用React.memo
的主要目的是控制父组件更新迫使子组件也更新的问题。
props值的类型可以为String、Boolean、Null、undefined、Number、Symbol、Object、Array、Function,简单的来说就是基础类型和引用类型。
两个值相等的基础类型的数据用==
比较返回true
,那两个值相等的引用类型的数据用==
比较返回false
,不信试一试以下代码,看是不是都为false
。
console.log({a:1} == {a:1});
console.log([1] == [1]);
const fn1 = () =>{console.log(1)};
const fn2 = () =>{console.log(1)};
console.log(fn1 == fn2);
正因为如此,当props的值为引用类型时,且这个值是通过函数计算出来的,用useMemo
或usecallback
来处理一下,避免计算出来的值相等,但是比较却不相等,导致组件更新。我认为usecallback
是专门为处理props值为函数而创建的Hook。
在下面的例子,List组件是一个渲染开销很大的组件,它有两个属性,其中data
属性是渲染列表的数据源,是一个数组,onClick
属性是一个函数。在Demo组件中引入List组件,用useMome
处理data
属性值,用useCallback
处理onClick
属性值,使得List组件是否重新渲染只受Demo组件的data
这个state控制。
List组件:
import React from 'react';
const List = (props) => {
const { onClick, data } = props;
//...
return (
<>
{data.map(item => {
return (
<div onClick={() =>{onClick(item)}} key={item.id}>{item.content}</div>
)
})}
</>
)
}
export default React.memo(List);
Demo组件:
import
React,
{ useState, useCallback, useMemo }
from 'react';
import List from './List';
const Demo = () => {
//...
const [data, setData] = useState([]);
const listData = useMemo(() => {
return data.filter(item => {
//...
})
}, [data])
const onClick = useCallback((item) => {
console.log(item)
}, []);
return (
<div>
<List onClick={onClick} data={listData} />
</div>
)
}
export default Demo;
可见useMemo
和usecallback
作为一个性能优化手段,可以在一点程度上解决React父组件更新,其子组件也被迫更新的性能问题。
假设List组件不用React.memo
包裹,还能用useMemo
或usecallback
来优化吗?先来看一段代码,也可以这么使用组件。
import React, { useState } from 'react';
import List from './List';
export default function Demo() {
//...
const [data, setData] = useState([]);
const list = () => {
const listData = data.filter(item => {
//...
})
const onClick = (item) => {
console.log(item)
}
return (
<List onClick={onClick} data={listData} />
)
}
return (
<div>
{list()}
</div>
)
}
其中list
返回一个React元素,是一个值,那么是不是可以用useMemo
来处理一下。
import React, { useState, useMemo } from 'react';
import List from './List';
export default function Demo() {
//...
const [data, setData] = useState([]);
const listMemo = useMemo(() => {
const listData = data.filter(item => {
//...
})
const onClick = (item) => {
console.log(item)
}
return (
<List onClick={onClick} data={listData} />
)
}, [data])
return (
<div>
{listMemo}
</div>
)
}
listMemo
这个值(React元素)是否重新生成只受Demo组件的data
这个state控制。这样不是相当List组件是否重新渲染只受Demo组件的data
这个state控制。
不能认为“不管什么情况,只要用useMemo
或useCallback
处理一下,就能远离性能的问题”
要认识到useMemo
和useCallback
也有一定的计算开销,例如useMemo
会缓存一些值,在后续重新渲染,将依赖数组中的值取出来和上一次记录的值进行比较,如果不相等才会重新执行回调函数,否则直接返回缓存的值。这个过程有一定的计算开销。
所以在使用useMemo
和useCallback
前一定要考虑清楚使用场景,不能滥用。
下面总结一下要使用useMemo
和useCallback
的场景:
一个渲染开销很大的组件的属性是一个函数时,用useCallback
处理一下这个属性值。
得到一个值,有很高得计算开销,用useMemo
处理一下。
一个通过计算得到的引用类型的值,且这个值被赋值给一个渲染开销很大的组件的属性,用useMemo
处理一下。
自定义Hook中暴露出的引用类型的值,用useMemo
处理一下。
总之一句话使用useMemo
和useCallback
是为了保持引用相等和避免重复成本很高的计算。
五、关于useEffect和useLayoutEffect的收获
useEffect(fn);
useLayoutEffect(fn);
useEffect
可谓是使用频率第二高的一个Hook,useLayoutEffect
使用频率比较低。下面介绍四个使用心得:
在使用useEffect
和useLayoutEffect
之前,要搞明白传入这个两个Hook的函数会在什么时候执行。
传入useEffect
的函数是在React完成对DOM的更改,浏览器把更改后的DOM渲染出来后执行的。
传入useLayoutEffect
的函数是在React完成对DOM的更改后,浏览器把更改后的DOM渲染出来之前执行的。
所以useEffect
不会阻碍页面渲染,useLayoutEffect
会阻碍页面渲染,但是如果要在渲染前获取DOM元素的属性做一些修改,useLayoutEffect
是一个很好的选择。
组件初始化和每次更新时都会重新执行传入useEffect
和useLayoutEffect
的函数。那只想在组件初始化时执行一次呢?相当Vue中的mounted
,这样实现:
useEffect(fn,[]);
useLayoutEffect(fn,[]);
上面介绍在useEffect
在第二参数传入一个空数组[]
相当Vue中的mounted
,那么在Vue的mounted
中经常会用addEventListener
监听事件,然后在beforeDestory
中用removeEventListener
移除事件监听。那用useEffect
实现一下。
useEffect(() => {
window.addEventListener('keypress', handleKeypress, false);
return () => {
window.removeEventListener('keypress', handleKeypress, false);
};
},[])
上面用来监听键盘回事事件,但是你会发现一个很奇怪的现象,有些页面回车后会执行handleKeypress
方法,有些页面回车后执行几次handleKeypress
方法后,就不再执行了。
这是因为一个useEffect
执行前会执行上一个useEffect
的传入函数的返回函数,这个返回函数可以用来解除绑定,取消请求,防止引起内存泄露。
此外组件卸载时,也会执行useEffect
的传入函数的返回函数。
示意如下代码所示:
window.addEventListener('keypress', handleKeypress, false); // 运行第一个effect
window.removeEventListener('keypress', handleKeypress, false);// 清除上一个effect
window.addEventListener('keypress', handleKeypress, false); // 运行下一个 effect
window.removeEventListener('keypress', handleKeypress, false); // 清除上一个effect
window.addEventListener('keypress', handleKeypress, false); // 运行下一个effect
window.removeEventListener('keypress', handleKeypress, false); // 清除最后一个effect
所以要解决上面的BUG,只要把useEffect
的第二参数去掉即可。这点跟Vue中的mounted
不一样。
在useEffect
和useLayoutEffect
的第二参数是个数组,称为依赖,当依赖改变时候会执行传入的函数。
比如监听 a
这个state 和 b
这个prop,这样实现:
useEffect(fn,[a,b]);
useLayoutEffect(fn,[a,b]);
但是要注意不要一次性监听过多的state或prop,也就是useEffect
的依赖过多,如果过多要去优化它,否则会导致这个useEffect
难以维护。
我是这样来优化的:
- 考虑该依赖是否必要,不必要去掉它。
- 将
useEffect
拆分为更小的单元,每个useEffect
依赖于各自的依赖数组。
- 通过合并依赖中的相关state,将多个state聚合为一个state。
六、结语
以上五点就是我这六个月中印象最深的收获,有踩过的坑,有写被leader吐槽的代码。不过最大的收获还是薪资涨幅120%,哈哈哈,各位掘友要勇于跳出自己的舒适区,才能有更丰厚的收获。
虽然以上的收获在某些掘友们眼里会觉得比较简单,但是对于刚转入React的掘友们这些知识的使用频率非常高,要多多琢磨。如果有错误欢迎在评论中指出,或者掘友有更好的收获也在评论中分享一下。