1. 引言

为什么要了解 Function 写法的组件呢?因为它正在变得越来越重要。

那么 React 中 Function Component 与 Class Component 有何不同?

how-are-function-components-different-from-classes 这篇文章带来了一个独特的视角。

顺带一提,以后会用 Function Component 代替 Stateless Component 的说法,原因是:自从 Hooks 出现,函数式组件功能在不断丰富,函数式组件不再需要强调其无状态特性,因此叫 Function Component 更为恰当。

2. 概述

原文事先申明:并没有对 Function 与 Classes 进行优劣对比,而仅仅进行特性对比,所以不接受任何吐槽。

这两种写法没有好坏之分,性能差距也几乎可以忽略,而且 React 会长期支持这两种写法。

Capture props

对比下面两段代码。

Class Component:

class ProfilePage extends React.Component {
showMessage = () => {
alert("Followed " + this.props.user);
}; handleClick = () => {
setTimeout(this.showMessage, 3000);
}; render() {
return <button onClick={this.handleClick}>Follow</button>;
}
}

Function Component:

function ProfilePage(props) {
const showMessage = () => {
alert("Followed " + props.user);
}; const handleClick = () => {
setTimeout(showMessage, 3000);
}; return <button onClick={handleClick}>Follow</button>;
}

在线 Demo

这两个组件都描述了同一个逻辑:点击按钮 3 秒后 alert 父级传入的用户名。

如下父级组件的调用方式:

<ProfilePageFunction user={this.state.user} />
<ProfilePageClass user={this.state.user} />

那么当点击按钮后的 3 秒内,父级修改了 this.state.user,弹出的用户名是修改前的还是修改后的呢?

Class Component 展示的是修改后的值:

Function Component 展示的是修改前的值:

那么 React 文档中描述的 props 不是不可变(Immutable) 数据吗?为啥在运行时还会发生变化呢?

原因在于,虽然 props 不可变,是 this 在 Class Component 中是可变的,因此 this.props 的调用会导致每次都访问最新的 props

而 Function Component 不存在 this.props 的语法,因此 props 总是不可变的。

为了便于理解,笔者补充一些代码注解:

Function Component:

function ProfilePage(props) {
setTimeout(() => {
// 就算父组件 reRender,这里拿到的 props 也是初始的
console.log(props);
}, 3000);
}

Class Component:

class ProfilePage extends React.Component {
render() {
setTimeout(() => {
// 如果父组件 reRender,this.props 拿到的永远是最新的。
// 并不是 props 变了,而是 this.props 指向了新的 props,旧的 props 找不到了
console.log(this.props);
}, 3000);
}
}

如果希望在 Class Component 捕获瞬时 Props,可以: const props = this.props;,但这样的代码很蹩脚,所以如果希望拿到稳定的 props,使用 Function Component 是更好的选择。

Hooks 也具有 capture value 特性

看下面的代码:

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>
</>
);
}

在线 Demo

在点击 Send 按钮后,再次修改输入框的值,3 秒后的输出依然是 点击前输入框的值。这说明 Hooks 同样具有 capture value 的特性。

利用 useRef 可以规避 capture value 特性:

function MessageThread() {
const latestMessage = useRef(""); const showMessage = () => {
alert("You said: " + latestMessage.current);
}; const handleSendClick = () => {
setTimeout(showMessage, 3000);
}; const handleMessageChange = e => {
latestMessage.current = e.target.value;
};
}

只要将赋值与取值的对象变成 useRef,而不是 useState,就可以躲过 capture value 特性,在 3 秒后得到最新的值。

这说明了利用 Function Component + Hooks 可以实现 Class Component 做不到的 capture props、capture value,而且 React 官方也推荐 新的代码使用 Hooks 编写

3. 精读

原文 how-are-function-components-different-from-classes 从一个侧面讲述了 Function Component 与 Class Component 的不同点,之所以将 Function Component 与 Class Component 相提并论,几乎都要归功于 Hooks API 的出现,有了 Hooks,Function Component 的能力才得以向 Class Component 看齐。

关于 React Hooks,之前的两篇精读分别有过介绍:

但是,虽然 Hook 已经发布了稳定版本,但周边生态跟进还需要时间(比如 useRouter)、最佳实践整理还需要时间,因此不建议重构老代码。

为了更好的使用 Function Component,建议时常与 Class Component 的功能做对比,方便理解和记忆。

下面整理一些常见的 Function Component 问题:

非常建议完整阅读 React Hooks FAQ

怎么替代 shouldComponentUpdate

说实话,Function Component 替代 shouldComponentUpdate 的方案并没有 Class Component 优雅,代码是这样的:

const Button = React.memo(props => {
// your component
});

或者在父级就直接生成一个自带 memo 的子元素:

function Parent({ a, b }) {
// Only re-rendered if `a` changes:
const child1 = useMemo(() => <Child1 a={a} />, [a]);
// Only re-rendered if `b` changes:
const child2 = useMemo(() => <Child2 b={b} />, [b]);
return (
<>
{child1}
{child2}
</>
);
}

相比之下,Class Component 的写法通常是:

class Button extends React.PureComponent {}

这样就自带了 shallowEqualshouldComponentUpdate

怎么替代 componentDidUpdate

由于 useEffect 每次 Render 都会执行,因此需要模拟一个 useUpdate 函数:

const mounting = useRef(true);
useEffect(() => {
if (mounting.current) {
mounting.current = false;
} else {
fn();
}
});

更多可以查看 精读《怎么用 React Hooks 造轮子》

怎么替代 forceUpdate

React 官方文档提供了一种方案:

const [ignored, forceUpdate] = useReducer(x => x + 1, 0);

function handleClick() {
forceUpdate();
}

每次执行 dispatch 时,只要 state 变化就会触发组件更新。当然 useState 也同样可以模拟:

const useUpdate = () => useState(0)[1];

我们知道 useState 下标为 1 的项是用来更新数据的,而且就算数据没有变化,调用了也会刷新组件,所以我们可以把返回一个没有修改数值的 setValue,这样它的功能就仅剩下刷新组件了。

更多可以查看 精读《怎么用 React Hooks 造轮子》

state 拆分过多

useState 目前的一种实践,是将变量名打平,而非像 Class Component 一样写在一个 State 对象里:

class ClassComponent extends React.PureComponent {
state = {
left: 0,
top: 0,
width: 100,
height: 100
};
} // VS function FunctionComponent {
const [left,setLeft] = useState(0)
const [top,setTop] = useState(0)
const [width,setWidth] = useState(100)
const [height,setHeight] = useState(100)
}

实际上在 Function Component 中也可以聚合管理 State:

function FunctionComponent() {
const [state, setState] = useState({
left: 0,
top: 0,
width: 100,
height: 100
});
}

只是更新的时候,不再会自动 merge,而需要使用 ...state 语法:

setState(state => ({ ...state, left: e.pageX, top: e.pageY }));

可以看到,更少的黑魔法,更可预期的结果。

获取上一个 props

虽然不怎么常用,但是毕竟 Class Component 可以通过 componentWillReceiveProps 拿到 previousPropsnextProps,对于 Function Component,最好通过自定义 Hooks 方式拿到上一个状态:

function Counter() {
const [count, setCount] = useState(0);
const prevCount = usePrevious(count);
return (
<h1>
Now: {count}, before: {prevCount}
</h1>
);
} function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}

通过 useEffect 在组件渲染完毕后再执行的特性,再利用 useRef 的可变特性,让 usePrevious 的返回值是 “上一次” Render 时的。

可见,合理运用 useEffect useRef,可以做许多事情,而且封装成 CustomHook 后使用起来仍然很方便。

未来 usePrevious 可能成为官方 Hooks 之一。

性能注意事项

useState 函数的参数虽然是初始值,但由于整个函数都是 Render,因此每次初始化都会被调用,如果初始值计算非常消耗时间,建议使用函数传入,这样只会执行一次:

function FunctionComponent(props) {
const [rows, setRows] = useState(() => createRows(props.count));
}

useRef 不支持这种特性,需要写一些冗余的函判定是否进行过初始化

掌握了这些,Function Component 使用起来与 Class Component 就几乎没有差别了!

4. 总结

Function Component 功能已经可以与 Class Component 媲美了,但目前最佳实践比较零散,官方文档推荐的一些解决思路甚至不比社区第三方库的更好,可以预料到,Class Component 的功能会被五花八门的实现出来,那些没有被收纳进官方的 Hooks 乍看上去可能会眼花缭乱。

总之选择了 Function Component 就同时选择了函数式的好与坏。好处是功能强大,几乎可以模拟出任何想要的功能坏处是由于可以灵活组合,如果自定义 Hooks 命名和实现不够标准,函数与函数之间对接的沟通成本会更大。

讨论地址是:精读《Stateless VS Class 组件》 · Issue #137 · dt-fe/weekly

如果你想参与讨论,请 点击这里,每周都有新的主题,周末或周一发布。前端精读 - 帮你筛选靠谱的内容。

关注 前端精读微信公众号

special Sponsors

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

精读《Function VS Class 组件》的更多相关文章

  1. 精读《V8 引擎 Lazy Parsing》

    1. 引言 本周精读的文章是 V8 引擎 Lazy Parsing,看看 V8 引擎为了优化性能,做了怎样的尝试吧! 这篇文章介绍的优化技术叫 preparser,是通过跳过不必要函数编译的方式优化性 ...

  2. 深入浏览器工作原理和JS引擎(V8引擎为例)

    浏览器工作原理和JS引擎 1.浏览器工作原理 在浏览器中输入查找内容,浏览器是怎样将页面加载出来的?以及JavaScript代码在浏览器中是如何被执行的? 大概流程可观察以下图: 首先,用户在浏览器搜 ...

  3. [翻译] V8引擎的解析

    原文:Parsing in V8 explained 本文档介绍了 V8 引擎是如何解析 JavaScript 源代码的,以及我们将改进它的计划. 动机 我们有个解析器和一个更快的预解析器(~2x), ...

  4. 一文搞懂V8引擎的垃圾回收

    引言 作为目前最流行的JavaScript引擎,V8引擎从出现的那一刻起便广泛受到人们的关注,我们知道,JavaScript可以高效地运行在浏览器和Nodejs这两大宿主环境中,也是因为背后有强大的V ...

  5. Chrome V8引擎系列随笔 (1):Math.Random()函数概览

    先让大家来看一幅图,这幅图是V8引擎4.7版本和4.9版本Math.Random()函数的值的分布图,我可以这么理解 .从下图中,也许你会认为这是个二维码?其实这幅图告诉我们一个道理,第二张图的点的分 ...

  6. (译)V8引擎介绍

    V8是什么? V8是谷歌在德国研发中心开发的一个JavaScript引擎.开源并且用C++实现.可以用于运行于客户端和服务端的Javascript程序. V8设计的初衷是为了提高浏览器上JavaScr ...

  7. 浅谈Chrome V8引擎中的垃圾回收机制

    垃圾回收器 JavaScript的垃圾回收器 JavaScript使用垃圾回收机制来自动管理内存.垃圾回收是一把双刃剑,其好处是可以大幅简化程序的内存管理代码,降低程序员的负担,减少因 长时间运转而带 ...

  8. V8引擎嵌入指南

    如果已读过V8编程入门那你已经熟悉了如句柄(handle).作用域(scope)和上下文(context)之类的关键概念,以及如何将V8引擎作为一个独立的虚拟机来使用.本文将进一步讨论这些概念,并介绍 ...

  9. 浅谈V8引擎中的垃圾回收机制

    最近在看<深入浅出nodejs>关于V8垃圾回收机制的章节,转自:http://blog.segmentfault.com/skyinlayer/1190000000440270 这篇文章 ...

  10. 深入出不来nodejs源码-V8引擎初探

    原本打算是把node源码看得差不多了再去深入V8的,但是这两者基本上没办法分开讲. 与express是基于node的封装不同,node是基于V8的一个应用,源码内容已经渗透到V8层面,因此这章简述一下 ...

随机推荐

  1. Python——合并指定文件夹下的所有excel文件

    前提:该文件夹下所有文件有表头且具有相同的表头. import glob # 同下 from numpy import * #请提前在CMD下安装完毕,pip install numppy impor ...

  2. CentOS下TensorFlow安装命令

    安装扩展源:sudo yum -y install epel-release 安装python-pip模块:sudo yum install python-pip 直接把whl文件download下来 ...

  3. jmeter分布式测试教程和远程的代理机无法连接网络的问题解决方法

    一.Jmeter分布式执行原理: 1.Jmeter分布式测试时,选择其中一台作为控制机(Controller),其它机器做为代理机(Agent). 2.执行时,Controller会把脚本发送到每台A ...

  4. Mac环境下Scrapy的安装

    直接命令安装: $ easy_install scrapy 从 GitHub 安装: $ git clonehttps://github.com/scrapy/scrapy.git $ cd scra ...

  5. 【easy】107. Binary Tree Level Order Traversal II 按层输出二叉树

    按层输出二叉树,广度优先. 3 / \ 9 20 / \ 15 7 [ [15,7], [9,20], [3] ] /** * Definition for a binary tree node. * ...

  6. go语言中使用defer、panic、recover处理异常

    go语言中的异常处理,没有try...catch等,而是使用defer.panic.recover来处理异常. 1.首先,panic 是用来表示非常严重的不可恢复的错误的.在Go语言中这是一个内置函数 ...

  7. go语言熟知的开源项目

    go语言在国内是比较火的,并且好多公司都用go语言进行重构之前的系统,今天来看下基本大家都知道的go语言的开源项目 1.Docker Docker 是一个开源的应用容器引擎,让开发者可以打包他们的应用 ...

  8. technologies

    RPC Apache Thrift, Thrift is an interface definition language and binary communication protocol, lik ...

  9. GIt -- git push 远程分支老是需要重新输入公钥密码问题处理?

     步骤: 先查看远程有哪些分支 删除远程分支,重新关联远程分支 最后再git push 到远程分支 git remote -v git remote rm RedisNote git remote a ...

  10. CSS div内文字显示两行,超出部分省略号显示

    1. 概述 1.1 说明 在项目过程中,有时候需要控制div内文字最多显示两行,超出的使用省略号进行处理.使用识别码/前缀-webkit进行处理可直接得到相应效果. 1.1 -webkit WebKi ...