响应式系统与React | 青训营

什么是React

React是用于构建用户界面的 JavaScript 库

  • React 采用声明式编程使代码更容易理解和调试。
  • React 使用组件化思想,将复杂系统拆解为多个模块,提高复用性。
  • React 可以在多个平台上使用,包括 Web,移动端和桌面应用程序,

lazy & suspense

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

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

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

javascript 复制代码
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 内容,提示用户正在加载。

ErrorBoundary

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

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

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

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

javascript 复制代码
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;
  }
}

更新流程

React 的更新流程包括

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

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

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

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

优缺点

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。这对于在多个层级中共享数据非常有用。

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

  • 数据:通过定义state操作视图,mount时获取数据更新state,ref保存与视图无关的值,unmount前清空ref.
  • 通信:props父子通信,redux & context组件信息共享。
  • UI:数据决定视图,通过ref获取到DOM。
  • 性能:函数使用usecallback值或者计算使用useMemo组件包裹memo。也可以在在组件卸载前进行清理操作,清理掉为 window 注册的全局事件以及定时器,防止组件卸载后继续执行影响应用性能。

类组件

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

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

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

函数式组件

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

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

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

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

javascript 复制代码
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,包括

  • useState
  • useEffect
  • useContext
  • useReducer
  • useCallback
  • useMemouseRef
  • useImperativeHandle
  • ...

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

函数式相比于class的优点

组件和Hook的关系

Hook的规则和原理

  • 只能在最顶层使用Hook,这能够让 React 在多次的 useState 和 useEffect 调用中保持 hook 的状态正确。
  • 只能在react函数中调用hook 。还有一点是在自定义 Hook 中调用其他 Hook。自定义hook必须以use开头。

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

Hook过期闭包问题

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

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

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

React常见api及其作用

React常见hook及其作用

业务场景与案例

父子组件通信案例

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

javascript 复制代码
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

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

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

javascript 复制代码
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 }

redux

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

csharp 复制代码
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元素。

javascript 复制代码
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元素结构。


React状态管理

状态管理本身,解决的就是这种"嵌套地狱"的问题,解决的是跨层级组件之间的数据通信和状态共享。

状态管理工具的本质:管理共享内存中的状态

单页应用的各个组件本身是共享内存的,如果将状态保存在内存中 就可以读写统一内存中的变量,从而达到状态共享的目的。

为什么React有这么多状态管理工具

  • Vue: Vuex(Pinia)
  • Angular: Service和和Rxis
  • React: Flux、Redux、Mobx、Rxis、 Recoil、 Jotai、 Zustand

跟不同前端框架的定义有关,Vue和Angular双向数据绑定,计算属性等,数据是响应式的控制视图刷新,拥有计算属性等,这些使得Vue和Angular需要状态管理的场景减少,此外其本身就包含了完整的状态管理工具,比如Vue的Vuex和Pinia,Angular的Service(RXis)等,从官方定调而React不一样,React是一个纯UI层的前端框架,UI = fnstate),React将状态的变动完全交给开发者。

状态管理 管理工具

React状态管理工具可以分为以下几类

  • React自带: Local State(props) 和Context
  • 单白数据流: Flux、Redux(Redux-toolkit)
  • 双向数据绑定:Mobx
  • 原子型状态管理:Recoil、Jotai
  • 异步操作密集型: Rxis

每一种状态管理工具都有其不同的适用性,不同场景下需要合理的选择状态管理工具

Local State(props): local State顾名思义,就是组件级别的局部状态,当组件创建时初始化和生效,组件销毁时生效。

Context: React中的Context解决了react中,props或者state进行多级数据传递,则数据需要自顶下流经过每一级组件,无法跨级的问题。但是Context在页面间共享数据的时候同样有很多问题。

  1. Context相当于全局变量, 难以追溯数据的变更情况
  2. 使用Context的组件内部耦合度太高,不利于组件的复用和单元测试
  3. 会产生不必要的更新(比如会穿透memo和dependicies等
  4. Context 只能存储单一值,无法存储多个各自拥有消费者的值的集合
  5. 粒度也不太好控制,不能细粒度的区分组件依赖了哪一个
  6. Context多个Context会存在层层嵌套的问题

END

React 是一款非常强大的框架,学好 React 对未来开发至关重要

相关推荐
千慌百风定乾坤20 小时前
Go 语言入门指南:基础语法和常用特性解析(下) | 豆包MarsCode AI刷题
青训营笔记
FOFO20 小时前
青训营笔记 | HTML语义化的案例分析: 粗略地手绘分析juejin.cn首页 | 豆包MarsCode AI 刷题
青训营笔记
滑滑滑2 天前
后端实践-优化一个已有的 Go 程序提高其性能 | 豆包MarsCode AI刷题
青训营笔记
柠檬柠檬3 天前
Go 语言入门指南:基础语法和常用特性解析 | 豆包MarsCode AI刷题
青训营笔记
用户967136399653 天前
计算最小步长丨豆包MarsCodeAI刷题
青训营笔记
用户52975799354724 天前
字节跳动青训营刷题笔记2| 豆包MarsCode AI刷题
青训营笔记
clearcold4 天前
浅谈对LangChain中Model I/O的见解 | 豆包MarsCode AI刷题
青训营笔记
夭要7夜宵4 天前
【字节青训营】 Go 进阶语言:并发概述、Goroutine、Channel、协程池 | 豆包MarsCode AI刷题
青训营笔记
用户336901104444 天前
数字分组求和题解 | 豆包MarsCode AI刷题
青训营笔记
dnxb1234 天前
GO语言工程实践课后作业:实现思路、代码以及路径记录 | 豆包MarsCode AI刷题
青训营笔记