使用React后为什么不再需要jQuery呢?

首先

我写这篇文章是为了那些完全不了解React(通常称为React.js)或者看了一些文章却不明白它是什么的人。

目的是让只会「只知道jQuery」的人具体了解氛围,这不是面向已经有动力的人的教程。只要有动力,读一下日文版文档并动手操作就能迅速掌握,所以我们的目标是让人们产生足够的兴趣来做到这一点。

以后我们将使用ES2015(ES6)语法(例如箭头函数等)。如果对此部分感到困惑的人,请先了解箭头函数和const语法,然后再继续前进。

在以下说明中,此图标表示的是(从2023年的角度来看)”旧故事”。当你编写新代码时,通常不需要了解这些内容,但为了在查看旧文章时避免混淆,这些内容作为参考信息写在这里。本文本身会定期更新,以跟进React的最新动态并进行修订。

是React所要做的

React 做的事情非常简单,API 也很少。它在”少记忆力却能力强”方面无人能敌。它的少数API已经足以驱逐掉大部分人们熟悉并喜欢使用的jQuery方法,可见其威力之大。

React 是一个用于构建用户界面的 JavaScript 库。它通过将应用程序分成多个可重用的组件来简化界面开发。React 通过使用虚拟 DOM 来实现高效的更新,并提供了强大的状态管理功能。

    利用一个保存了页面状态的“纯 JavaScript 对象”,通过作用一个“模板式的函数”,来提取出称为“虚拟 DOM”的 DOM 设计图,并利用该设计图来构建真实的 DOM。

请将“这里所说的’纯粹的 JavaScript 对象’是指仅解析 JSON 即可获取的对象,不包含任何标签字符串,仅包含干净的数据”的意思,用中文进行同义表达。

从Web API等获得原始对象,然后使用jQuery创建DOM是一个常见的操作。例如,在”显示商品数据列表”的场景中,可以像这样直接进行操作。

$.getJSON("/api/items").then((data) => {
  const ul = $("ul.item-list").empty();
  data.items.forEach((item) => {
    const li = $("<li>").addClass("item").appendTo(ul);
    if (item.stock === 0) li.addClass("soldout");
    $("<div>").addClass("item-name").text(item.name).appendTo(li);
    $("<div>").addClass("item-price").text(item.price).appendTo(li);
  });
});

我并没有做什么困难的事情,但它的外观不够直观。

用 React 撰写相同的内容会产生完全不同的外观(行数增加是因为对函数进行了拆分)。

// ItemListのコンポーネント定義(実体は関数)
const ItemList = (props) => {
  return (
    <ul className="item-list">
      {props.items.map((item) => (
        <ItemDetail item={item} />
      ))}
    </ul>
  );
};

// ItemDetailのコンポーネント定義(実体は関数)
const ItemDetail = (props) => {
  const item = props.item;
  return (
    <li className={"item" + item.stock === 0 ? " soldout" : ""}>
      <div className="item-name">{item.name}</div>
      <div className="item-price">{item.price}</div>
    </li>
  );
};

fetch("/api/items")
  .then((res) => res.json())
  .then((data) => {
    ReactDOM.createRoot().render(
      <ItemList items={data.items} />, // これを
      document.getElementById("container") // ここにレンダーしろ
    );
  });

嗯,总之就是有人看到长相奇怪的像是HTML标签的东西就立即想逃回家的情况对吧。然而在React中,JavaScript和HTML(?)的结合就是基本规范。嗯,请稍微忍耐一下吧。作者刚开始也讨厌这个外观,打算逃开,但不到一天就改变了主意。稍后我会详细解释一些,但总之这个“标签”实际上只是一个简单的函数调用而已,通过Babel等工具转换成函数调用形式,所以这只是JavaScript而已。代替$.getJSON()的是通过fetch来实现,当然你也可以使用superagent或axios,无妨。

不再直接切割或粘贴DOM部分了,取而代之的是定义了两个函数(组件)。每个函数返回了一些类似于“HTML标签”的东西,其中嵌入了JavaScript表达式(变量){}。这个“标签状物体”就是被称之为虚拟DOM的那个东西,一直在讨论中。

只需调用一个 React 的 API,就是最后一个 ReactDOM.createRoot().render()。而且这是这篇文章中唯一涉及到的 React 的 API。只要需要显示数据,只要有这个就可以了。

也许有人在旧文章中看到过使用 class 構文(直到2019年)或使用 React.createClass API(直到2015年)来定义组件的内容,但这些已经成为了遗留的做法。现今,组件基本上是通过简单的函数来定义的。

一看就觉得它很像常用的服务器端语言中所谓的“模板”。但是与模板只是基于字符串的处理方法,并且需要进行HTML词法解析的过程不同,React则是基于称为“虚拟DOM”的两个“DOM设计图”来直接处理实际的DOM。像ItemList和ItemDetail这样的函数返回的“类似标签”的东西并不是真正的DOM元素,只是设计图,也就是非常轻量级的JavaScript对象。

在传统的模板引擎中,开发者需要学习引擎独有的模板语法(例如循环等),并编写模板文件。而在 React 中,开发者需要用 JavaScript 编写返回 DOM 设计图的函数。除了这种类似 HTML 标签的写法和 {} 中可以插入任意表达式之外,没有其他独有的规则。循环和条件分支只需要像平时在 JavaScript 中一样使用三元运算符和 Array.prototype.map 即可。

通过类似标签的描述(函数调用)创建虚拟 DOM 后,ReactDOM.render 会在指定位置构建相应的真实 DOM 元素。这样做的原因是为了使开发者不需要手动编写像 appendTo()、removeClass()、text()、val() 这样的 jQuery 类 DOM 操作。

React 是一个只做基本事情的库。嗯,你觉得没什么特别的吗?换句话说,它只是让以前在服务器端语言(比如 PHP 或 JSP)中使用的模板变得稍微麻烦一些。相较于模板,有什么值得高兴的地方呢?为什么有些人称它为一种能够彻底改变世界的力量呢?接下来我会解释。

原因:JSX很方便且安全。

既经过所见,React引入了被称为JSX的类HTML语法来编写JavaScript代码。这是因为仅仅使用普通的JavaScript语法来读写DOM的设计图对人类来说非常困难。虽然JavaScript语法非常适合描述类似JSON的数据,但是它无法高效地表示类似HTML/XML的数据结构,比如”元素有属性,并且子元素可以是文本节点或其他元素”。

在JSX/HTML/XML的语法中,可以通过`

Hello World

`直观地表达结构。如果用JSON写的话,会变成`{ element: ‘div’, attributes: { title: ‘message’ }, children: [ ‘Hello ‘, { element: ‘b’, children: [‘World’] } ] }`。这样写不好。

当提及的“标签”通过Babel、TypeScript或CoffeeScript(>=2.0)等工具处理后,会机械地变成以下的形式。

React.createElement(
  "div",
  { title: "message" },
  "Hello ",
  React.createElement("b", null, "World")
);

如果您想尝试,可以在 Babel 的 REPL 中进行各种尝试(请将react预设打开)。简而言之,Text 会被转换为类似于React.createElement(Element,{a:’b’,c:d},’Text’)的 JavaScript 代码3。该函数在执行时会输出类似于上述JSON的对象,这就是虚拟DOM的最终真身。因为只有这个功能,所以想要手写的人不必使用JSX,实际上,一些精通React的开发者可能不再需要JSX。本文将通过使用JSX来论证4。

只要有JSX语法,JavaScript从一开始就具备了古典模板引擎辛勤实现的循环、条件分支、子模板调用(函数调用)和数值计算等功能,而且还超级高效,所以没有理由不使用。

巴别(Babel)、esbuild、TypeScript和CoffeeScript都已经普遍支持JSX,如果对这些工具熟悉的话,引入JSX的门槛就会很低。JSX本身只是一种通用且非常便利的语法糖,因此在许多其他库中也被采用,并且在各种编辑器中受到了非常良好的支持。可以说,现在已经几乎成为了事实上的标准了。

大约在2015年之前,JSX语法的生态系统比较薄弱,React开发团队发布了专用的JSX转换工具。因此,偶尔还能看到”React与altJS不兼容”的文章。

使用慣用的模板与循环相比,循环的可读性稍微降低了一些,但简单的语法错误,例如标签的不匹配,在编译时会进行检查,并且几乎不会出现XSS漏洞,例如转义错误。如果使用TypeScript,还可以进行严格的类型检查。

JSX最初可能会感到不适,但实际上,它与当时XML更为流行时提出的E4X语法非常相似,而且已经在Firefox中得到了实现。在还没有JSON的年代,有人想象使用XML取代JSON的未来,并且在那个时代,就像使用双引号””将字符串字面量括起来一样,使用将XML字面量括起来并在JavaScript中编写是很自然的。如果了解这一点,JSX可能会被视为JavaScript的非常自然的扩展。

然后,让我们认为HTML和JavaScript的合并不仅仅是一个禁忌,而且是一个非常重要的优点。长时间以来,“分离HTML和JavaScript”一直都是一个铁规,因为HTML是一个能够独立存在的主要文档,通过服务器端在HTML中艰难嵌入实际数据,而JavaScript只是一个附加物的时代已经过去了。现在在SPA中,几乎所有的实际数据都通过API传送,并且所有内容都以动态方式构建,因此HTML部分逐渐变成了“空壳子”的静态展示场所,如“没有内容的

    • ”、“等待变形成日历和滑块的

”、“与

没有连接的表单元素”、“仅有标题行的表格”、“点击也不会跳转的

有人可能会关注到“使用JSX时无法将实现和设计分开,对设计师来说可能会很难受”的问题,但是在React中,根据项目的规模,可以将视图专注的(纯粹的模板式)组件和逻辑为主的组件分开。更多详细信息请参考这个和这个。当然,根据规模和策略的不同,不分开可能更容易。

在Vue和Svelte等框架中,都有将HTML、JavaScript(以及CSS)结合成一个组件来进行管理的思想,这一思想经常被作为与React竞争的一个特点来提及。

如果担心失去语义化的HTML会降低可访问性的人,可以学习ARIA。

此外,由於JSX僅是JavaScript的語法糖,所以它可以輕鬆處理HTML語言中本來難以直接處理的值,例如”布林值”、”數字”、”函數”、”陣列”和”物件”。

原因:相比于模板,差分绘图速度明显更快。

当页面的状态因某些原因(例如用户操作)而改变并需要重新绘制屏幕时,React并不会每次都从头开始重建DOM。在更新屏幕时,React会比较新旧设计图并计算出差异,然后对DOM进行补丁式的操作。

开发者的任务就是准备一个包含新状态的新的 JavaScript 对象,并在刚刚的 ReactDOM.render() 中重新绘制。然后,React将自动根据以前的设计图与新的操作进行比较,并执行相应的操作,类似于jQuery中的remove(),val(),prop()和addClass()等等。这些操作不需要人工手动编写。

由于这个差分绘画速度非常快,所以可以将整个网页改用React去应用。每按下一个按键时,使用包含整个应用程序状态的纯JavaScript对象来“重新绘制”整个应用程序,这样可以相对舒适地工作(对于非常复杂的页面,可能需要优化)。这是基于字符串的模板不可能做到的事情,因为太慢了。比较虚拟DOM是轻量级的操作,仅在JavaScript内完成,因此与涉及HTML字符串解析的操作相比速度非常快。

使用React,您可以忘记“应该如何更改DOM以反映状态”,而专注于定义“与特定状态对应的DOM应该是什么”。换句话说,在繁琐的情况下,如“在加载中,临时禁用这5个按钮并显示指示器…”或“使用此选择内容改变此表单的格式…”,页面的一致性将被自动保持。

这个功能非常强大。刚才的 jQuery 示例并没有什么特别的,但是我们可以根据用户操作动态地添加库存、用户评价、颜色选择框和弹出菜单,同时在两个位置上显示相同的商品或按钮并保持同步,还可以对商品进行过滤和排序… 等等,当我们开始添加各种动态效果时,就会打开混沌之门。在示例级别上,很难真正感受到 React 的强大之处,而在实际应用中,我们会大大减少对于错误的担忧。

此外,由于能够将整个页面的信息收集到一个地方,因此实现诸如“即使重新加载浏览器也能恢复状态”以及“撤销/重做用户操作”等功能相对较容易。

原因:超轻量的组件

解释:对于超轻量的组件的理由

在 React 中,编写”部件”或”自定义标签”这样的东西非常容易。组合它们也非常简单。尽管称为组件定义,其实只是一个简单的函数,甚至可以写成一行。在使用组件时,几乎没有标准的 HTML 元素和自定义标签之间的界限,就像突然实现了完整功能的日历”标签”或论坛”标签”一样的感觉来处理。元素名称以小写字母开头表示原生标签(例如span、ul),以大写字母开头表示用户定义的组件。

// 1行でコンポーネントを定義
const Heading = (props) => <h1>{props.title} <em>{props.subtitle}</em></h1>;

// コンポーネントの使用
const virtualDOM = <Heading title="Hello" subtitle="React" />;

这只是一个极端简单的例子,但在开发实际应用时,我们也应尽可能使用简单的函数编写组件,然后将它们组合成一个应用程序。我们可以编写几行到十几行代码的”头部组件”、”目录组件”等多个组件,最终组合成一个应用程序。这种便利性是无法与jQuery UI等组件相媲美的。它的影响力可以媲美从Subversion切换到Git的程度。

然而,为了能够实现这样的技巧,”组件通常不具有内部状态(无状态)”这一原则非常重要。因此,接下来来谈谈这个问题。

原因:基于无状态组件的原则

组件是无状态的,这意味着返回的DOM设计图仅由外部提供的“标签属性”(props)信息唯一确定(类似数学意义上的函数)。多亏了这个思想,组件可以仅用函数来编写。

使用React时,最需要改变思维方式的部分可能是这个。在jQuery中,组件(如日历、滑块)通常会有内部状态(属性/状态),例如”当前输入值”或”是否禁用”,并且初始化和增加删除操作的代码会变得很冗长。而在React组件中,重要的状态大部分是从外部传入的。组件只是将外部信息转化为DOM设计图的棱镜。了解这一点,应该尽可能地做到这样。

我已经写了…,但是可以在函数组件中使用钩子来编写具有内部状态的组件。
但是,将其作为状态化的仅限于“不需要与父组件进行任何交互的私有内部状态”,比如“下拉菜单是否正在被下拉中”。

「外部存在状态」并不仅限于自定义组件。在React中,即使是等标准表单元素也充分体现了这个概念,并且表现为无状态的行为。在下面的例子中,由于value是由外部固定的值提供的,因此这个input元素就成为只读的,无法更改其内容。

在CodePen上查看React示例。

如果要实现编辑功能,就需要通过那个管理页面状态的简洁 JavaScript 对象。当在文本框内进行输入操作时,不是等到内部状态改变后再触发事件来更新外部状态,而是通过回调函数先改变外部状态,然后再重新绘制屏幕。就像这样的想象。

查看CodePen上naruto(@narutan)的paVbjx的代码。

虽然可能会觉得我绕远了,但是只有一个干净的原始数据(“单一真相源”)存在于外部的一个地方,DOM 仅仅是反映这个数据,正因如此,这种结构在复杂的表单应用中更加方便。在使用 jQuery 编写设置界面等表单时,需要使用 val() 或 text() 来实现“将原始数据的状态反映到 DOM 中”,以及相反的“从 DOM 的状态重新构建原始数据”的功能,但是在这种结构中,不需要进行这样的操作。(如果你听说过“单向数据流”的口号,它指的就是这个。)

有些人可能会惊讶地看到,在上述例子中,被视为久远黑历史的onChange和onClick属性华丽地复活了。但从React的设计思想来看,这样的写法并没有任何问题,我们应该将其视为一种新的语法。在内部,已经进行了各种优化。

在React中,一个形式的复杂度大约是jQuery UI的一半,并且可以在1/5的时间内实现(由6个无状态组件组成。虽然没有自己编写带有状态的组件,但使用了React-Bootstrap。还有很多其他方便的UI库可供选择)。

condition-image

那么,React 本身是一个单功能绘图库,对于“外部状态”应该在什么地方以什么样的粒度保存完全不关心。对于小页面或小部件来说,像上面的示例一样将状态放入对象中并完全更新没有任何问题(实际上,通常是将该对象作为最顶层组件的状态来维护,而不是多次调用 render)。然而,在大型项目中,如果一味把“重要的状态放在外部”,就会产生一个巨大的状态对象,并且有可能陷入类似于“全局变量过多”的混乱危险中。为了避免这种情况,需要混合使用其他库来有条理地管理“外部状态”,其中代表性的是 Redux 和 Recoil(严格来说是与 React 无关的)。根据需要使用它们。

虽然经常被误解,但像 Redux 这样的工具并非必需品。在 React 文档中也没有任何提及。在小型项目中,突然引入类似 Redux 的专用状态管理工具可能会增加代码量,而实际上并不清楚其中的实用优势。除非已经对单元测试等概念非常熟悉并进行大规模开发的人,否则应该首先熟悉 React 的思想而不使用 Redux。此外,建议新项目使用官方的 Redux Toolkit,因为它能大大减少样板代码并与 TypeScript 兼容。

如果React不合适的话

当然,React并非万能的(对于标题有些夸张了,抱歉)。要考虑React的弱点和不适用的情况,然后慎重选择使用。

    • React 自体の理解に加えて、Babel/TypeScript のようなトランスパイラの知識は(絶対ではないけど)ほぼ必須で、Webpack のようなモジュールバンドラの知識も(絶対ではないけど)ほぼ必須。そこまで至っていない人にとっては多少学習コストはあります。React 公式のスターターやフレームワークを使って 3 分で React アプリの雛形を作ることもできますが、まあ内部で何をやっているのかは知っておいた方が良いでしょう。
    • 複雑で動的なものほど React は威力を発揮しますが、HTML が主役で動的な要素が少ないサイトや 1 日で書き捨てるようなサイトは jQuery でも特に困りません(jQuery の方が楽な訳でもないですが)。大雑把ですが、作ろうとしているものが「アプリ(HTML が 1 で JavaScript が 9)」なら断然 React が良いはずですが、「記事(HTML が 9 で JavaScript が 1)」なのであれば学習コストの兼ね合いでの判断になります。
    • あくまで JavaScript ベースのライブラリなので、デザイナ系の人の理解と協力が必要(とはいえそんなに難しくないはずであることは前述しました)。
    • 大抵の場合はサーバ側の協力も必要(基本的にはサーバ側は楽になります)。純な React アプリでは HTML は数行くらいの固定ファイルになり、あらゆる実データは REST や GraphQL などの API 経由で来ます。サーバサイドに必要なのは API であって、テンプレートで HTML にデータを埋め込むことではありません。
    • React 最新版では IE8 以下は切り捨て。も、もういいよね…
    React にとって DOM とはあくまで外部状態を投影するためのスクリーンなので、逆に DOM からデータを取ってくるような作業(スクレイピング)は行えません。

結論:你不需要使用jQuery嗎?

以前引起了轰动的是一篇名为”你不需要jQuery”的文章,从看到的反应,比如在Hatebu上,意外地很多人说”我更加意识到jQuery的易用性了”。

然而,在该文章的开头部分,写着“直接操作DOM已经成为一种反模式”,该文章是在这个前提下撰写的。不是说“(通过努力,标准API也能写出与jQuery相同的东西,所以)不再需要jQuery”,而是更根本的“(类似于jQuery的DOM操作不是人类应该做的事情,所以)不再需要jQuery”。通过使用jQuery,大部分可以通过比标准API更节省几个字符的工作,根本上就不需要写一行代码了。

$.proxy、$.each、$.ajax、$.extendなどのかつて便利だったjQueryのユーティリティメソッド(DOM操作以外)は、現在ではすべて標準の代替方法があり、2023年現在では利点が全くありません。そのような気づきにより、残されたjQueryの依存を取り除きたいという衝動に駆られる場合に、このような記事が役立つのです。わずか数カ所の文字数を増やすだけで、100KB以上のライブラリを廃棄することができるなら、Web上での利点は大きいです。

你可以在原文中找到链接,但我个人推荐的是一个名为”你也许不需要 jQuery”的网站,内容很全面,值得一看。


备注:由于得到大家的支持,这篇文章成为了受欢迎的阅读内容,因此我会定期进行补充修正和更新。最后一次更新是在2023年6月。现在看起来可能有些脱节的书签评论等很可能是由于我进行的补充造成的。


以下是对以上内容的汉语解释:”React.js” 和 “ReactJS” 都是俗称。值得一提的是,从Angular的第二个版本开始,官方名称中不再包含JS。 ↩

“虚拟DOM” 在文章中也被称为 “React元素”。这是因为React的应用现在不仅仅限于浏览器上的DOM。本文中我们将使用著名且直观的名称 “虚拟DOM”。 ↩

React.createElement 部分可以通过编译器的配置等来修改,所以不仅限于React,其他库也可以使用JSX。 ↩

如果只是想使用React而不使用打包工具和编译器,那么可以选择不使用JSX。请参考官方网站上的这里和那里。 ↩

严谨地说,”纯渲染” 和 “无状态” 是两个不同的概念,但本文不作详细讨论。 ↩

广告
将在 10 秒后关闭
bannerAds