响应式系统与React | 青训营

56 阅读25分钟

一、React的历史与应用

1.React应用场景

  • 前端应用开发,如 Facebook、Instagram、Netfix网页版。
  • 移动原生应用开发,如 Instagram、Discord、Oculus。
  • 结合 Electron,进行桌面应用开发。

2.React发展历史

  • 2011年:React的前身是Facebook的XHP框架,用于提升前端代码的可维护性和性能。
  • 2013年:Facebook开始尝试使用React来解决复杂的用户界面问题,特别是在大规模数据变化时的性能问题。
  • 2013年5月:React首次公开亮相,演讲中介绍了React的核心原则,包括虚拟DOM和单向数据流。
  • 2013年8月:React正式开源,发布了第一个版本。它成为一个视图库,帮助提升了性能和开发效率。
  • 2015年3月:发布了React Native,扩展了React到移动应用开发,允许构建原生iOS和Android应用。
  • 2016年10月:引入Fiber架构的概念,旨在提升渲染性能和并发模式的支持。
  • 2018年5月:发布了React 16.3,改进了状态共享和生命周期方法。
  • 2020年2月:发布了React 16.13,继续改进性能、稳定性和开发体验。
  • 2020年10月:发布了React 17,着重于稳定性、兼容性和升级易用性。

二、React的设计思路

1.UI编程痛点

  • 状态更新,UI 不会自动更新,需要手动地调用 DOM 进行更新。
  • 欠缺基本的代码层面的封装和隔离,代码层面没有组件化。
  • UI之间的数据依赖关系,需要手动维护,如果依赖链路长,则会遇到“Callback Hell”

2.响应式与转换式

  • 响应式系统的基本工作流程:事件发生 → 执行既定的回调函数 → 该回调函数导致系统的状态发生变更
  • 前端UI的基本工作流程:事件发生 → 执行既定的回调函数 → 该回调函数导致系统的状态发生变更 → UI 更新

3.响应式编程特点

  • 状态更新,UI 自动更新: 前端开发中,常用"响应式"的方式来处理应用程序的状态和用户界面之间的关系。当状态发生变化时,前端框架会自动监测这些变化,并根据变化自动更新用户界面,确保界面反映最新的状态。
  • 前端代码组件化,可复用,可封装: 前端开发中的组件化是指将UI元素划分为独立的、可复用的部件(组件)。每个组件都有自己的逻辑、样式和外观,可以在应用中多次使用。这种模块化的方法使得代码更易于维护、测试和重用。
  • 状态之间的互相依赖关系,只需声明即可: 前端框架允许开发者通过声明的方式来管理和定义状态,同时处理状态之间的依赖关系。通过在应用中声明状态之间的关系,框架可以自动处理状态的更新和同步。

4.组件化

组件化是一种前端开发方法,通过将页面上的不同部分划分为独立、可复用的组件来构建用户界面。每个组件具有自己的逻辑、样式和外观,可以在应用中多次使用。

  • 组件化的步骤:

    • 组件划分:首先,将页面拆分为不同的功能块或模块,每个模块可以成为一个组件
    • 组件设计:对于每个组件,设计其内部的逻辑、样式和外观。组件的逻辑可能包括处理用户交互、数据获取、状态管理等。样式定义组件的外观和布局。
    • 组件封装:将组件的逻辑、样式和外观封装在一起,创建一个独立的、可复用的实体。这可以是一个包含HTML、CSS和JavaScript的文件,或者是一个独立的JavaScript类。
    • 组件复用:使用创建的组件在不同的页面或应用中多次复用。通过将相似的功能封装在一个组件中,减少了重复编写相同代码的工作量。
  • 组件化开发中的重要概念

    • 组件是组件的组合/原子组件: 是指组件可以由其他更小的组件(称为原子组件)组合而成。这种层次化的结构允许构建复杂的用户界面,通过将各种小块的功能组合起来形成更大的功能。
    • 组件内拥有状态,外部不可见: 在前端框架中,组件通常拥有自己的状态,这是指组件内部的数据。这些数据可以用于控制组件的行为、展示和交互。组件的状态通常是组件内部的,外部的其他组件不能直接访问或修改这些状态。这种封装性有助于隔离组件,使其更可维护和可复用。
    • 父组件可将状态传入组件内部: 父组件可以通过属性(props)将数据传递给子组件。这使得父组件可以将自己的状态传递给子组件,以便子组件可以使用这些数据来渲染内容、处理逻辑等。这种父子组件之间的数据传递方式有助于在不同层次的组件之间建立通信。
  • 组件设计

    • 组件声明了状态和 UI 的映射: 在前端框架中,组件通常会将状态(数据)与用户界面(UI)进行映射。即组件的状态决定了界面的呈现。当状态发生变化时,界面会自动更新以反映新的状态。
    • 组件有 Props / State 两种状态: 组件可以有两种类型的状态:Props(属性)和State(状态)。Props是由父组件传递给子组件的数据,而State是组件内部管理的状态。Props通常用于传递静态数据,而State用于管理可能会变化的数据。
    • “组件”可由其他组件拼装而成: 组件的复用性在于它们可以被其他组件组合起来形成更复杂的结构。这种嵌套和组合的方式允许构建复杂的用户界面,同时保持每个组件的独立性和可维护性。
  • 组件代码

    • 组件内部拥有私有状态 State: 每个组件通常会维护自己的私有状态,也就是称为State。这个状态只在组件内部可见和管理。通过使用State,组件可以存储和追踪内部的数据,而不会被其他组件直接访问或修改。
    • 组件接受外部的 Props 状态提供复用性: Props是一种从父组件向子组件传递数据的机制。通过将数据以Props的形式传递给组件,可以实现组件之间的数据传递和共享。这使得组件在不同上下文中都可以被复用,因为父组件可以为子组件提供不同的Props数据,从而影响其行为和外观。
    • 根据当前的 State / Props,返回一个 UI: 组件的主要目标是根据当前的状态(State)和外部传递的数据(Props),生成相应的用户界面(UI)。即组件的外观和行为取决于其状态和Props。当这些状态或Props发生变化时,组件会自动重新渲染,以确保UI与数据保持同步。

5.状态归属问题

  • 状态归属

    状态归属问题指的是在React组件中,合理地确定状态应该属于哪个组件。

    当一个状态需要在两个组件之间共享时,可以考虑将这个状态上移到它们的最近共同祖先组件中。

    实现步骤:

    1. 找到共同祖先节点: 首先,确定需要共享状态的两个组件。然后,向上逐级寻找这两个组件的共同祖先节点,即这两个组件的父组件、祖父组件等等。
    2. 将状态放置在共同祖先节点中: 找到共同祖先节点后,将需要共享的状态放置在这个共同祖先节点的状态中。这可以通过将状态作为这个祖先组件的 state 或者使用状态管理工具来实现。
    3. 通过 Props 传递给子组件: 将状态从共同祖先组件传递给需要使用它的子组件,通过 Props 将状态传递下去。这两个子组件都可以访问和使用这个状态,而且不需要进行状态上升。
  • 状态上升

    状态上升(State Lifting)是指将状态从一个组件提升到其父组件或更高层次的组件,以便多个子组件可以共享和访问该状态。这种做法可以解决多个组件之间需要共享数据的情况,但也需要谨慎使用,以避免造成组件层次复杂性的增加。

  • 思考:

    • React是单项数据流,还是双向数据流?

      React 是单向数据流(One-Way Data Flow)的框架。即数据在 React 应用中的流动是单向的,从父组件传递给子组件,并且不允许子组件直接修改父组件的数据。

      在 React 中,数据流动的方式:

      1. 父组件传递数据给子组件:父组件可以通过 Props(属性)的方式将数据传递给子组件。子组件通过访问 Props 来获取数据。
      2. 子组件不直接修改 Props 数据:子组件不能直接修改从父组件传递的 Props 数据。Props 被视为只读的,只能在父组件内部修改。
      3. 子组件可以使用状态(State) :每个组件可以拥有自己的状态(State),状态是组件内部的数据。子组件可以根据其状态来管理和呈现内容。
      4. 数据的变化通过 Props 或状态进行传递:当父组件的数据发生变化时,它会重新渲染并将新的数据通过 Props 传递给子组件。同时,子组件可以通过更新自己的状态来响应用户交互或其他事件。
    • 如何解决状态不合理上升的问题?

      在 React 中,状态不合理上升(State Lifting)是指将某个状态从组件下移到更高层的父组件,导致父组件变得臃肿、复杂,而子组件可能变得过于受控、缺乏自主性。

      如何解决状态不合理上升步骤:

      1. 确定真正需要共享的状态: 首先,仔细考虑哪些状态确实需要在父组件和子组件之间共享。不是所有状态都需要上升到父组件。
      2. 将共享状态提升到共同的祖先组件: 找到共享状态的共同祖先组件,将状态从需要共享的子组件中提升到这个共同祖先组件。这样,父组件和子组件都可以通过 Props 接收到这个状态。
      3. 通过 Props 传递状态给子组件: 在将状态上升到共同祖先组件后,通过 Props 将这个状态传递给需要使用它的子组件。这样子组件就可以直接从 Props 获取状态,而不必再去操作上层状态。
      4. 让子组件保留自己的私有状态: 子组件仍然可以拥有自己的私有状态,用于管理与其自身相关的信息。这有助于保持子组件的独立性和可维护性。
      5. 考虑使用状态管理工具: 对于更复杂的状态管理,可以考虑使用专门的状态管理工具,如 Redux 或 Mobx。这些工具可以帮助更好地管理全局状态,减少状态上升问题。
      6. 使用回调函数进行数据交流: 如果子组件需要在状态发生变化时通知父组件,可以通过传递回调函数作为 Props 给子组件,让子组件在适当的时机调用这些回调函数。
    • 组件的状态改变后,如何更新 DOM?

      当组件的状态发生变化后,React 会自动根据新的状态值来更新 DOM,以反映最新的界面状态。

      实现过程:

      1. 状态变化触发重新渲染: 当组件的状态发生变化时,React会触发一个重新渲染过程。这意味着组件将被重新执行,生成新的虚拟DOM树(Virtual DOM Tree)。
      2. 生成新的虚拟DOM: 在重新渲染过程中,React会生成一个新的虚拟DOM树,该树与之前的虚拟DOM树进行比较,找出哪些部分需要更新。
      3. 进行虚拟DOM的差异比较: React会使用之前的虚拟DOM树和新的虚拟DOM树进行比较,找出两者之间的差异。这个过程被称为"虚拟DOM的协调"。
      4. 生成更新操作: 根据差异比较的结果,React会生成一系列更新操作,这些操作描述了需要添加、更新或删除的DOM元素以使其与新的虚拟DOM树保持一致。
      5. 执行DOM更新: 最后,React会将生成的更新操作应用于实际的DOM树,从而实现DOM的更新。React会尽量最小化DOM的操作,只更新需要变化的部分,以提高性能和效率。

6.React组件的生命周期

React 组件的生命周期描述了组件在不同阶段的创建、更新和销毁过程。

React 组件的生命周期过程:

  1. Mounting(挂载阶段)

    • constructor(props):构造函数,用于初始化组件的状态和绑定方法。
    • static getDerivedStateFromProps(props, state):在组件实例化和接收新的 Props 时调用,返回一个新的状态对象。
    • render():渲染方法,返回 JSX 表示的组件界面。
    • componentDidMount():组件挂载到 DOM 后调用,通常用于进行网络请求、添加事件监听等操作。
  2. Updating(更新阶段)

    • static getDerivedStateFromProps(props, state):在挂载后和接收新 Props 时调用,返回一个新的状态对象。
    • shouldComponentUpdate(nextProps, nextState):决定组件是否需要重新渲染,默认返回 true。可以通过优化避免不必要的渲染。
    • render():重新渲染组件。
    • getSnapshotBeforeUpdate(prevProps, prevState):在更新 DOM 之前获取 DOM 的快照,返回值将传递给 componentDidUpdate 方法。
    • componentDidUpdate(prevProps, prevState, snapshot):组件更新完成后调用,通常用于处理更新后的副作用操作。
  3. Unmounting(卸载阶段)

    • componentWillUnmount():组件即将被卸载时调用,用于清理资源、取消订阅等操作。

三、React(hooks)的写法

useState函数

useState 是 React 中的一个 Hook,用于在函数组件中添加和管理状态(state)。可以在不使用类组件的情况下,在函数组件中使用状态来追踪和更新数据。

  1. 使用方式useState 是一个由 React 提供的函数。可以通过在函数组件中导入它,并在组件内调用它来创建状态。
  2. 参数useState 函数接受一个初始状态值作为参数,该值指定了状态的初始值。函数的返回值是一个数组,数组的第一个元素是当前的状态值,第二个元素是一个更新状态值的函数。
  3. 创建状态: 通过调用 useState 并传递初始状态值,可以在函数组件中创建一个状态。例如:
import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  // ...
}
  1. 状态值和更新函数useState 返回的数组包含了当前的状态值和一个用于更新状态值的函数。在上面的例子中,count 是当前状态值,setCount 是更新状态的函数。
  2. 状态的更新: 调用状态的更新函数(例如,setCount)会触发组件的重新渲染,并且新的状态值会被应用。可以通过更新状态值来驱动组件的变化和重新渲染。
  3. 函数式更新useState 的更新函数也可以接受一个回调函数,这个回调函数会在更新状态之前被调用,可以根据之前的状态进行计算。可以避免因为异步操作导致的状态更新问题。

useEffect函数

useEffect 是 React 中的一个 Hook,用于处理组件中的副作用操作,比如数据获取、DOM 操作、订阅等。

  1. 使用方式useEffect 是一个由 React 提供的函数。可以通过在函数组件中导入它,并在组件内部调用它来处理副作用。
  2. 参数useEffect 接受两个参数:一个副作用函数和一个依赖数组。副作用函数用于执行副作用操作,依赖数组用于指定在什么情况下触发副作用函数的执行。
  3. 执行时机useEffect 的副作用函数会在每次组件渲染后执行。默认情况下,它在每次渲染时都会执行一次。
  4. 控制副作用的执行时机: 通过传递依赖数组,可以控制副作用函数的执行时机。如果依赖数组为空,副作用函数只会在组件挂载和卸载时执行一次。如果依赖数组包含某些值,副作用函数会在这些值发生变化时执行。
  5. 清理副作用: 在副作用函数中,可以返回一个清理函数,用于清除副作用操作,比如取消订阅、清理定时器等。这个清理函数会在下一次副作用执行之前执行。
  6. 处理异步操作useEffect 内部可以处理异步操作,例如数据获取。通常,在副作用函数中使用 async/awaitPromise 来执行异步操作。
  7. 副作用的执行顺序: 如果一个组件中使用了多个 useEffect,它们的执行顺序与它们在组件内部的顺序一致。

Hooks

React 中的 Hooks 是一系列函数,可以在函数式组件中添加和管理状态、副作用等功能,而不必使用类组件。

  1. useStateuseState 是最基本的 Hook,用于在函数组件中添加状态。它返回一个状态值和一个更新状态的函数,使得组件能够追踪和修改状态。
  2. useEffectuseEffect 是用于处理副作用操作的 Hook。通过它可以在组件渲染后执行异步操作、订阅、DOM 操作等,而不必担心繁琐的生命周期管理。
  3. useContextuseContext 允许在组件中访问 React 的上下文,使得跨层级的组件之间可以共享数据。
  4. useReduceruseReducer 是一个状态管理的替代方案,适用于处理复杂的状态逻辑。它类似于 Redux 中的 reducer。
  5. useCallback 和 useMemouseCallbackuseMemo 用于性能优化。前者用于缓存回调函数,后者用于缓存计算结果,以避免不必要的重复计算。
  6. 自定义 Hooks: 可以编写自定义的 Hooks,将组件逻辑封装成可复用的函数。可以提高代码的可维护性和重用性。
  7. 规则和限制: Hooks 有一些使用规则和限制,例如,只能在函数式组件中使用,不能在普通 JavaScript 函数中使用。
  8. 清晰的组件逻辑: Hooks 的引入使得组件逻辑更加集中和清晰,不再需要将相关的逻辑分散在多个生命周期方法中。

四、React的实现

React实现的问题

1. React实现的问题

  • 问题1:JSX 不符合 JS 标准语法

    JSX(JavaScript XML)是一种类似XML的语法扩展,它允许在JavaScript代码中编写类似HTML的结构。

    JSX与JavaScript标准语法不符的主要问题:

    • 标签和元素的写法: 在JSX中,可以像编写HTML一样创建组件元素,例如<Component />。然而,在JavaScript标准中,尖括号被用于比较运算符、位运算符等,因此这种语法在JavaScript中是非法的。
    • 属性名和属性值: 在JSX中,可以为组件传递属性,例如<Component prop="value" />。然而,在JavaScript标准中,属性名和属性值不能直接放在尖括号内,因为这不符合JavaScript的对象字面量语法。
    • className替代class: 在JSX中,为了避免与JavaScript的关键字冲突,需要使用className属性来代替HTML中的class属性。是因为class是JavaScript的保留关键字。
    • 自闭合标签: 在HTML中,可以使用自闭合标签,例如<img src="image.jpg" />。但在JavaScript标准中,自闭合标签是无效的,因为JavaScript不支持这种写法。
    • 多元素的问题: 在JSX中,只能返回一个根元素。如果需要返回多个相邻的元素,就必须将它们包装在一个父元素内。而在JavaScript标准中,相邻的元素是允许的。

    JSX是React的一种语法扩展,虽然它不符合JavaScript标准语法,但通过使用Babel等工具,可以在React项目中使用JSX,并在构建过程中将其转换为合法的JavaScript代码。

  • 问题2:返回的 JSX 发生改变时,如何更新 DOM

    当使用React构建应用程序时,它采用了一种称为"虚拟DOM(Virtual DOM)"的机制,以最小化DOM操作的开销并提高性能。

    当状态发生变化导致JSX发生改变时,React会执行以下过程来更新DOM:

    • 状态改变触发渲染: 当应用程序的状态(state)发生改变,或者父组件传递给子组件的props发生变化时,React会触发组件的重新渲染。
    • 生成虚拟DOM: 在重新渲染之前,React会生成一个新的虚拟DOM树,这个树结构和实际的DOM结构相似,但它仅仅是内存中的一个表示。
    • 对比虚拟DOM: React会将新生成的虚拟DOM树与之前的虚拟DOM树进行对比,找出两者之间的差异。这个过程称为"协调(Reconciliation)"。
    • 计算差异(Diffing): React使用一种高效的算法来计算两个虚拟DOM树之间的差异,找出需要进行更新的部分。
    • 生成变更集(Patch): 一旦计算出差异,React会生成一个称为"变更集(Patch)"的对象,其中记录了需要添加、删除或更新的DOM操作。
    • 应用变更集: 最后,React将变更集应用到实际的DOM上。这些DOM操作会被批量处理,减少了实际的DOM访问次数,提高了性能。

    React的更新过程并不是直接操作实际的DOM,而是通过虚拟DOM的比较和批量更新来实现。

  • 问题3:State / Props 更新时,要重新触发 render 函数

    当使用React构建应用程序时,组件的render函数是用于生成组件的虚拟DOM。当组件的state(状态)或者从父组件传递来的props(属性)发生变化时,React会重新触发组件的render函数,从而生成新的虚拟DOM树。

2. 如何对比虚拟DOM

虚拟DOM是指使用JavaScript对象来表示DOM结构的一种技术,它可以在内存中进行操作和比较,而不需要直接与浏览器的真实DOM进行交互。而真实DOM则是浏览器中实际渲染和展示的DOM结构。

对比虚拟DOM的一些常用方法和技巧:

  • 渲染差异:虚拟DOM通过比较前后两个状态下的虚拟DOM树的差异,找出需要更新的部分,并生成最小的DOM操作。这样可以减少对真实DOM的操作次数,提高应用的性能。
  • Diff算法:虚拟DOM的对比过程中使用了一些算法来优化性能,最常见的是Diff算法。Diff算法的时间复杂度是以O(n)的方式增长,其中n代表虚拟DOM树的节点数。它会对比两棵树中对应位置的节点,判断它们是否相同。如果节点类型不同,那么这两个节点及其子树将被认为是完全不同的结构;如果节点类型相同,那么会进一步比较节点的属性和子节点。这个过程会在整个虚拟DOM树中逐级递归进行下去。
  • 批处理:为了减少对真实DOM的操作次数,虚拟DOM通常会将多个DOM操作合并成一次批处理。这样可以减少浏览器重绘和回流的次数,提高性能。
  • 键(Key)的使用:为了更精确地确定需要更新的部分,虚拟DOM通常会使用键(Key)来标识每个节点。通过给节点添加唯一的键,可以在对比时更准确地识别新增、删除或移动的节点。
  • 异步更新:为了进一步提高应用性能,虚拟DOM还支持异步更新。通过将虚拟DOM的比较和渲染过程放入事件循环的任务队列中,可以避免阻塞主线程,提高应用的响应性。

五、React状态管理库

核心思想

Redux 的核心思想是将应用的状态(state)集中存储在一个单一的状态树中,而不是分散在各个组件中。

推荐

  • Redux: Redux 是一个用于 JavaScript 应用状态管理的库,特别适用于大型、复杂的应用。Redux 帮助开发者管理应用的全局状态,并提供了一种可预测性的方式来处理状态变化。Redux 的优势在于它提供了一个清晰的状态管理模式,适用于需要跨多个组件共享状态、管理复杂状态逻辑、实现时间旅行调试等场景。
  • xstate: xstate 是一个用于管理有限状态机(Finite State Machines)的 JavaScript 库。有限状态机是一种数学模型,用于描述对象在不同状态之间的转换及其相应的行为。xstate 提供了一个强大且可扩展的方式来管理应用的状态,特别适用于处理复杂的状态逻辑。xstate 提供了一种清晰、可视化的方式来管理状态和状态转换,适用于需要处理复杂状态逻辑的应用。它尤其适用于构建有限状态机和状态图相关的系统,如用户界面、流程控制等。
  • MobX: MobX 是一个用于状态管理的 JavaScript 库,它可以帮助开发者管理应用中的状态,并使状态变化的过程更加简洁和自然。与 Redux 或 xstate 不同,MobX 更加注重在状态变化的同时保持代码的简洁性。MobX 的主要优势在于它可以让状态管理变得非常简洁和自然。相比于 Redux,MobX 不需要编写大量的模板代码,而且状态变化的过程更加直观。它适用于需要快速开发、轻量级状态管理的应用,尤其是中小型应用。
  • Recoil: Recoil 是一个由 Facebook 开发的状态管理库,专门用于管理 React 应用中的状态。与其他状态管理库类似,Recoil 旨在帮助开发者更好地管理应用的状态,并提供了一些独特的特性。Recoil 的主要优势在于它的简单性和灵活性。与 Redux 相比,Recoil 采用了更简洁的 API 和更自然的开发方式。它适用于需要管理中小规模状态的应用,特别是对于 React 开发者来说,上手较为轻松。

状态机

状态机是一种模型,用于描述在给定状态下,应用如何通过事件触发从一个状态转换到另一个状态。在 React 状态管理库中,状态机的基本思想是将应用的状态抽象成不同的状态(states),然后通过事件(events)触发状态之间的转换。

Modern.js

Modern.js 是字节跳动 Web 工程体系的开源版本,它提供多个解决方案,来帮助开发者解决不同研发场景下的问题。Modern.js 支持 React 应用所需要的所有配置和工具,并内置额外的功能和优化。开发者可以使用 React 构建应用的 UI,然后逐步采用 Modern.js 的功能来解决常见的应用需求,如路由、数据获取、状态管理等。

它主要包含以下特性:

  • Rust 构建:提供双构建工具支持,轻松切换到 Rspack 构建工具,编译飞快。
  • 渐进式:使用最精简的模板创建项目,通过生成器逐步开启插件功能,定制解决方案。
  • 一体化:开发与生产环境 Web Server 唯一,CSR 和 SSR 同构开发,函数即接口的 API 服务调用。
  • 开箱即用:默认 TS 支持,内置构建、ESLint、调试工具,全功能可测试。
  • 周边生态:自研状态管理、微前端、模块打包、Monorepo 方案等周边需求。
  • 多种路由模式:包含自控路由、基于文件约定的路由(嵌套路由)等。

本篇笔记为个人总结,学习交流使用,欢迎各位大佬评价指正,非常感谢!