面试题
面试官:说一说react函数组件和类组件的区别?
我:······
先来看看正确解答吧
相同点:
- 都可以接收props返回react元素
不同点:
-
编程思想:类组件需要创建实例,面向对象,函数组件不需要创建实例,接收输入,返回输出,函数式编程
-
内存占用:类组建需要创建并保存实例,占用一定的内存
-
值捕获特性:函数组件具有值捕获的特性
-
可测试性:函数组件方便测试
-
状态:类组件有自己的状态,函数组件没有只能通过useState
-
生命周期:类组件有完整生命周期,函数组件没有可以使用useEffect实现类似的生命周期
-
逻辑复用:类组件继承 Hoc(逻辑混乱 嵌套),组合优于继承,函数组件hook逻辑复用
-
跳过更新:shouldComponentUpdate PureComponent,React.memo
-
发展未来:函数组件将成为主流,屏蔽this、规范、复用,适合时间分片和渲染
其实:函数式组件捕获了渲染时所使用的值,这是两类组件最大的不同。
函数组件值的捕获
下面我们重点说一下函数组件的值捕获的特性,先看看下面的两个代码,他们的打印结果有什么区别?
类组件:
import React from 'react';
export default class Home extends React.Component {
state = {
num: 0,
};
click = () => {
setTimeout(() => {
console.log('类', this.state.num);
}, 3000);
this.setState({num: this.state.num + 1});
};
render() {
return <div onClick={this.click}>click {this.state.num}</div>;
}
}
函数组件:
import React, {useState} from 'react';
export default function Home() {
const [num, setNum] = useState(0);
const click = () => {
setTimeout(() => {
console.log('函数', num);
}, 3000);
setNum(num + 1);
};
return <div onClick={click}>click {num}</div>;
}
来看看控制台的打印吧,我们先来看看函数组件 :
我们可以看到,当我们点击过了3s后控制台依次打印了1,2,3,如果我们在3s内快速的点击3下来试试呢?
其实界面的值也是随着我们每次点击改变的,但是打印的结果确实最后依次改变的那个值作为结果进行打印,其实这也很好理解class组件总是会通过this拿到最新的值(props、state)。那我们来看看函数组件呢?
我们先慢慢点击:
我们可以看到,函数组件虽然界面的值已经发生了改变,可是打印的结果却还是上一次的。
然后我们在3s内快速点击3次,结果也是这样的。
这是为什么呢?为什么函数组件的值是上一次的呢?看上去有点问题,可是事实真的如此吗?其实这个是react函数组件的一个特性。
我们都知道,React 框架有一个经典的公式是 UI = f(data),React框架做的本质工作就是吃入数据,吐出UI,把声明式的代码转换为命令式的 DOM 操作,把数据层面的描述映射到用户可见的 UI 变化中去。这也就是说React的数据应该紧紧的和渲染绑定在一起,但是问题的关键就在于类组件是做不到这一点的。
我们采用 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>;
}
}
这个组件返回的是一个按钮,点击之后延迟三秒,页面弹出 ‘Followed XXX‘的文案。 看上去好像是没有什么问题,但是在sandBox例子中,如果你在dan用户下点击 follow按钮,并且在三秒内把用户切换到 Sophie, 最终弹出的提示框会变成 ‘Followed Sophie’,这明显很不合理。
所以看到这里,你似乎明白了为什么及时我们改变了num界面的num也改变了,然后打印的结果却没有改变。我们可以确保函数执行的瞬间的值是我们最初捕获到的,而不是我们后面改变的,当一些值的改变比如props的改变导致函数的重新执行也不会影响到我们上一次执行的结果。这就是 Dan 所说的函数式组件捕获了渲染所使用的值,并且我们还能进一步意识到:函数组件真正将数据和渲染紧紧的绑定到一起了。
这里有个小Tips,很多人认为在函数组件中延迟输出的 state 是调用时的 state,而不是最新的 state 是一个Bug,恰恰相反,这是一个函数式组件的特性,是真正践行了React设计理念的正确方式。 Hooks也给出了获取最新的props和state的方法,就是 useRef,详细用法我不再赘叙,大家有兴趣可以自己去查阅
解决上面例子的问题
如果我们确实想在setTimeout中输出最新改变过后的值怎么办呢?正如上面所言:我们可以用useRef。
import React, {useState, useRef, useEffect} from 'react';
export default function Home() {
const [num, setNum] = useState(0);
const latestNum = useRef('')
useEffect(() => {
latestNum.current = num
})
const click = () => {
setTimeout(() => {
console.log('函数ref', latestNum.current);
}, 3000);
setNum(num + 1);
latestNum.current = num;
};
return <div onClick={() => click()}>click {num}</div>;
}
这样我们就可以捕获到最后的值了
拓展问题
import React, {useState} from 'react';
export default function Home() {
const [num, setNum] = useState(0);
const click = () => {
setNum(num + 1);
console.log(num);
};
return <div onClick={() => click()}>click {num}</div>;
}
在这种场景中,我们虽然改变了num的值,但是因为值的捕获问题,我们打印的结果还是"上一次的"。但是我们确实又想拿到现在的值怎么办呢?
我们可以这样做:
export default function Home() {
const [num, setNum] = useState(0);
const click = useSyncCallback(() => {
setNum(num + 1);
fn();
})
const fn = useSyncCallback(() => {
console.log(num);
})
return <div onClick={() => click()}>click {num}</div>;
}
接下来我们实现useSyncCallback方法
import {useEffect, useState, useCallback} from 'react';
const useSyncCallback = callback => {
const [proxyState, setProxyState] = useState({current: false});
const Func = useCallback(() => {
setProxyState({current: true});
}, [proxyState]);
useEffect(() => {
if (proxyState.current === true) setProxyState({current: false});
}, [proxyState]);
useEffect(() => {
proxyState.current && callback();
});
return Func;
};
export default useSyncCallback;
那么问题就可以解决了。