作者:hongfeng(团队成员)
React 16.8推出了react hook,使得开发人员可以在使用函数式组件(function component),通过函数式写法,可以让我们不用再去写生命周期函数。对于function component的一些基本用法相信大家都看看官方文档也就会了,有以下几个问题值得思考:
1、函数式组件和类组件对props的取值渲染时机是一样的吗?
2、useEffect(fn,[])等同于componentDidMount吗?
3、函数式组件如何实现setState回调函数?
问题一:答案是否定的。无论在函数式组件还是类组件中,父子组件之间传值都会用到props,大多数时候我们在使用的时候也没有什么问题,但是加上异步后,情况就变得不一样了,考虑下面一种情况:
通过父组件App包含一个改变name值的方法,并且将该值传给了子组件FTest
import React from "react";
import ReactDOM from "react-dom";
import FTest from "./funcTest";
class App extends React.Component {
state = {
name: "aa"
};
render() {
return (
<>
<p
onClick={() => {
this.setState({
name: "bb"
});
}}
>
{this.state.name}
</p>
<FTest name={this.state.name} />
</>
);
}
}
子组件FTest是函数式组件,包含了一个打印name值的异步方法:
import React from "react";
export default props => {
const showName = () => {
setTimeout(() => {
console.log(props.name);
}, 3000);
};
return <button onClick={showName}>showName</button>;
};
此时我们先点击按钮触发showName方法,然后接着马上点击aa改变name值,我们会发现3s后打印出的值是 aa
我们把FTest改成类组件写法:
import React from "react";
export default class extends React.Component {
showName() {
setTimeout(() => {
console.log(this.props.name);
}, 3000);
}
render() {
return <button onClick={this.showName}>showName</button>;
}
}
执行同样的操作,会发现打印出的值是 bb
两种写法拿到的值是不一样的,函数组件从props.name中取值,类组件从this.props.name中取值,虽然在react中props是不可变的,但是this却是可变的,以保证在渲染函数和生命周期函数中可以拿到最新的值,子组件通过this拿到的始终是最新的props。但函数式组件却不一样,拿到的始终是组件渲染时的props值,如同闭包产生的效果。
问题二:两者也是有差异的。在很多时候我们对类组件进行函数改造的时候会发现,经常会对componentDidMount进行useEffect(fn,[])的改写,往往初次渲染需要的数据都在这里处理,大多数时候两者都能实现相同的功能。但是加上异步之后,情况也会所有不同,考虑下面这种情况:
import React, { useEffect } from "react";
export default () => {
setTimeout(() => {
console.log("default_async");
});
console.log("default");
useEffect(() => {
setTimeout(() => {
console.log("useEffect_async");
});
console.log("useEffect");
}, []);
return <div>async test</div>;
};
打印结果如下: default default_async useEffect useEffect_async
换成componentDidMount写法如下:
import React from "react";
export default class extends React.Component {
constructor(props) {
super();
console.log("default");
setTimeout(() => {
console.log("defalut_async");
});
}
componentDidMount() {
setTimeout(() => {
console.log("componentDidMount_async");
});
console.log("componentDidMount");
}
render() {
return <div>async test</div>;
}
}
打印结果如下: default componentDidMount defalut_async componentDidMount_async
可以发现两者在异步中的表现是不一样的,在函数式组件中,useEffect中fn始终在默认执行代码之后执行,而在类组件中,会出现componentDidMount的代码在constructor代码前执行的情况,此时异步会对执行顺序起主导作用(上面的例子中是在constructor中执行默认的打印结果,在componentWillMount中执行结果也是一样的)。
导致这种结果的原因是useEffect的执行就是异步的,但是componentDidMount却不是,在componentWillMount中执行的异步操作会在componentDidMount的同步操作之后执行。因此在初次渲染的时候遇到异步相关的问题需要额外关注。以上情况只发生在异步操作延迟相同的时候:当给异步操作一个比较大的延迟时这种顺序会发生变化,不能保证执行顺序严格按照上面说的那样。
问题三:函数式组件如何实现setState回调函数这个问题在对类组件进行函数改造的时候往往会遇到
由于在类组件中,setState往往在生命周期或者回调事件中执行,此时setState是异步的,保证获取最新的数据需要通过回调函数。在函数式组件中,useState的方法是不支持回调的,但由于useEffect是可以多次使用的, 因此可以通过将state中的数据(例如name)作为一个useEffect的依赖useEffect(fn,[name])来实现,需要注意fn中不能再去改变name,否则会导致重复循环渲染的问题,这种方式在实现自定义hook的时候也会经常用到