青训营-react基础与实践

169 阅读11分钟

一、React简介与特点

(一)React简介

React:用于构建用户界面的javascript库。

(二)React特点

  1. 声明式:React采用声明式编程,使代码更易于理解和调试。举例:React 的 JSX 语法。它允许你直接在 JavaScript 代码中编写类似 HTML 的标记,这样可以更直观地描述组件的结构和内容。

  2. 组件化:React使用组件化思想,将复杂的系统拆分成多个功能模块,便于重复利用。

  3. 跨平台编写:React可以在多个平台上使用,包括Web、移动端和桌面应用程序。举例:使用 React Native 来构建 iOS 和 Android 应用程序。你可以使用相同的代码库来构建两个平台的应用程序,而不需要为每个平台单独编写代码。

(三)React哲学

React 用 JavaScript 构建 快速响应大型 Web 应用程序的首选方式之一。它在 Facebook 和 Instagram 上表现优秀。

等待资源加载

浏览器线程执行

image.png

3.1 React哲学_lazy和suspense

React 解决等待资源加载的问题,可以使用 React.lazyReact.Suspense 来实现代码分割和按需加载。这样可以减少首次加载时需要下载的资源数量,从而提高页面加载速度。

浏览器线程执行时,由于 JavaScript 是单线程的,所以在执行长时间运算时会阻塞页面渲染。React 通过异步更新和时间切片来解决这个问题。异步更新可以让 React 在空闲时间执行更新操作,而不会阻塞页面渲染。时间切片则可以将长时间运算拆分成多个小任务,在空闲时间依次执行,从而避免阻塞页面渲染。

下面是一个使用 React.lazyReact.Suspense 的简单示例:

import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

在这个示例中,我们使用 React.lazy 来动态导入 OtherComponent 组件。当 MyComponent 组件渲染时,OtherComponent 组件会被按需加载。在组件加载过程中,我们使用 React.Suspense 来显示一个 fallback 内容,提示用户正在加载。

image.png

3.2 React哲学_ErrorBoundary

ErrorBoundary 是 React 16 中引入的一种方法,用于捕获和处理组件 UI 部分中发生的JS错误。因此,错误边界仅捕获 生命周期方法、渲染方法和 useEffect 等钩子中发生的错误。

要创建一个错误边界,我们只需创建一个类组件并定义一个状态变量来确定错误边界是否捕获了错误。我们的类组件还应至少具有三种方法:

  • 一个名为 getDerivedStateFromError 的静态方法,用于更新错误边界的状态。
  • 一个 componentDidCatch 生命周期方法,用于在我们的错误边界捕获错误时执行操作,例如记录到错误记录服务。
  • 一个渲染方法,用于渲染我们的错误边界的子元素或在发生错误时渲染后备 UI。

下面是一个简单的错误边界示例:

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    // Update state so the next render will show the fallback UI.
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    // You can also log the error to an error reporting service
    logErrorToMyService(error, errorInfo);
  }

  render() {
    if (this.state.hasError) {
      // You can render any custom fallback UI
      return <h1>Something was wrong.</h1>;
    }

    return this.props.children;
  }
}

3.3 React哲学_更新流程

React 的更新流程包括调度器(Scheduler)、渲染器(Renderer)和协调器(Reconciler) 三个部分。

  • 调度器负责确定何时执行工作,它可以将工作拆分为多个小块,以便在执行工作的同时保持浏览器的响应能力。

  • 渲染器负责将 React 组件渲染到目标平台,例如 DOM、Canvas 或原生移动应用。不同的渲染器有不同的实现方式,但它们都遵循相同的模式。

  • 协调器负责比较新旧两棵树,并计算出需要进行的最小更改。这个过程称为协调(Reconciliation)。Reconciler 起作用的阶段我们称为 render 阶段,Renderer 起作用的阶段我们称为 commit 阶段。

image.png

在 React 中,最多同时存在两棵 fiber tree。一棵是当前屏幕上显示内容对应的 fiber tree,称为 current fiber tree;另一棵是正在内存中构建的 fiber tree,称为 workInProgress fiber tree。workInProgress fiber tree 构建完成后,React 会使用它直接替换 current fiber tree。

image.png

3.4 React_优缺点

优点:

1、快速响应:Fiber

2、组件化:复用性强

3、声明式导航

4、跨平台:只需修改渲染器

缺点:

需要使用 JSX 语法,学习曲线较陡峭。此外,React 不适合单独做一个完整的框架,做大型项目需要和其他框架组合使用。

image.png

二、React基础

(一)用react开发web应用

  • 架构:打包配置,JSX-babel-js,加载优化和错误降级。(其实我个人觉得这里的架构指的是Scheduler(调度)Reconciler(协调)Renderer(渲染)。这些部分协同工作,使得 React 应用具有高效的性能和良好的可维护性。)

  • 路由:在 React web 单页面应用中,页面级 UI 组件的展示和切换完全由路由控制,每一个路由都有对应的 URL 及路由信息,我们可以通过路由统一高效地管理我们的组件切换,保持 UI 与 URL 同步,保证应用的稳定性及友好体验。

  • UI: React 的内置 Hooks 非常适合 UI ,复用逻辑抽离成hook.

  • 状态管理: 多页面多组件间共享信息。redux & context。Context 是 React 的一个特性,它允许你在组件树中传递数据,而无需手动传递 props。这对于在多个层级中共享数据非常有用。

image.png

(二)用react开发web应用_组件

  • 数据:通过定义state操作视图,mount时获取数据更新state,ref保存与视图无关的值,unmount前清空ref.

  • 通信:props父子通信,redux & context组件信息共享。

  • UI:数据决定视图,通过ref获取到DOM。

  • 性能:函数使用usecallback值或者计算使用useMemo组件包裹memo。也可以在在组件卸载前进行清理操作,清理掉为 window 注册的全局事件以及定时器,防止组件卸载后继续执行影响应用性能。

image.png

(三)组件_class组件

在 React 中,组件可以分为两种类型:class 组件和函数式组件。

Class 组件是通过定义一个继承自 React.Component 的类来创建的。它具有状态(state)和生命周期方法,可以使用 setState 方法来更新状态。Class 组件的 render 方法返回组件的 JSX 表示。

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

(四)组件_函数式组件

函数式组件是通过定义一个函数来创建的。它没有状态和生命周期方法,借助Hooks,只接收 props 作为参数并返回组件的 JSX 表示。函数式组件通常更简单、更容易理解和测试。

ps: Hooks 是一些可以让你在函数组件里“钩入” React state 及生命周期等特性的函数。它们的目的是让你不必写 class 就能使用这些特性。

举个例子,useState 是一个 Hook,它允许你在函数组件中添加 state。当你调用 useState 时,它返回一对值:当前 state 以及更新 state 的函数。你可以在事件处理函数中或其他一些地方调用这个函数来更新 state。

React Hooks 允许你在函数式组件中使用 state。下面是一个简单的例子:

import { useState } from 'react';

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

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

React 官方提供了一些常用的 Hooks,包括 useStateuseEffectuseContextuseReduceruseCallbackuseMemouseRefuseImperativeHandle 等。此外,还有一些第三方库提供了更多的自定义 Hooks,例如 react-use 库中封装了大量可直接使用的自定义 Hooks。

(五)函数式相比于class的优点

image.png

(六)组件和Hook的关系

image.png

(七)Hook的规则和原理

  • 只能在最顶层使用hook Hook 是 JavaScript 函数,但在使用的时候它有着它需要遵循的规则。只在最顶层使用 Hook,这能够让 React 在多次的 useState 和 useEffect 调用中保持 hook 的状态正确。

image.png

  • 只能在react函数中调用hook 只在 React 函数中调用 Hook,也就是说在 React 的函数组件中调用 Hook,还有一点是在自定义 Hook 中调用其他 Hook。自定义hook必须以use开头。

image.png 这些规则的目的是确保 Hook 在每一次渲染中都按照同样的顺序被调用,从而让 React 能够在多次的 useState 和 useEffect 调用之间保持 hook 状态的正确

(八)Hook过期闭包问题

使用 Hooks 时可能遇到的一个问题就是过时的闭包,这可能很难解决。过时的闭包是指在函数组件中,由于闭包的原因,某些变量或函数引用了旧的状态或 props,导致行为异常。

例如,在 useEffect Hook 中使用了计时器状态并在该状态发生改变时重新启动定时器的情况下,由于计时器回调函数的闭包中包含了旧的计时器状态,当新的状态被更新时,回调函数仍然使用旧状态的值。这就可能导致计时器的行为出现异常,例如重复调用回调函数或无法停止计时器等。

有一些方法可以解决这个问题,例如使用 ref 来存储最新的状态或 props,或者使用 setState 的函数式更新来避免引用旧状态。

(九)React常见api及其作用

image.png

(十)React常见hook及其作用

image.png

三、业务场景与案例

(一)父子组件通信案例

在React中,父子组件之间可以通过多种方式进行通信。父组件可以通过props向子组件传递数据,子组件可以通过回调函数或事件冒泡向父组件传递信息。

下面是一个简单的例子,展示了如何使用props和回调函数实现父子组件之间的通信:

class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { message: "" };
  }

  handleMessage = (message) => {
    this.setState({ message });
  };

  render() {
    return (
      <div>
        <Child onMessage={this.handleMessage} />
        <p>Message from child: {this.state.message}</p>
      </div>
    );
  }
}

const Child = ({ onMessage }) => {
  const handleClick = () => {
    onMessage("Hello from child!");
  };

  return <button onClick={handleClick}>Send Message</button>;
};

在这个例子中,Parent 组件通过 propsChild 组件传递了一个回调函数 handleMessage。当 Child 组件中的按钮被点击时,它会调用这个回调函数并传递一个消息给 Parent 组件。然后,Parent 组件会更新它的状态并显示来自 Child 组件的消息。

(二)组件间共享信息_Reducer & Context

在React中,可以使用Reducer和Context来拓展应用。Reducer可以整合组件的状态更新逻辑,而Context可以将信息深入传递给其他组件。你可以组合使用它们来共同管理一个复杂页面的状态。

这是一个使用Context和Reducer的例子,演示了如何在React中使用它们来共享状态。在这个例子中,我们创建了一个ProfileContext,并使用useReducer来管理状态。还定义了一个reloadProfile函数来异步获取数据,并在组件中使用useEffect来调用它。最后,将状态和dispatch函数传递给ProfileContext.Provider,以便在组件树中的其他组件中访问它们。

import React, { useCallback } from 'react'

const ProfileContext = React.createContext()

const initialState = {
  data: false
}

let reducer = (state, action) => {
  switch (action.type) {
    case 'unload':
      return initialState
    case 'profileReady':
      return { data: action.payload }
  }
}

const reloadProfile = async () => {
  try {
    let profileData = await fetch('/profile')
    profileData = await profileData.json()
    return profileData
  } catch (error) {
    console.log(error)
  }
}

function ProfileContextProvider(props) {
  let [profile, profileR] = React.useReducer(reducer, initialState)

  const customDispatch = useCallback(async (action) => {
    switch (action.type) {
      case 'reload': {
        const profileData = await reloadProfile()
        profileR({ type: 'profileReady', payload: profileData })
        break
      }
      default:
        // Not a special case
        profileR(action)
    }
  }, [])

  return (
    <ProfileContext.Provider value={{ profile, customDispatch }}>
      {props.children}
    </ProfileContext.Provider>
  )
}

export { ProfileContext, ProfileContextProvider }

image.png

(三)组件间共享信息_redux

另一种在React中实现组件之间信息共享的方法是使用Redux。Redux是一个独立的状态管理库,它可以与React一起使用。在Redux中,我们可以定义一个全局的store来存储应用程序的状态。然后,定义action来描述状态的变化,并使用reducer函数来处理这些action并更新状态。

下面是一个简单的例子,它演示了如何在React中使用Redux来共享状态:

import { createStore } from 'redux'

// 1. 定义初始化的state
const initState = {
  num: 0
}

// 2. 定义action
function add() {
  return { type: 'INCREMENT' }
}

function dec() {
  return { type: 'DECREMENT' }
}

// 3. 编写reducer函数
function reducer(state = initState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { num: state.num + 1 }
    case 'DECREMENT':
      return { num: state.num - 1 }
    default:
      return state
  }
}

// 4. 创建store
const store = createStore(reducer)

// 5. 使用dispatch触发action
store.dispatch(add())
store.dispatch(add())
store.dispatch(dec())

(四)你不知道的冒泡_Portal

在React中,我们可以使用Portal来将子节点渲染到父组件DOM层次结构之外的DOM节点。这可以通过调用ReactDOM.createPortal(child, container)来实现。其中,child是要渲染的React元素,而container是要将子节点插入到的DOM元素。

export default function App() {
  const [isShow, setIsShow] = useState(false);
  function click(e) {
    console.info("e", e);
    setIsShow(!isShow);
  }
  return (
    <div
      style={{ backgroundColor: "#a00", width: `200px`, height: `100px` }}
      onClick={click}
    >
      {isShow && (
        <Modal>
          <span>zhangsan</span>
        </Modal>
      )}
    </div>
  );
}

虽然Portal可以将子节点渲染到父组件DOM层次结构之外的DOM节点,但是它仍然遵循React的事件冒泡机制。也就是说,如果你在Portal中触发了一个事件,它会沿着React组件树向上冒泡,而不是沿着实际渲染的DOM元素结构。