我了解到,掌握了某种语言、框架或工具的人与没有掌握的人之间的最大区别在于他们所使用的思维模型(Mental Model)。前者拥有清晰而先进的思维模型,而后者则没有。

通过良好的思维模型,你可以直观地理解复杂的问题和解决方案,这比逐步的去寻求解决方案要快得多。

我每天都用 React 工作,并且一直都在寻找解决难题的解决方案。我可以通过在围绕 React 创建的良好思维模型来做到这一点。在本文中,我将解释那些有助于解决问题和解决复杂性的思维模型。

无论你是已经使用 React 多年的老鸟还是刚开始使用的新手,在我看来,有用的思维模型是使自己有信心使用它的最快方法。

什么是思维模型?

思维模型是我们如何想象一个系统正常工作的方法。我们通过了解系统的不同部分并把他们连接起来,这一点很重要,因为它可以帮助我们理解世界并解决问题。

心理模型的视觉表示

思维模型的一个很好例子就是互联网:互联网是一个复杂的系统,有许多相互连接的部分,但是请考虑一下你是怎样想象它工作方式的。我的想像是通过许多大型服务器相互连接的大量计算机,其中有许多中间设备对每条信息的存储位置进行重定向。

当然这并不是一个完整的思维模型,但足够好,我可以用它来解决问题,并在需要时加以改进,这就是重点:思维模型旨在帮助我们解决问题和理解世界。

为什么思维模型很重要?

当我在 2014 年开始搭建网站时,很难理解它的工作原理。用 WordPress 构建我的博客很容易,但是我对托管、服务器、DNS、证书等等一无所知。

当我开始阅读文章并尝试一些东西(并多次破坏我的服务器配置)时,就开始掌握这种系统来了解它的工作方式,直到最终它被建立。我的头脑围绕该系统建立了一个思维模型,可以用来与之合作。

如果有人解释了它,并将他们的思维模型转移给我,我就会更快地了解它。在这里我将会解释(并展示)自己在 React 中使用的思维模型。它将帮助你更好地理解 React,并使你成为更好的开发人员。

React 思维模型

React 帮助我们比以往更轻松地构建复杂的交互式 UI。它还鼓励我们以某种方式编写代码,指导我们创建更易于浏览和理解的应用。

React 本身是一个以简单思想为核心的思维模型:对依赖相似逻辑和 UI 的程序部分进行封装,React 将会始终确保该部分保持最新。

无论你是刚刚开始使用 React 还是已经用了多年,拥有清晰的思维模式能够让你更有信心去使用它。所以我要把自己的思维模式转移给你,并从第一原理开始并在其基础上进行构建。

函数贯穿始终

首先为 JavaScript 和 React 的基本构建模块建模:函数。

  • React 组件只是一个函数
  • 包含其他组件的组件是调用其他函数的函数
  • prop 是函数的参数

这被 React 所使用的标记语言 JSX 隐藏。剥离掉 JSX 的 React 是一堆互相调用的函数。 JSX 本身就是一种实用的思维模型,使 React 用起来更简单、更直观。

让我们分别看一下所有的部分。

组件是返回 JSX 的函数

React 与 JSX(JavaScript XML)一起使用,JSX 是一种完全利用 JavaScript 的功能来编写类似 HTML 代码的方法。 JSX 为以直观的方式使用嵌套函数提供了一个出色的应用思维模型。

让我们忽略类组件,而将注意力集中在更常见的功能组件上。 功能组件是一个行为与其他 JavaScript 函数完全相同的函数。 React 组件始终返回 JSX,然后执行并将其转换为 HTML。

以下是是简单的 JSX :

const Li = props => <li {...props}>{props.children}</li>;

export const RickRoll = () => (
  <div>
    <div className='wrapper'>
      <ul>
        <Li color={'red'}>Never give you up</Li>
      </ul>
    </div>
  </div>
);

用 Babel 编译成纯 JavaScript:

const Li = props => React.createElement('li', props, props.children);

export const RickRoll = () =>
  React.createElement(
    'div',
    null,
    React.createElement(
      'div',
      {
        className: 'wrapper',
      },
      React.createElement(
        'ul',
        null,
        React.createElement(
          Li,
          {
            color: 'red',
          },
          'Never give you up',
        ),
      ),
    ),
  );

如果你发现自己很难驾驭这些代码,那么你并不孤单,你将会了解为什么 React 团队决定改用 JSX。

现在,请注意每个组件作为函数是如何调用另一个函数的,每个新组件是 React.createElement 函数的第三个参数。每当你编写组件时,请记住它是正常的 JavaScript 函数,这很有用。

React 的一个重要特征是组件可以有多个子组件,但只有一个父组件。我发现这很令人困惑,直到我意识到 HTML也有相同的逻辑,每个元素必须位于其他元素内并且可以有很多子元素。你可以在上面的代码中注意到这一点,其中只有一个父级 div 包含所有子级。

组件的 prop 与函数的参数相同

在使用函数时,我们可以用参数与该函数共享信息。对于 React 组件,我们将这些参数称为 prop(有趣的故事:我很长时间以来都没有意识到 propproperties 的缩写)。

在本质上,prop 的行为与函数参数完全“一样”,不同之处在于我们通过 JSX 的更好接口与它们进行交互,而 React 为 prop(如 children)提供了额外的功能。

围绕函数创建思维模型

利用这些知识,我们可以建立一个思维模型来直观地理解函数!

当我想到一个函数时,会把它想象成一个盒子,当它被调用时,这个盒子会做一些事情。它可以返回值,也可以不返回:

function sum(a, b) {
  return a + b;
}

console.log(sum(10, 20)); // 30

function logSum(a, b) {
  console.log(a + b); // 30
}

由于组件是一种奇特的函数,所以我们也把组件变成一个盒子,以 props 作为原料,盒子需要创建输出。

在执行组件时,它将会运行其具有的任何逻辑(如果有的话),并评估其 JSX。其中的任何标签都将会变为 HTML,并将执行所有组件,并且重复该过程,直到到达子链中的最后一个组件。

由于一个组件可以有多个子组件,但只有一个父组件,所以我把多个组件想象成一组盒子,一个盒子装在另一个盒子里。每个盒子都必须包含在一个更大的盒子中,并且里面可以有多个较小的盒子。

一个里面有着很多小盒子的大盒子,上面写着“这是另一个盒子里的盒子”

但是如果不了解一个组件如何与其他组件交互,用来表示组件的盒子这一思维模型是不完整的。

如何思考闭包

闭包是 JavaScript 中的核心概念。它们启用了该语言的复杂功能,对于能够帮助理解 React 的良好思维模型而言,理解闭包非常重要。

这也是初学者最苦恼的功能之一,所以在不解释技术细节的前提下,我将向大家展示我对闭包的思维模式。

闭包的基本描述是它是一个函数。我想像它是一个盒子,它可以防止里面的东西溢出,同时又允许它外面的东西进入,就像一个半透水的盒子。但是溢出到哪里呢?

尽管闭包本身是一个框,但是任何闭包都将位于较大的框内,而最外面的框是 Window 对象。

窗口对象封装了其他所有内容

但是闭包究竟是什么?

闭包是 JavaScript 函数的特性。如果你使用了函数,则用的就是闭包。

正如我所提到的,函数是一个框,也使闭包成为一个框。考虑到每个函数可以在其中包含许多其他函数,因此闭包是函数使用其外部信息的能力,同时保持其内部的信息不会“泄漏”或由外部函数使用。

用我的思维模型来讲:我想象函数是作为盒子中的盒子,每个较小的盒子都可以看到外部盒子或父级盒子的信息,但是大盒子却看不到较小盒子的信息。这就是我所能做的关于闭包的简单而准确的解释。

函数只能访问自己和父级的信息

闭包很重要,因为可以利用它们来创建一些强大的机制,而 React 则充分利用了这一点。

React 中的闭包

每个 React 组件也是一个闭包。在组件内,你只能将 prop 从父对象传递到子对象,而父对象看不到子对象内部的内容,这是一项旨在使我们程序的数据流更易于跟踪的功能。为了找到数据的来源,我们通常需沿着树结构向上查找是哪个父级将其发送出去的。

一个很好的 React 中闭包的例子是通过子组件更新父级状态。你可能已经做了这件事,却没有意识到自己正在用闭包。

首先,我们知道父级不能直接访问子级的信息,但是子级可以访问父级的信息。因此,我们通过 props 把该信息从父级发送到子级。在这种情况下,信息将采用函数的形式更新父级状态。

const Parent = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      The count is {count}
      <div>
        <ChildButtons onClick={setCount} count={count} />
      </div>
    </div>
  );
};

const ChildButtons = props => (
  <div>
    <button onClick={() => props.onClick(props.count + 1)}>
      Increase count
    </button>
    <button onClick={() => props.onClick(props.count - 1)}>
      Decrease count
    </button>
  </div>
);

onClick 发生在 button 中时,它将执行从 props.props.onClick 接收到的函数,并用 props.count 更新值。

这里的见解在于我们通过子级来更新父级状态的方式,在本例中为 props.onClick 功能。之所以起作用,是因为该函数是在 Parent 组件作用域内(在其闭包内)“声明”的,因此可以访问父级信息。一旦在子级中调用了该函数,它仍存在于相同的闭包中。

这可能很难理解,所以我认为它是闭包之间的“隧道”。每个都有自己的作用域,但是我们可以创建一种将两者连接的通信隧道。

一旦了解了闭包如何影响我们的组件,就可以迈出下一步:React state。

把 React 的状态放入我们的思维模型

React 的哲学很简单:它负责处理何时如何渲染元素,而开发人员则控制怎样进行渲染。状态是我们决定做什么的工具。

当状态被更改时,其组件将渲染并因此重新执行其中的所有代码。我们这样做是为了向用户显示最新被更新的信息。

在我的思维模型中,状态就像盒子内部的特殊属性。它独立于其中发生的一切。它将在第一次渲染时得到默认值,并且始终保持最新值。

每个变量和函数都在每次渲染上被创建,这意味着它们的值也是全新的。即使变量的值没有改变,每次也会重新计算并重新分配。状态不是这种情况,只有在通过 set state 事件要求更改状态时才会被更改。

状态是盒子中一个特殊的、独立的部分;prop 是从外面来的

状态遵循一个简单的规则:只要被更改,状态就会重新渲染组件及其子级。prop 遵循相同的逻辑,如果 prop 发生更改,组件将会重新渲染,但是我们可以通过对其进行修改来控制状态,而 prop 更为静态,并且通常会根据对状态变化的反应而进行更改。

渲染的思维模型:了解 React 的魔力

我认为渲染是 React 最令人困惑的部分,因为在渲染过程中发生了很多事情,而通过查看代码有时并不明显。这就是为什么拥有清晰的思维模式会对你有所帮助的原因。

我想象用我虚构的盒子进行渲染的方式有两种:第一种渲染使盒子存在,即状态初始化时。第二种是重新渲染时,这时盒子是被回收重新利用的,其中大部分都是全新的,但一些重要元素仍然保持其原来的状态。

在每个渲染中,都会创建组件内部的所有内容,包括变量和函数,这就是为什么我们可以使用变量来存储计算结果的原因,因为它们将在每个渲染中重新计算。这也是把函数作为值不可靠的原因,因为每个渲染的引用(函数的值本身)都不相同。

const Thumbnail = props => (
  <div>
    {props.withIcon && <AmazingIcon />}
    <img src={props.imgUrl} alt={props.alt} />
  </div>
);

根据组件接收的 prop ,以上内容将给出不同的结果。在每次 porp 更改时,React 必须重新渲染的原因是它希望使用户了解最新的信息。

但是,重新渲染后状态不会改变,它们的值得以维持。这就是为什么盒子是“回收重利用的”而不是每次都创建全新的。在内部 React 会跟踪每个盒子并确保其状态始终保持一致。这就是 React 怎样知道何时去更新组件的方式。

当道prop(或状态)发生变化时,会进行新的渲染,并且组件的输出会发生变化

通过想象一个盒子被回收,我可以了解其中的状况。对于简单的组件而言,它很容易掌握,但是组件变得越复杂,它所接收的 prop 越多,维护的状态也就越多,那么清晰的思维模型就越有用。

完整的 React 思维模型:将它们整合在一起。

现在我已经分别解释了拼图的所有碎片,下面把它们放在一起。这是我在 React 组件中使用的完整思维模型,把它从我的想象中直接转化为文字。

我想象一个 React 组件是一个盒子,它在其内部包含所有信息,包括它的子级,也就是更多的盒子。

就像现实中的盒子一样,它可以在其中包含其他盒子,而这些盒子中又可以包含更多盒子。这样每个盒子(组件)都必须有一个父对象,并且一个父对象可以有多个子对象。

React 组件的基本表示

这些盒子是半渗透性的,这意味着它们从不会把任何东西泄漏到外部,但是可以使用来自外部的信息,就像属于它们自己的一样。我想像这代表闭包在 JavaScript 中的工作方式。

在 React 中,组件之间共享信息的方式称为 props ,同样的想法也适用于函数,并被称为 arguments,它们都以相同的方式工作,但是语法不同。

在组件内部,信息​​只能从父级那里传播到子级。换句话说,子组件可以访问其父组件的数据和状态,但不能反过来,而我们通过 prop 共享信息。

我想像这种有方向的信息共享是盒子内部的盒子。最里面的盒子能够吸收父母的数据。

数据从父级组件共享给子级组件

但是必须首先创建这个,并且发生在 render 上,默认值赋给 state,就像函数一样,该组件中的所有代码都将会被执行。在我的思维模型中,这等效于盒子被“创建”。

随后的渲染或“重新渲染”将会再次执行组件中的所有代码,重新计算变量,重新创建函数等。除了 state 外,所有内容在每个渲染器上都是全新的。 状态的值在渲染过程中保持不变,只能通过 set 方法来更新。

在我的思维模型中,我将重新渲染视为回收盒子,因为大多数盒子是重新创建的,但是由于 React 跟踪组件的状态,所以它仍然是同一个盒子。

当回收一个盒子时,其中的所有盒子,即它的子盒子也都被回收了。发生这种情况的原因是组件的状态已被修改或 prop 已更改。

prop 或 state 被更改时,React 组件的模型会重新渲染

请记住,state 或 prop 的更改意味着用户看到的信息已过时,React 会始终希望保持 UI 更新,以便它能够重新渲染必须显示新数据的组件。

通过这些思维模型,我在使用 React 时会充满信心。它们帮我把可能是迷宫的代码可视化为全面的思维导图。它还揭开了 React 的神秘面纱,并使我达到更熟悉它的水平。

一旦你开始理解它的核心原理并创造出一些用来想象代码如何工作的方式,React 就不会那么复杂。


我希望这篇文章对你有用!我意识到自己能够凭直觉理解 React,而把它理解为文字是有挑战性的。

本文给出的某些解释只是简化过的,例如不会在每个渲染器上重新执行更多操作,例如 useEffect,useCallback 和 useMemo hook。我完整的思维模型比我在一篇文章中所能解释的更加复杂,请继续关注本文的后续。

本文后续的第 2 部分将会重点介绍 React 的 API 的深入模型,例如 useMemo、useCallback 和 useEffect,以及如何用思维模型来改善 React 应用的性能。第 3 部分将会重点介绍 Context 等高级功能,以及我用于 React 的精确而完整的思维模型的摘要。