JSX
JSX 出现的原因
JSX 出现的主要原因是为了解决 React 中组件渲染的问题。在 React 中,用户界面是由组件构造的,而每个组件都可以看作是一个函数。这些组件或函数需要返回一些需要渲染的内容,而这些内容通常是 HTML 元素。
在早期的 JavaScript 中,如果要创建和操作HTML元素,需要使用一些相对较为复杂的 DOM API,这对开发者来说可能并不友好。而 JSX 就是一个 JavaScript 的语法扩展,它使得我们可以在 JavaScript 中直接写HTML(或者说看起来很像HTML)的语法,极大地提高了开发效率,也使得代码更加易读和易维护。
因此,JSX 的出现使得 React 的组件化开发变得更加简单和直观。通过JSX,开发者可以更加专注于组件的逻辑,而不是DOM操作,从而提高开发效率。
React 的一大亮点就是虚拟 DOM:可以在内存中创建虚拟 DOM 元素,由虚拟 DOM 来确保只对界面上真正变化的部分进行实际的 DOM 操作。和真实 DOM 一样,虚拟 DOM 也可以通过 JavaScript 来创建:
虽然通过以上的方式,就可以构建成 DOM 树,但这种代码可读性比较差,于是就有了 JSX 。JSX 是 JavaScript 的语法扩展,使用 JSX ,就可以采用我们熟悉的 HTML 标签形式来创建虚拟 DOM,也可以说 JSX 是 React.createElement
的一个语法糖。
什么是JSX
JSX(JavaScript XML),即在 JavaScript 语言里加入类 XML 的语法扩展。在 React 中,JSX 是一种 JavaScript 的语法扩展。它看起来很像 HTML,允许你在 JavaScript 中直接写 HTML 代码。JSX 能提高代码的可读性,使得你的代码更加直观和易于维护。实际上,JSX 只是提供了一种创建 React 元素的语法糖,它最终会被转换到普通的 JavaScript 函数调用和对象。因此,JSX 既是 React 的一个重要特性,也是编写 React 应用的一种推荐方式。有不少初学者对 React 的第一印象就是 JSX 语法,以至于会有这样的误解:
- JSX 就是 React?
- JSX 就是 React 组件?
- JSX 就是另一种 HTML?
- JSX 既能声明视图,又能混入 JS 表达式,那是不是可以把所有逻辑都写在 JSX 里?
总的来说,React 是一套声明式的、组件化的前端框架。顾名思义,声明组件是 React 前端开发工作最重要的组成部分。在声明组件的代码中使用了 JSX 语法,JSX 并非 HTML,它也并不代表组件的全部内容。
如何使用JSX
- JSX 标签类型
在 JSX 语法中,有两种标签类型:
- HTML 类型的标签:这种标签名需小写字母开头;
- 组件类型的标签(在之后的小节会详细介绍组件):这种标签必须以大写字母开头。
React 通过标签首字母的大小写来区分渲染的是标签类型。React 中的所有标签,都必须有闭合标签 />
- JSX 中使用 JavaScript 表达式
在 JSX 中,也可以使用 JavaScript 表达式,只需要使用{}
将表达式包裹起来就行。通常给标签属性传值或通过表达式定义子组件时会用到。例如下面代码:
JSX 中使用 JavaScript 表达式时,不能使用多行 JavaScript 语句。
JSX 的规则
- 只能返回一个根元素
- 标签必须闭合
- 自定义 React 组件时,组件本身采用的变量名或者函数名,需要以大写字母开头。
- 在 JSX 中编写标签时,HTML 元素名称均为小写字母,自定义组件首字母务必大写。
props
属性名称,在 React 中使用驼峰命名(camelCase),且区分大小写。
JSX 元素类型
SX 产生的每个节点都称作 React 元素,它是 React 应用的最小单元;React 元素有四种基本类型:
- React 封装的 DOM 元素,如
<div></div>
、<img />
等等元素会最终被渲染为真实的 DOM; - React 组件渲染的元素,如
<App />
,这部分元素会调用对应组件的渲染方法; - React Fragment 元素,
<React.Fragment></React.Fragment>
或者简写成<></>
,这一元素没有业务意义,也不会产生额外的 DOM,主要用来将多个子元素分组。 - React 中内置的一些有实际作用的组件:
<Suspense></Suspense>
、<Profiler></Profiler>
、<StrictMode></StrictMode>
等,他们不会将其直接渲染在 DOM 中。
JSX 的属性设置
React 对 DOM 元素的封装实际上是对整个浏览器 DOM 的一次 React 化标准化。例如,HTML 中容易引发混淆的 readonly="true"
,其W3C标准应为 readonly="readonly"
,而常被误用的 readonly="false"
实际上没有效果。但在 React 的 JSX 中,这就统一为 readOnly={true}
或 readOnly={false}
,这更接近JS的开发习惯。而对于样式中的 className="container"
,主要是因为 HTML 标签中的 class 是 JS 的保留字,所以需要避免使用。
在 React 组件渲染的元素中,JSX 的 props 应与自定义组件定义中的 props 相对应;如果没有特殊处理,那些没有对应 props 的元素会被忽略。这也是开发 JSX 时常会遇到的一个错误,那就是在组件定义中更改了 props 的属性名,但忘记了更改对应的 JSX 元素中的 props,导致子组件无法获取属性值。对于 Fragment 元素,它是没有 props 的。
JSX 子元素类型
JSX元素可以定义子元素。这里有一个重要的概念要理解:并非所有子元素都是子组件,但所有子组件一定都是子元素。
子元素的类型包括:
- 字符串,最终会被渲染成 HTML 标签里的字符串;
- 另一段 JSX,会嵌套渲染;
- JS 表达式,会在渲染过程中执行,并让返回值参与到渲染过程中;
- 布尔值、
null
值、undefined
值,不会被渲染出来; - 以上各种类型组成的数组。
- 字符串:最终会被渲染成 HTML 标签里的字符串。例如:
- 另一段JSX:会嵌套渲染。例如:
- JS 表达式:会在渲染过程中执行,并让返回值参与到渲染过程中。例如:
- 布尔值、
null
值、undefined
值:这些值在 JSX 中不会被渲染出来。例如:
- 以上各种类型组成的数组:例如:
以上代码会渲染出 "Hello"
,一个包含 "World"
的段落元素,以及数字3。null
、undefined
和 false
不会被渲染。
JSX 中的 JS 表达式
在JSX中,我们可以嵌入JavaScript表达式,这些表达式被大括号 { }
包围。这主要在两个方面被应用:
- 作为属性(Props)的值,也就是紧跟在
=
覆符号后的属性。
在这个例子中,我们定义了一个变量myClass,并用大括号把它作为className属性的值。
- 作为JSX元素的子元素,比如标签内的文本或者JS表达式结果。
在这个例子中,我们定义了一个变量text,并用大括号把它作为div元素的子元素。
JSX是声明性的,因此其内部不应包含命令式的语句,例如 if ... else ...。当你不确定JSX { } 里的代码是否是表达式时,你可以尝试将这部分代码直接赋值给一个JS变量。如果赋值成功,那么它就是一个表达式;如果赋值失败,那么你可以从以下四个方面进行检查:
- 是否有语法错误。
- 是否使用了for...of的声明式变体array.forEach ,这个中招几率比较高。
- 是否没有返回值。
- 是否有返回值,但不符合 props 或者子元素的要求。
有个 props 表达式的特殊用法: 展开语法,<Button {...defaultProps}>
利用的 JavaScript 中的展开语法把 defaultProps
这个对象的所有属性都传给 Button
这个组件。
JSX 中使用注释
如果你尝试在JSX中使用HTML的注释方法,你会发现它无法通过编译。因此,你需要使用 {/ 这是注释 /} 的格式来添加注释。在编译过程中,这种格式的注释会自动被识别为JS注释。
React 中的组件
在我们已经初步理解了JSX的基础上,接下来我们将探讨什么是组件,以及JSX与React组件的关系是什么。
组件化开发现已经成为前端开发的主流方法,几乎所有的前端框架都包含了组件的概念。在一些框架中,它被称为"Component",而在其他一些中则被称为"Widget"。然而在React中,组件被视为前端应用的核心。
什么是 react 组件
组件是对视图以及与视图相关的逻辑、数据、交互等的封装。如果没有组件这层封装,这些代码将有可能四散在各个地方,低内聚,也不一定能低耦合,这种代码往往难写、难读、难维护、难扩展。
React 组件层次结构从一个根部组件开始,一层层加入子组件,最终形成一棵组件树。
这棵树由节点组成,每个节点代表一个组件。例如,App、FancyText、Copyright 等都是树中的节点。
在 React 渲染树中,根节点是应用程序的 根组件。在这种情况下,根组件是 App,它是 React 渲染的第一个组件。树中的每个箭头从父组件指向子组件。
JSX 与 React 组件的关系
JSX就是React组件的语法糖,它让我们可以使用类似于HTML的语法来定义React组件。在React中,我们通常使用JSX来描述组件的UI结构。当我们编写JSX代码时,实际上我们是在定义React组件的渲染输出。
例如,我们可以定义一个名为"HelloWorld"的React组件,使用JSX来描述它的UI:
在上述代码中,<h1>Hello, world!</h1>
就是 JSX。当 React 渲染这个HelloWorld
组件时,它会将 JSX 转换为相应的 HTML,然后将其插入到 DOM 中。
组件的类型
组件化开发已经成为前端开发的主流趋势,市面上大部分前端框架都包含组件概念,有些框架里叫 Component,有些叫 Widget。在React框架中,主要有两种类型的组件:类组件和函数组件。类组件通常用于需要内部状态或生命周期方法的复杂情况,而函数组件则适用于无状态的、更简单的情况。但是从React 16.8版本开始,借助React Hooks,函数组件也可以拥有状态和生命周期方法。
每种组件类型都有其优势和适用场景,理解它们的作用和差异是成为一名高效的开发者的关键。
类组件
在React中,类组件是一种可以包含状态和生命周期方法的组件类型。类组件是ES6的类,它们继承自 React.Component
或 React.PureComponent
。
要定义一个 React 类组件,你需要扩展内置的 Component 类并定义一个 render()
方法。React 会在需要确定屏幕上显示什么内容时调用你的 render 方法。
例如:
类组件在定义是,同样可以使用属性:
在类组件中,通过 this
对象访问其自身 props
属性对象。
通过类组件构造 React 元素时,也可以为其指定属性赋值:
完整代码如下( github 中查看源码):
运行效果如下:
类组件还可以跟踪它们的状态(state),并使用状态更新来触发重新渲染。这使得类组件非常适合用于需要内部状态管理的复杂组件。
函数组件
将 UI 拆分成独立的、可复用的代码片段,并对每个代码片段进行单独处理。在 React 中,有两类常用的组件:函数组件(也叫无状态组件)和类组件(也叫 class 组件);然而,目前 React 官方以及社区的发展趋势,已经开始更多地推荐和支持使用函数组件,而不是类组件。因此,我们接下来的学习和探索,将主要围绕函数组件进行。
React 组件是一段可以使用标签进行扩展 的 JavaScript 函数。如下所示(你可以编辑下面的示例):
函数组件在某些方面可以替代类组件,它们的语法更为简洁明了。然而,函数组件面临着两个主要的挑战:
- 缺乏内部状态(state)
- 缺乏生命周期方法
这两个问题在某些情况下可能会限制函数组件的使用。但值得注意的是,自从 React 16.8 引入 Hooks 功能后,函数组件现在也可以拥有状态和生命周期方法,这大大增强了函数组件的功能性和灵活性。
state 与 props
在上述两个例子中,我们都提到了状态(state)和 props,并且在类组件中我们还使用了 props;那究竟什么是state和props呢?
props
React 组件使用 props 相互通信。props 是父组件向子组件传递数据的方式。无论是函数组件还是类组件,都可以接收 props。props 是只读的,也就是说:子组件不能修改父组件传递过来的 props。props 可能会让您想起 HTML 属性,可以传递任何 JavaScript 值,包括对象、数组和函数。
将 props 传递给组件
在下面这段代码中,Profile 组件没有向其子组件 Avatar 传递任何参数:
类组件写法如下:
如果要给 Avatar 组件添加参数,可以经过下面的流程:
- 将 props 传递给子组件
可以给Avatar
组件传递两个 props,一个person
和size
:
类组件的写法就是:
2.读取子组件内部的 props
类组件的写法:
state
在 React 中,state 是组件内部管理和存储数据的一种机制。理解 React 中的 state 非常重要,因为它决定了组件的状态和行为,直接影响到组件的渲染和交互。
state 是一个 JavaScript 对象,用于存储组件的内部数据;每个组件可以有自己的 state,用来描述组件当前的状态。
作用:
- 状态管理:通过 state 可以跟踪和管理组件的变化和交互。
- 数据驱动渲染:当 state 发生变化时,React 会重新渲染组件,以反映最新的状态。
使用场景:
- 存储和更新组件的动态数据。
- 控制组件的行为和外观。
- 响应用户输入和事件。
如何使用 State
初始化 State
- 在类组件中,通过构造函数初始化 state。
- 在函数式组件中,使用 useState hook 来初始化 state。
访问 State
- 在类组件中,使用 this.state.propertyName 访问 state 中的属性。
- 在函数式组件中,直接使用 state 变量。
更新 State
- 在 React 中,不直接修改 state。而是使用 setState 方法来更新 state。
- 在函数式组件中,使用 useState hook 返回的更新函数。
异步更新
setState
是异步的,因此 React 可以批量更新状态以提高性能。如果需要基于先前的 state 更新,可以使用函数形式的setState
。
完整代码如下:
- 类组件
- 函数组件
生命周期
通过上面的例子,我们知道 React 会把状态的变更更新到 UI,然后页面显示的内容更新,状态的变更过程必然会经历组件的生命周期。首先要知道所谓生命周期,就是组件从开始生成到最后消亡的过程, React 通常将组件生命周期分为三个阶段:装载、更新和卸载,我们怎么能确定组件进入到了哪个阶段呢?通过 React 组件暴露给我们的钩子函数就可以知晓。接下来我们将一起学习 React 组件的生命周期。
16.3 版本之前:
16.3 版本:
16.4 及之后:
通过上面的图片,我们可以看到 getDerivedStateFromProps 在 React v16.4 中有一定的改动,这个函数会在每次 render 之前被调用,也就意味着即使你的 props 没有任何变化,由父组件的 state 的改动导致的 render,这个生命周期依然会被调用,使用的时候需要注意。
根据上面的图片可以看出,在 React v16.4 中,getDerivedStateFromProps
方法有了一些改动。现在,这个生命周期方法在每次组件即将渲染之前都会被调用,不再只在接收新 props
时触发。这意味着,即使组件的 props
没有实际变化,只要父组件的 state
发生改变导致重新渲染,这个生命周期方法也会被执行。因此,在使用时需要特别注意这一点。
挂载阶段
挂载阶段组件被创建,然后组件实例插入到 DOM 中,完成组件的第一次渲染,该过程只会发生一次,在此阶段会依次调用以下这些方法:
constructor
getDerivedStateFromProps
render
componentDidMount
constructor
组件的构造函数是第一个被执行的部分。如果我们显式定义了构造函数,就必须在其中调用 super(props)
,这样才能确保在构造函数内部正确获取到 this
。这涉及到了 ES6 类的继承机制,详细内容可以参考阮一峰的 《ECMAScript 6 入门》。
在构造函数里一般会做两件事:
- 初始化组件的
state
- 给事件处理方法绑定
this
getDerivedStateFromProps
这是一个静态方法,因此不能在其内部使用 this
。它接收两个参数:
props
接收到的新属性state
当前组件的状态对象
该方法应返回一个对象,用于更新当前的状态对象;如果不需要更新,则返回 null
。这个方法会在组件挂载时或者接收到新的 props
、或调用了 setState
和 forceUpdate
时被调用。例如,当我们接收到新的属性并希望更新状态时,可以在此方法内进行处理。
现在我们可以显式传入 counter
,但出现了一个小问题:如果我们希望通过点击事件来增加 state.counter
的值,会发现它始终保持着 props
传入的初始值,没有发生任何变化。这是因为在 React 16.4 及更高版本中,setState
和 forceUpdate
也会触发 getDerivedStateFromProps
生命周期方法。因此,当组件内部的状态发生变化时,会再次调用该方法,并将状态值重置为 props
的值。为了解决这个问题,我们需要在 state
中添加一个额外的字段来记录之前的 props
值。
render
React 中最核心的方法是 render
方法。一个 React 组件必须包含 render
方法,它根据组件的状态 state
和属性 props
来决定返回什么内容,从而渲染组件到页面上。
在 render
方法中,通常会返回以下类型中的一个:
- React 元素:包括原生的 DOM 元素或者其他 React 组件。
- 数组和 Fragment(片段):可以返回多个元素作为一个整体。
- Portals(插槽):可以将子元素渲染到不同的 DOM 子树中。
- 字符串和数字:会被渲染成 DOM 中的文本节点。
- 布尔值或
null
:表示不渲染任何内容。
componentDidMount
在组件挂载后调用 componentDidMount
方法时,我们可以获取到 DOM 节点并进行操作,例如对 canvas、svg 进行绘制,或者发起服务器请求等操作。
然而,需要注意的是,在 componentDidMount
中调用 setState
会触发一次额外的渲染过程,导致多一次 render
方法的执行。尽管这次渲染是在浏览器刷新屏幕前进行的,用户通常不会察觉到,但在开发过程中,应尽量避免这种做法以避免潜在的性能问题。为了优化性能,我们应该尽早在 constructor
中初始化组件的 state
对象,而不是在 componentDidMount
中进行状态的初始化操作。
在组件挂载之后,将计数数字变为10。
更新阶段
当组件的 props 改变了,或者组件内部调用了 setState 或 forceUpdate 方法,都会触发更新和重新渲染的过程。在这个阶段,React 组件会按照以下顺序依次调用这些方法:
static getDerivedStateFromProps(nextProps, prevState)
: 当 props 发生变化时调用,用于根据新的 props 更新组件的状态。这个静态方法返回一个对象来更新状态,或者返回null
表示不需要更新状态。shouldComponentUpdate(nextProps, nextState)
: 在重新渲染之前调用,用于判断是否需要重新渲染组件。默认返回true
,可以根据新的 props 和 state 来进行优化判断,避免不必要的渲染。render()
: 根据最新的 props 和 state 返回需要渲染的 React 元素、数组、Fragment、Portals、字符串、数字或null
。getSnapshotBeforeUpdate(prevProps, prevState)
: 在最终渲染之前调用,用于获取更新前的 DOM 状态。它的返回值将作为componentDidUpdate
方法的第三个参数传递给后者,常用于处理 DOM 更新前后的差异。componentDidUpdate(prevProps, prevState, snapshot)
: 在组件更新完成后调用,可以执行与更新后的 DOM 交互的操作。通常用于处理网络请求、手动操作 DOM 或者更新状态的逻辑。
这些方法协同工作,确保 React 组件能够响应外部变化,并及时更新用户界面。
getDerivedStateFromProps
这个方法在挂载阶段已经说过了,这里不再赘述,记住在更新阶段,无论接收到新的 props,还是调用了 setState
或者 forceUpdate
,这个方法都会被触发。
shouldComponentUpdate
在讲这个生命周期函数之前,我们先来探讨两个问题:
- setState 函数在任何情况下都会导致组件重新渲染吗?例如下面这种情况:
- 如果没有调用 setState,props 值也没有变化,是不是组件就不会重新渲染?
我们先探讨上面两个问题:
第一个问题:当 setState 被调用时,React 会更新组件的状态并重新渲染组件。setState 会合并新的状态对象到当前状态,然后触发 render 方法。 这是 React 的默认行为,用于确保组件反映最新的状态和 props。
当然也有特殊情况,如果在 setState 中更新的状态与当前状态相同,React 可能会跳过重新渲染。 在这种情况下,setState 被调用了,但是状态对象 { number: this.state.number } 与之前的状态相同。React 进行状态合并后,发现新的状态和之前的状态没有变化,默认情况下,React 会优化跳过 render 调用,避免不必要的渲染。这种优化有助于提升性能,避免不必要的计算和 DOM 操作。
第二个问题:如果是父组件重新渲染时,不管传入的 props 有没有变化,都会引起子组件的重新渲染。那么有没有什么方法解决在这两个场景下不让组件重新渲染进而提升性能呢?
React 通过以下机制来优化和控制组件的重新渲染:
- 浅比较(Shallow Compare):
React 在内部会对新的状态和旧的状态进行浅比较。只有当比较结果不同时,React 才会决定更新组件。因此,如果传递给 setState 的对象和当前状态对象是相同的,React 就会跳过渲染。 - shouldComponentUpdate 方法:
在类组件中,你可以通过覆盖shouldComponentUpdate
方法来自定义渲染行为。,这个生命周期函数是用来提升速度的,它是在重新渲染组件开始前触发的,默认返回true
,我们可以比较this.props
和nextProps
,this.state
和nextState
值是否变化,来确认返回true
或者false
。当返回false
时,组件的更新过程停止,后续的render
、componentDidUpdate
也不会被调用。
卸载阶段
在 React 的卸载阶段(Unmounting Phase),只有一个生命周期函数:componentWillUnmount
。这个函数在组件即将从 DOM 中移除之前调用。在这一阶段,你可以执行一些必要的清理操作,以确保组件被正确地释放,并防止内存泄漏和其他潜在问题。
典型用法
- 清除定时器
如果组件中使用了setTimeout
或setInterval
,你需要在componentWillUnmount
中清除这些定时器,以防止在组件卸载后它们继续运行,导致潜在的资源浪费或试图访问已被卸载的组件。
- 取消未完成的网络请求
在组件卸载之前取消未完成的网络请求,避免在组件已经卸载后试图更新组件状态或访问组件数据。通常,你可以通过在请求中使用一个标志或取消令牌来实现这一点。
- 移除事件监听器
如果组件在componentDidMount
或其他地方添加了事件监听器(如窗口的 resize 事件或自定义的事件),你需要在componentWillUnmount
中移除这些监听器,以防止在组件卸载后事件处理函数仍然被调用。
- 清理资源和对象
如果组件中使用了某些资源或创建了需要手动清理的对象(如 WebSocket 连接、文件资源等),在 componentWillUnmount 中释放这些资源是一个好习惯。
生命周期所演示的代码都可以在 github 上找到。