学习笔记-react

259 阅读21分钟

受控组件 & 非受控组件

受控组件:表单数据交由 state 对象管理。特点是可以实时得到表单数据。代码相对复杂。

非受控组件: 表单数据交由 DOM 节点管理。特点是表单数据在需要时利用 ref 进行获取。代码实现相对简单。

选用标准:大多数情况下,推荐使用受控组件处理表单数据。如果表单在数据交互方面比较简单,使用非受控表单,否则使用受控表单。

Hook 相关

1. 类组件的不足(Reacthooks 要解决的问题):

  1. 缺少逻辑复用机制:使用类组件时都是使用渲染属性和高阶组件来实现逻辑复用。为了复用逻辑增加无实际渲染效果的组件,增加了组件层级,显得十分臃肿,增加了调试的难度以及运行效率的降低。
  2. 类组件经常会变的很复杂难以维护:将一组相干的业务逻辑拆分到了多个生命周期函数中;在一个生命周期函数内存在多个不相干的业务逻辑。
  3. 类成员方法不能保证this指向的正确性

2. useState 使用细节

参数:

  1. 接收唯一的参数即状态初始值,初始值可以是任意数据类型
  2. 返回值为数组,数组中存储状态值和更改状态值的方法。 方法名称约定以 set 开头,后边加上状态名称
  3. 方法可以被调用多次,用以保存不同状态值
  4. 参数可以是一个函数,函数返回什么,初始状态就是什么,函数只会被调用一次,用在初始值是动态值的情况

设置状态值方法:

  1. 设置状态值方法的参数可以是一个值也可以是一个函数
  2. 设置状态值方法的方法本身是异步的 如图, handleCount 方法中,setCount 的方法是异步的,所以 setCount 方法和第 11 行代码并不会按顺序执行。

image.png

3. useReducer

useReducer 是另一种让函数组件保存状态的方法。它的使用方式和 redux 类似。调用 useReducer 方法,它的第一个参数是一个 reducer 函数,第二个参数是状态的初始值。返回一个数组,数组的第一项就是要处理的状态,第二项是 dispatch 方法,通过 dispatch 方法可以触发 action。

image.png

useReducer 相对于 useState 的好处:比如这个组件的子组件想要修改 count 的值,useState 就需要把修改的方法都传给子组件,而用 useReducer 可以将 dispatch 传给子组件,子组件用 dispatch 调用对应的 action 就行了。

4. Context & useContext

useContext 在跨组件层级获取数据时简化获取数据的代码。

先看一段 Context 的使用:

image.png

  1. 使用 createContext() 方法创建 context,然后用它的 provider 包裹子组件,并传入属性 value 为初始值。
  2. 在子组件中 使用 context 的 Consumer 包裹可以拿到 value。

再来看看使用 useContext:

image.png

使用 useContext(countContext) 可以拿到刚才的value,简化了获取数据的代码。

5. useEffect

让函数型组件拥有处理副作用的能力,类似生命周期函数。

1,useEffect 如果没有第二个参数,则组件挂载完成和有状态更新都会执行。

2,如果有第二个参数,应该为数组,数组中是状态,表示组件挂载完成和指定的状态更新的时候执行副作用。

3,如果第二个参数为空数组,则表示任何状态更新都不会执行副作用,即只有组件挂载完成才会执行副作用。

image.png

useEffect 解决的问题:

  1. 按照用途将代码进行分类(将一组相干的业务逻辑归置到了同一个副作用函数中)
  2. 简化重复代码,使组件内部代码更加清晰

useEffect 中的参数函数不能是异步函数,因为 useEffect 函数要返回清理资源的函数,如果是异步函数就变成了返回 Promise。所以如果在 useEffect 中想要执行异步函数,可以这样做:

image.png

6. useMemo

useMemo 的行为类似 Vue 中的计算属性,可以检测某个值的变化,根据变化值计算新值。

useMemo 会缓存计算结果,如果检测值没有发生变化,即使组件重新渲染,也不会重新计算。此行为可以有助于避免在渲染上进行昂贵的计算。

image.png

7. memo

性能优化,如果本组件中的数据没有发生变化,阻止组件更新。类似类组件中的 PureComponent 和 shouldComponentUpdate

image.png

8. useCallback

性能优化。对函数进行缓存,使组件重新渲染时得到相同的函数实例。

当父组件需要给子组件传递一个函数的时候,如果父组件中任意一个状态发生变化重新渲染的时候,都会重新生成这个函数,向子组件传递的将会是不同的函数实例。这对于子组件来说就是一个状态更新,即使使用了 memo, 还是会重新渲染,造成浪费。

所以可以使用 useCallback 将函数缓存。这样每次传给子组件的都是同一个函数实例。

useCallback 第一个参数就是要执行的函数,第二个参数是一个数组,是需要监听的值,当值发生变化,才会生成新的函数实例。

image.png

9. useRef

  1. 用来获取 DOM 元素对象。 将 useRef() 返回的对象赋值给需要获取的 DOM 的ref 属性,然后通过这个对象的 current 属性就可以拿到 DOM 元素。eg: username.current

image.png

  1. 保存数据(跨组件周期)。 即使组件重新渲染,保存的数据仍然存在,保存的数据被更改不会触发组件重新渲染。 useState 保存的数据是状态数据,当数据改变以后会触发组件重新渲染。而 useRef 保存的数据不是状态数据,更改不会触发组件重新渲染。通常用 useRef 保存一些程序在运行过程中的辅助的数据。

10. 自定义 Hook

  • 自定义 Hook 是标准的封装和共享逻辑的方式
  • 自定义 Hook 是一个函数,其名称以 use 开头
  • 自定义 Hook 其实就是逻辑和内置 Hook 的组合

image.png

11. react 路由 Hooks

react-router-dom 路由提供的钩子函数。

useHistory, useLocation, useRouteMatch, useParams.

当进入某一个路由组件,在这个组件的 props 属性下会多出几个对象,比如 history, location, match. 这几个钩子函数就是用来获取对应的对象信息的。其中 useParams 是用来获取 match 下的 params 对象。

image.png

Formik

Formik 是 React 官方推荐使用的用于表单增强的第三方模块。有了 Formik, 我们就可以专注处理自己的业务逻辑,不再需要分心处理表单技术上的细节。

Formik:增强表单处理能力,简化表单处理流程。

useFormik

demo:

image.png

根据 demo,对 useFormik 进行简单的介绍:

  • 通过 formik 模块解构出 userFormik 方法。
  • userFormik 接收一个对象为参数。其中属性 initialValues 为表单数据的初始值,其中的属性需要和表单项的 name 属性值对应,validate 为表单数据的校验规则,onSubmit 为表单提交的方法。
  • userFormik 方法返回的值是一个对象。从该对象的属性中可以拿到对应的 submit 方法, value 值等。formik.handleSubmit 是提交方法; formik.values 可以拿到表单所有数据的 value,通过 .username 可以拿到 username 的值; formik.handleChange 可以拿到 change 方法等。
  • formik.handleBlur 是失去焦点方法,formik.touched 可以检查表单元素的值是否被改动过。由于任意文本框修改都会进行 validate 校验,会导致还没有输入的 input 框也出现错误提示。所以需要给文本框传入失去焦点的方法,并用 formik.touched 来判断元素是否改变来避免这一错误。
  • formik.getFieldProps 方法用来获取表单项中的 value, onChange, onBlur 属性,然后以对象的形式返回。所以我们可以通过解构 formik.getFieldProps('username') 的方式替换三个属性,解决重复代码的问题。
  • formik 的表单规则校验可以结合 yup 来使用,使代码更简洁,如下图。将 validate 属性换成 validatioinSchema, 这个属性用来配合 yup 使用的,这个属性的值就是验证规则。使用 Yup.object 方法定义规则。传入参数为对象,对象中的属性表示需要验证的表单项。然后利用 Yup 提供的方法链式调用来书写验证规则,具体可以看 yup 的使用文档

image.png

formik 组件的方式构建表单

formik 组件简单使用

formik 中给我们提供了一些列的组件让我们构建表单,使用组件的方式构建表单可以让代码看起来更加整洁,很多细节代码在组件内部都封装好了。formik 中为我们提供了 Formik,Form, Field,ErrorMessage 组件。

image.png

  • Formik 组件包裹在表单的最外层,可以传入initialValues,onSubmit, validationSchema 等属性。传入 Formik 的属性其实就是传入 useFormik 方法的对象的内容。
  • Form 组件就是表单组件,其中包裹一个一个的表单项。
  • Field 就是具体的表单项。name 属性指定当前表单项的名字。Field 组件默认被渲染成文本框。如果要生成其他表单元素,可以使用 as 属性指定要生成的表单元素。
  • ErrorMessage 组件用来显示表单项在没有通过验证时的提示信息。通过 name 属性指定提示哪个表单项的信息。

useField

useField 用来实现自定义表单控件。

如下图,是一个自定义 password 的例子。useField 传入 props 对象,props 对象中包含的属性是 type,name, placeholder 这些表单元素的属性。返回值是一个数组,数组的第一项 field 存储的是表单控件需要的属性 value,onChange,onBlur,第二项 meta 存储的是和表单验证相关的信息。

image.png

自定义 checkbox:在这个例子中,input 要自定义 onChange 方法,所以没有把 {...field} 传入。checkbox 的值是数组,点击某一项,需要把这项的值插入数组或者从数组中移除。checkbox 的值就存储在 meta.value 中,但是这个值不能直接更改,需要使用内部提供的方法,在 useField 返回的数组的第三项 helpler 对象中有一个方法 setValue 可以修改 value 值。change 方法中利用 Set 实例对数组进行判断有就删,没有就加。 checkbox 的默认选中可以通过 checked 属性判断 meta.value 中是否存在当前 value 实现。

image.png

import React from "react";
import { Formik, Form, Field, ErrorMessage, useField } from "formik";
import * as Yup from "yup";

function Checkbox ({label, ...props}) {
  const [field, meta, helper] = useField(props);
  const { value } = meta;
  const { setValue } = helper;
  const handleChange = () => {
    const set = new Set(value);
    if (set.has(props.value)) {
      set.delete(props.value);
    }else {
      set.add(props.value);
    }
    setValue([...set])
  }
  return <div>
    <label htmlFor="">
      <input checked={value.includes(props.value)} type="checkbox" {...props} onChange={handleChange}/> {label}
    </label>
  </div>
}

function App() {
  const initialValues = {username: '', hobbies: ['足球', '篮球']};
  const handleSubmit = (values) => {
    console.log(values);
  };
  const schema = Yup.object({
    username: Yup.string()
      .max(15, "用户名的长度不能大于15")
      .required("请输入用户名"),
  });
  return (
    <Formik
      initialValues={initialValues}
      onSubmit={handleSubmit}
      validationSchema={schema}
    >
      <Form>
        <Field name="username" />
        <ErrorMessage name="username" />
        <Checkbox value="足球" label="足球" name="hobbies"/>
        <Checkbox value="篮球" label="篮球" name="hobbies"/>
        <Checkbox value="橄榄球" label="橄榄球" name="hobbies"/>
        <input type="submit"/>
      </Form>
    </Formik>
  );
}

export default App;

React 组件性能优化

  1. 组件卸载前执行清理操作
  2. 通过纯组件提升性能。 类组件继承 PureComponent, 函数组件使用 memo 方法。 纯组件会对组件输入数据进行浅层比较,如果数据没变,组件不会重新渲染。 浅层比较就是比较引用数据类型在内存中的引用地址,比较基本数据类型的值。浅层比较和进行 diff 比较操作对比消耗更少的性能,因为 diff 操作会重新遍历整颗 virtualDOM 树,而浅层比较只操作当前组件的 state 和 props。
  3. 类组件通过 shouldComponentUpdate 生命周期函数提升组件性能。 返回 true 重新渲染组件,返回 false 阻止重新渲染。第一个参数是 nextProps, 第二个参数是 nextState。纯组件只能进行浅层比较,要深层比较对象中的属性的变化,可以使用 shouldComponentUpdate 编写自定义逻辑。
  4. 函数组件使用 memo 可以传递自定义比较逻辑来比较对象中的变化。 memo 的第二个参数是一个比较函数,这个函数有两个参数,prevProps 和 nextProps,返回 false 则重新渲染,返回 true 则阻止重新渲染(和 shouldComponentUpdate 相反)。
  5. 使用组件懒加载可以减少 bundle 文件大小,加快组件呈递速度。 如果组件不采用懒加载,所有的组件文件代码都会被打包到同一个 bundle 文件中,这个 bundle 文件就会变的非常的庞大,当我们首次加载这个庞大的文件,页面空白时间就会很长,用户体验不好。使用懒加载,不同的组件打包到不同的文件中,当第一个访问 react 应用时,只加载了首页对应的组件文件,bundle 文件就会很小,很快就可以看到页面显示。

image.png

在引入组件的时候,使用 react 的 lazy 方法来引入。 (此时可以用 /* webpackChunkName: "Home" */ 的方式来指定组件打包的名称,如果不指定则为数字。)

在使用 Suspense 来指定页面在加载过程中显示的 UI。

如下图,还可以根据条件进行组件懒加载,适用于组件不会随条件频繁切换的情况。

image.png

  1. 使用 Fragment 避免额外标记。 react 组件中返回的 jsx 如果有多个同级,必须有一个共同的父级,但是如果使用 div,就会多一个无意义的标记,如果每个组件都多出这样一个无意义的标记的话,浏览器渲染引擎的负担就会加剧。所以可以使用 react 的 fragment 占位符标记代替 div,既满足了拥有共同父级的要求又不会多处无意义的标记。

image.png

如果觉得 fragment 写起来太长,也可以使用空标记,此时无需引入 fragment。

image.png

  1. 不要使用内联函数定义。 在使用内联函数后,render 方法每次运行时都会创建该函数的新实例,导致 react 在进行 Virtual DOM 比对时,新旧函数比对不相等,导致 react 总是为元素绑定新的函数实例,而旧的函数实例又要交给垃圾回收器处理。

image.png

在这里, onChange 绑定了一个内联函数,每次 render 渲染的时候由于引用地址不同,都会生成新的函数实例。解决方法是定义一个方法。

image.png

  1. 在构造函数中进行函数 this 绑定。 在类组件中如果使用 fn(){} 这种方式定义函数,函数 this 默认指向 undefined,这是需要更正函数 this 指向。可以在构造函数中进行更正,也可以在行内更正,两者看起来没有太大区别,但是对性能的影响是不同的。

在构造函数中只执行一次,所以函数 this 指向更正的代码也只执行一次。但是在 render 方法中每次执行都会调用 bind 方法生成新的函数实例。

image.png

  1. 避免使用类组件中的箭头函数。 在类组件中使用箭头函数不会存在 this 指向问题,但是该函数会被添加为类的实例对象属性,而不是原型对象属性。如果组件被多次重用,每个组件实例对象中都将会有一个相同的函数实例,降低了函数实例的可重用性,造成了资源浪费。 所以正确的做法还是使用 fn(){} 并在构造函数中更正 this 指向。

  2. 避免使用内联样式属性。 当使用内联 style 为元素添加样式时,内联 style 会被编译为 javascript 代码,将样式规则映射到元素身上,浏览器就会花费更多的时间执行脚本和渲染 UI,从而增加了组件的渲染时间。

  3. 避免重复无限渲染。 render 方法中不要调用 setState 方法,不要使用其他手段查询更改原生 DOM 元素,以及其他更改应用程序的任何操作。否则会发生 render 方法递归调用导致应用报错。

  4. 为组件创建错误边界。 默认情况下组件渲染错误会导致整个应用程序中断,创建错误边界可以确保在特定组件发生错误时应用程序不会中断。

错误边界设计到两个生命周期函数,分别是 getDerivedStateFromError 和 componentDidCatch.

getDerivedStateFromError 为静态方法,方法中需要返回一个对象,该对象会和 state 对象进行合并,用于更改应用程序状态。

componentDidCatch 方法用于记录应用程序错误信息,该方法的参数就是错误对象。

注意:错误边界不能捕获异步错误,比如点击按钮时发生的错误。

image.png

SSR

概念

CSR: Client Side Rendering 客户端渲染

数据和 html 的拼接是在客户端完成的,是在浏览器中使用 javascript 完成的。服务器仅需要返回 JSON 数据就可以了。

客户端渲染存在的问题:

  1. 首屏等待时间长,用户体验差。

    客户端渲染过程:客户端向服务器端发送请求获取应用的首页面,服务器端返回一个空 html,这个 html 包含一些 css,js 外链,这时用户处于等待状态; 然后客户端执行这个空的 html 文档,执行的过程中向服务端发送请求获取这些 css,js 外链,这是用户还是处于等待状态;外链文件加载完成后执行对应的 js 文件,这时向服务端发送请求获取页面所需数据,此时还是等待状态;数据请求到后,客户端将数据和 html 拼接好,这时,用户才能看到界面。

  2. 页面结构为空,不利于 SEO。 搜索引擎爬虫工具爬取页面内容时,因为页面内容时空的,什么也爬取不到。

SSR: Server Side Rendering 服务端渲染

数据和 html 的拼接是在服务器端完成的。客户端向服务端发送请求,服务端接收到请求,将数据和 html 拼接好一起返回给客户端,客户端将接收到的数据显示出来。

服务端渲染解决了客户端渲染存在的问题。

服务端渲染如何解决客户端渲染的两个问题: (服务端渲染过程:) 客户端用服务端发送请求,服务器端接收到请求将数据和 html 拼接好返回客户端,客户端接收到信息是有数据的页面,用户可以看到页面内容,此时是静态页面;然后浏览器执行文档,去请求 js 文件,请求成功后去执行 js 文件,执行完页面就有了动态效果。

React SSR 同构 同构指的是代码复用,即实现客户端和服务器端最大程度的代码复用,例如路由,redux。

Next.js

Next.js 是 React 服务端渲染应用框架,用于构建 SEO 友好的 SPA (单页)应用。

  1. 支持两种预渲染方式,静态生成 和 服务器端渲染。
  2. 基于页面的路由系统,路由零配置。
  3. 自动代码拆分,优化页面加载速度。
  4. 支持静态导出,可将应用导出为静态网站。
  5. 内置 CSS-in-JS 库 styled-jsx。
  6. 方案成熟,可用于生产环境,世界许多公司都在使用。
  7. 应用部署简单,拥有专属部署环境 Vercel, 也可以部署在其他环境。

创建页面

  • 在 Next.js 中,页面是被放置在 pages 文件夹中的 React 组件。
  • 组件需要被默认导出
  • 组件文件中不需要引入 React
  • 页面地址与文件地址是对应的关系

image.png

pages/index.js -> /

pages/list.js -> /list

pages/post/first.js. -> /post/first

页面跳转

页面与页面之间通过 Link 组件进行跳转

image.png

  • Link 组件默认使用 javascript 进行跳转,即 SPA 形式的跳转
  • 如果浏览器禁用了 javascript,则使用链接跳转,即 a 标签跳转的形式
  • Link 组件只能有 href 属性,其余属性添加在 a 标签上
  • Link 组件在生产环境可以通过预取功能自动优化应用程序以获得最佳性能。预取就是当页面内容都加载完毕处于空闲阶段,浏览器会预先将 Link 标签对应的页面加载,使后续用户跳转这个页面时更快速。

静态资源

静态资源放在根目录下的 public 文件夹下,访问方式如下:

public/images/1.jpg -> /images/1.jpg

public/css/base.css -> /css/base.css

元数据

通过 Head 组件修改元数据

image.png

CSS 样式

全局样式文件 在 pages 文件夹中新建 _app.js 并创建对应的 css 文件:

import '../styles.css'

// 新创建的 `pages/_app.js` 文件中必须有此默认的导出(export)函数
export default function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

组件级 CSS

CSS 模块约定样式文件的名称必须为 [name].module.css

image.png

CSS-in-JS

在 Next.js 中内置了 styled-jsx, 它是一个 CSS-in-JS 库, 允许在 React 组件中编写 CSS, CSS 仅作用于组件内部

image.png

预渲染

预渲染是指数据和 html 的拼接在服务器端提前完成。可以使 SEO 更加友好。会带来更好的用户体验,无需运行 javascript 就可以查看到界面。浏览器加载一个页面时,其 JavaScript 代码将运行并使页面完全具有交互性。

在 Next.js 中有两种预渲染的方式: 静态生成服务器端渲染。这两种方式是生成 html 的时机不同。静态生成是在构建时生成 html,即运行 npm run build 的时候,之后每次访问都使用构建好的 html。服务器端渲染是在请求的时候生成 html,每次请求都会重新生成 html。

Next.js 允许开发者为每个页面选择不同的预渲染方式. 不同的预渲染方式拥有不同的特点. 应根据场景进行渲染. 但建议大多数页面建议使用静态生成.

静态生成一次构建, 反复使用, 访问速度快. 因为页面都是事先生成好的. 适用场景:营销页面、博客文章、电子商务产品列表、帮助和文档

服务器端渲染访问速度不如静态生成快, 但是由于每次请求都会重新渲染, 所以适用数据频繁更新的页面或页面内容随请求变化而变化的页面.

静态生成 (getStaticProps,getStaticPaths)

如果组件不需要在其他地方获取数据, 直接进行静态生成.因为 Next.js 默认使用静态生成。

如果组件需要在其他地方获取数据, 在构建时 Next.js 会预先获取组件需要的数据, 然后再对组件进行静态生成.

getStaticProps

getStaticProps 获取组件静态生成所需的数据,通过 props 的方式将数据传递给组件。该方法是一个 async 异步函数,需要在组件内部导出。开发模式下,改为在每个请求上运行。

image.png

getStaticPaths

如果页面使用动态路由并且使用 getStaticProps,则需要使用 getStaticPaths 来定义参数,有多少个参数就会在构建时静态生成多少个 html。

在构建应用时, 先获取用户可以访问的所有路由参数, 再根据路由参数获取具体数据, 然后根据数据生成静态 HTML.

  1. 创建基于动态路由的页面组件文件, 命名时在文件名称外面加上[], 比如[id].js
  2. 导出异步函数 getStaticPaths, 用于获取所有用户可以访问的路由参数
  3. 导出异步函数 getStaticProps, 用于根据路由参数获取具体的数据

image.png

image.png

fallback:false 表示当访问的路由参数没有在 getStaticPaths 返回时,显示 404 页面。 fallback:true 表示此时不显示404 页面,将加载这时的数据生成新的 html。可以为其设置等待状态显示的UI。下图中 router.isFallback 为 true 时表示当前页面暂未生成。

image.png

可以通过在 pages 文件夹中创建 404.js 文件来自定义 404 页面。

注: getStaticPaths 和 getStaticProps 只运行在服务器端, 永远不会运行在客户端, 甚至不会被打包到客户端 JavaScript 中, 意味着这里可以随意写服务器端代码, 比如查询数据库.

服务器端渲染(getServerSideProps)

采用服务器端渲染,在组件中带出 getServerSideProps 异步方法

image.png