likes
comments
collection
share

如何理解函数式组件?

作者站长头像
站长
· 阅读数 13

今天读了一篇很好的文章,是讲解关于useEffect这个api的,叫useEffect 完整指南,是react的核心维护者本人自己写的,这本文章写的很好,也很长,有点类似于迷你书(文中原话),还没看完的时候,我有一种自己想要出来讲讲我对于react函数式组件的理解的冲动。

我非常喜欢把函数作为组件这种编程方式,因为函数是JavaScript语言本身的特性,因此,如果组件即函数,那么关于函数的基础知识在组件里就仍然适用,而且函数非常容易编写,意味着组件也很容易创建,可以让组件变得非常的轻量化,有利于组件的拆分。

接着我们开始讲我准备讲解的内容。为了书写方便,我们接写来函数时组件统一简写为FC。

一个FC的基本结构是这样子的:

    function Component() {
        // 先是各种变量声明、函数定义、hooks调用等
        const a = x
        
        const [data, setData] = useState(defaultData)
        
        const anyFunc = () => {} 
        
        useEffect(() => {}, [])
        
        // 最后返回jsx
        return <></>
    }

基于这个结构,我们先讲解一些基本事实:

  • 一个组件就是一个普通的JavaScript函数,并没有什么特殊,即便是返回的JSX,经过编译后也是一个对象,因此,它就是一个返回对象的函数。
  • 这个函数的返回值,会被React用于这个组件的渲染
  • 每一次重新渲染的过程,就是重新调用这个函数,重新得到返回值的过程,因此,重新渲染等价于重新调用。
  • 每一次调用函数,函数内的代码从第一行开始,直到遇到return语句,都会依次重新执行一遍,所以,中间定义的各种变量,hooks调用都会重新执行一遍

ok,接下来我们看一个例子:

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {    
      setTimeout(() => { alert('You clicked on: ' + count); }, 3000);  
  }
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
     </div>
  );
}

这个例子就是文章开头说的那篇文章里的,这个例子很简单,有两个按钮,一个是非常常见的计数器,另一个是一个按钮,点击后,调用setTimeout函数,3s后输出count的值。你可以点击这里体验

现在,进行如下操作:

  1. 点击count到3
  2. 此时点击另一个按钮
  3. 在3s计时器结束前,继续点击count到5

现在请你判断,在计时器结束后,会输出什么?

答案是3。

其实答案不太重要,毕竟这个问题表面也就是让你在3和5之间选择而已,这个例子真正想让读者自省的是,答案为什么是3?你是否理解了以上操作步骤背后整个的react运作过程?

我们结合刚刚讲的几个基本事实来梳理这个过程。

  1. 当我们进行第(1)步的时候,这个函数反复执行了三次,为什么会重新执行,这是useState这个hooks返回的setState的作用,当我们调用setState,为state赋一个新的值之后,下一次调用useState,返回的state就是这个新值,另外,每次重新调用这个函数,内部的handleAlertClick函数都会重新定义一次,也就是说,每一次重新渲染,在jsx里面每一次绑定的handleAlertClick函数都是跟上一次渲染绑定的引用时不同的了,但是第一次、第二次渲染时,这个函数不管是被定义也好,被绑定为点击处理事件的引用函数也好,都没有意义,因为没有用到过。
  2. 当count为3的时候,我们我们进行了步骤(2),此时,第三次渲染时,定义的handleAlertClick函数被调用了,我们知道,函数被定义时,我们要会对内部的变量进行一个扫描,检测其中是否引用了非本函数内部定义的外部作用域的变量,如果有的话,那么不论这个函数在何时、何处被调用,他都可以始终访问到其内部的定义的这些外部作用域的变量,这是一个在代码编译阶段就会完成的过程,这个特性,在JavaScript里面被称为闭包。之所以叫闭包,可以形象的理解为,这个函数就像一个封闭的小背包,装着它里面的变量可以到处跑。我们可以看到,handleAlertClick函数内部是有一个访问外部变量的count的,我们知道,[count, setCount]这个语法其实是ES6的解构赋值语法,实际上每次执行的时候,我们都重新声明了一个count变量用于接收useState函数的返回值,因此,每一次执行时count变量的地址都是不一样的,所以每一次handleAlertClick函数定义的时候携带的闭包里面的count变量每次也都是不一样的,而第三次渲染时,handleAlertClick函数内部的携带的显示就是值3的那个count变量。
  3. 接着我们执行完步骤3,在我们加到5的过程中,函数又被重新执行了两次,又重复定义了count、handleAlertClick,但跟第一、二次一样,都没有意义,当3s计时器结束时,这个被setTimeout推入宏任务队列的函数内部携带的,其实是值为3的count,所以就输出了3。

我不知道是否讲解的足够清晰,假设读者已经充分理解的情况下,我们可以看到,这整个过程的解释逻辑本质上跟react的关系不大,完全就是一些函数的基础知识,这也是我提到:如果组件即函数,那么关于函数的基础知识在组件里就仍然适用这句话的原因。

所以,我对函数式组件的理解就是:React的设计哲学就是希望用函数来描述组件,让组件编写的心智模型跟编写JavaScript函数时的心智模型统一,减少开发者的心智负担,所以我喜欢这个设计。这跟hooks是没有关系的,你把上例中的useState抠掉,这个组件顶多就是没有办法记录状态,退化为"无状态"而已,这种"函数即组件"的设计哲学仍然成立,实际上,在hooks出现之前,函数式组件就是被普遍称为"无状态组件"的。