React 原理&底层逻辑&源码探析(2)

380 阅读15分钟

React 原理&底层逻辑&源码探析

数据是如何在 React 组件之间流动的

🍐 “组件间通信“的背后是一套环环相扣的 React 数据流解决方案

基于 props 的单向数据流

组件,从概念上类似于 JavaScript 函数。它接受任意的入参(即props”)并返回用于描述页面展示内容的 React 元素。

单向数据流:

当前组件的state以props的形式流动时,只能流向组件树中比自己层级更低的组件

通过 props 的数据传递方式,可以很轻松的实现父子组件,子父组件,兄弟组件之间的通信

  • 父 → 子

    父组件直接通过 props 将数据传递给子组件即可

  • 子 → 父

    父组件通过 props 传递给子组件带有自身上下文的函数,子组件将数据作为函数参数,从而实现子组件与父组件通信

  • 兄弟组件

    父组件可以将数据的更新函数传递给 A 组件,数据传递给 B 组件,A 组件可以通过更新函数实现与 B 组件交流

弊端:

当组件间层次加深时,两个组件间的通信会使两组件层级之间的组件接受无用的数据。就像课堂上传纸条那样,两个同学想要交流,中间的同学就要帮忙传,对中间的同学而言这毫无意义。

利用”发布-订阅“模式驱动数据流

发布-订阅模式就能实现跨层次交流,只需一端订阅,一端发布即可

API 设计思路:

那么这个 API 必然想要一个监听器,订阅者可以通过监听器说明自己想要订阅什么事件,当事件发布时,又想要触发什么函数。即注册监听函数。

然后是发布器,发布者可以通过发布器说明需要发布什么类型的事件,以及携带的数据

API 将发布者想要发布的数据交给订阅者的触发函数,从而实现两端的交流

那么这个 API 就需要知道某个类型的事件对应有哪些触发函数,即存在一个映射关系

示例:

// 定义发布者类
class EventListener {
    // 存储所有订阅者的回调函数
    eventMap = {}

    // 添加订阅者
    subscribe(type, callback) {
        if (!callback instanceof Function) {
            throw new TypeError("用于监听的回调函数应是 Function 类型");
        }

        // 如果不存在该类型事件的回调存储数组,则新建数组
        if (!this.eventMap[type]) {
            this.eventMap[type] = [];
        }

        this.eventMap[type].push(callback);
    }

    // 发布事件
    publish(type, ...params) {
        // 当该类型事件存在订阅时,传入参数即可
        if (this.eventMap[type]) {
            this.eventMap[type].forEach(func => func(...params));
        }
    }

    // 移除订阅者
    unsubscribe(type, callback) {
        if (this.eventMap[type]) {
            this.eventMap[type] = this.eventMap[type].filter(func => func != callback);
        }
    }
};

使用 Context API 维护全局状态

Context API 是 React 官方提供的一种组件树全局通信的方式

🍐 在React16.3之前,Context APl由于存在种种局限性 并不被React官方提倡使用 开发者更多的是把它作为一个概念来探讨 而从v16.3.0开始,React对Context AP/进行了改进 新的Context APl具备更强的可用性

图解工作流:

Untitled 10.png

  • Context API 的一些核心概念:

    1. Context 对象:Context API 中的核心概念,它是一个 JavaScript 对象,用于在组件之间传递数据。Context 对象需要在父组件中创建,并且可以包含任何类型的数据。
    2. Provider 组件:用于在父组件中将 context 对象的值传递给子组件。Provider 组件需要包裹在组件树中的某个位置,它可以接收一个 value 属性,用于传递 context 对象的值。
    3. Consumer 组件:用于在子组件中访问 context 对象的值。Consumer 组件需要包裹在需要访问 context 对象的子组件中,并且可以接收一个函数作为子组件,这个函数的参数就是 context 对象的值。
    4. useContext hook:用于在函数式组件中访问 context 对象的值。通过 useContext hook,你可以在函数式组件中直接访问 context 对象的值,避免了创建额外的组件来传递 context 对象的问题。
  • 过时的 Context API 存在以下几个问题:

    1. 性能问题:在过时的 Context API 中,当一个 context 值改变时,所有依赖于该 context 值的组件都会重新渲染,即使它们并不真正需要使用这个新的 context 值。这会导致不必要的性能开销,降低应用程序的性能。
    2. 复杂性问题:在过时的 Context API 中,需要手动编写大量的代码来创建和管理 context,并且需要深入了解 React 的内部实现细节。这增加了代码的复杂性,使得开发过程更加困难和容易出错。
    3. 不稳定性问题:由于过时的 Context API 是 React 16.3 版本之前的 API,因此它已经被标记为过时的 API,并且不再受到官方支持。这意味着在将来的版本中,可能会删除这个API,导致应用程序出现不兼容的问题。

旧 Context API 的工作流图解:

Untitled 11.png

🍊 如果组件提供的一个Context发生了变化,而中间父组件的shouldComponentUpdate返回false,那么使用到该值的后代组件不会进行更新。使用了Context的组件则完全失控,所以基本上没有办法能够可靠的更新Context。 —— React官方

第三方数据流框架的”课代表“:初探 Redux

🍐 Redux 是 JavaScript 的状态容器,它提供可预测的状态管理

数据在组件与 Redux 之间的关系表现,使 Redux 成为像仓库一般的存在

Untitled 12.png

  • Redux 是如何帮助 React 管理数据的
    • store 是一个单一的数据源,而且是只读的

    • action 是对变化的描述

      // 一个 action 对象大致长这样
      const action = {
      	type = "ADD_ITEM",
      	payload = '<li>text</li>'
      }
      
    • reducer 负责对变化进行分发和处理

✨ **在 Redux 的整个工作过程中,数据流都是严格单向的**

(当面试过程中被提问到 Redux 相关问题时,首先抛出这句)

Redux 的单一数据流:

  • actions 会在用户交互如点击时被 dispatch
  • store 通过执行 reducer 方法计算出一个新的 state
  • UI 读取最新的 state 来展示最新的值

https://cn.redux.js.org/assets/images/ReduxDataFlowDiagram-49fa8c3968371d9ef6f2a1486bd40a26.gif

这里是我关于 Redux 的笔记:

跟我一起学习 Redux | 阅读文档后理解 (1) - 掘金

跟我一起学习 Redux | 阅读文档后理解 (2) - 掘金

从编码角度理解 Redux 工作流

import {createStore} from 'redux'

// 创建 store
const store = createStore(
	reducer,
	initial_state,
	applyMiddleware(middleware1, middleware2, ...)
);

reducer 的作用是将新的 state 返回给 store

const reducer = (state, action) => {
	// 此处是各种 state 处理逻辑
	return new_state;
}

const store = createStore(reducer);

想要让 state 更新就必须使用正确 action 来驱动 reducer

const action = {
	type = "ADD_ITEM",
	payload = '<li>text</li>'
}

store.dispatch(action);

React-Hooks 设计动机与工作模式

React-Hooks 设计动机初探

🍊 它是React团队在真刀真枪的React组件开发实践中逐渐认知到的一个改进点,背后涉及**对类组件和函数组件两种组件形式的思考和侧重**

那么首先需要知道何为类组件,何为函数组件

  • 在 React 中,类组件是通过继承 React.Component 类来创建的组件,它是 React 早期版本中创建组件的主要方式。类组件提供了一种声明式的方式来描述 UI,它将组件的状态(state)和属性(props)封装在一个类中,并且提供了一些生命周期方法(lifecycle methods)来控制组件的渲染过程。

    以下是类组件的一些特点:

    1. 继承 React.Component 类:类组件通过继承 React.Component 类来创建,它需要实现 render() 方法来返回要渲染的 JSX 元素。
    2. 带有状态的组件:类组件可以通过 state 属性来存储组件的状态,当状态发生变化时,React 会自动重新渲染组件。
    3. 支持组件属性:类组件可以通过 props 属性来接收父组件传递的数据,这使得组件之间可以方便地进行数据传递和交互。
    4. 生命周期方法:类组件提供了一些生命周期方法,例如 componentDidMountcomponentDidUpdatecomponentWillUnmount 等,用于控制组件的渲染过程和处理组件相关的操作。
  • 代码示例

    下面是一个简单的类组件的代码例子,它接收一个名字和年龄属性,并在页面上显示一个 "Hello, [name]! You are [age] years old." 的消息。

    import React from 'react';
    
    class Greeting extends React.Component {
      constructor(props) {
        super(props);
        this.state = {
          name: props.name,
          age: props.age
        };
      }
    
      render() {
        return (
          <div>
            <p>Hello, {this.state.name}! You are {this.state.age} years old.</p>
          </div>
        );
      }
    }
    
    export default Greeting;
    
    

    在这个例子中,Greeting 组件继承了 React.Component 类,并且在构造函数中初始化了组件的状态(state)。在 render() 方法中,组件会根据状态(state)中的 name 和 age 值来渲染一个包含消息的 <p> 元素。这个组件可以通过以下代码在另一个组件中使用:

    import React from 'react';
    import Greeting from './Greeting';
    
    function App() {
      return (
        <div>
          <Greeting name="Alice" age={25} />
          <Greeting name="Bob" age={30} />
        </div>
      );
    }
    
    export default App;
    
    

    在这个例子中,Greeting 组件被包裹在另一个组件 App 中,并且传递了不同的 name 和 age 属性值。当 App 组件渲染时,它会在页面上显示两个不同的 "Hello" 消息,分别针对 "Alice" 和 "Bob" 两个人。

  • 在 React 中,函数组件是一种使用函数定义的组件,它是在 React 16.8 版本中引入的新特性。函数组件是一种更加简洁、易于理解和管理的组件定义方式,它不需要继承 React.Component 类或使用类的构造函数,而是直接返回一个用 JSX 编写的元素。

    以下是函数组件的一些特点:

    1. 使用函数定义组件:函数组件是通过定义一个普通的 JavaScript 函数来创建的,它接收一个 props 对象作为参数,并返回一个用 JSX 编写的元素。
    2. 无状态组件:函数组件不需要维护自己的状态,因此它是一种无状态(stateless)组件。
    3. 纯函数组件:函数组件应该是纯函数(pure function),即对于相同的输入,始终返回相同的输出。这使得函数组件更加易于测试和理解。
    4. 结构简洁:函数组件不需要继承 React.Component 类,因此它的代码结构通常更加简洁、易于理解和管理,同时也提高了组件的性能。
  • 代码示例

    下面是一个简单的函数组件的代码例子:

    import React from 'react';
    
    function Greeting(props) {
      return (
        <div>
          <p>Hello, {props.name}!</p>
        </div>
      );
    }
    
    export default Greeting;
    
    

    在这个例子中,Greeting 组件是一个函数组件,它接收一个 props 对象作为参数,并使用其中的 name 属性来显示一个 "Hello" 消息。这个组件可以通过以下代码在另一个组件中使用:

    import React from 'react';
    import Greeting from './Greeting';
    
    function App() {
      return (
        <div>
          <Greeting name="Alice" />
          <Greeting name="Bob" />
        </div>
      );
    }
    
    export default App;
    
    

    在这个例子中,Greeting 组件被包裹在另一个组件 App 中,并且传递了不同的 name 属性值。当 App 组件渲染时,它会在页面上显示两个不同的 "Hello" 消息,分别针对 "Alice" 和 "Bob" 两个人。

函数组件与类组件的对比:无关”优劣“,只谈”不同“

  • 类组件需要继承class,函数组件不需要
  • 类组件可以访问生命周期方法,函数组件不能
  • 类组件中可以获取到实例化后的ths,并基于这个this做各种各样的事情,而函数组件不可以
  • 类组件中可以定义并维护state(状态),而函数组件不可以

那么可以得出类组件比函数组件好的结论吗?

答案是不可以。应该这么说:在React-Hooks出现之前的世界里,类组件的能力边界明显强于函数组件

重新理解类组件:包裹在面向对象思想下的“重装战舰”

类组件就是面向对象编程的一种表征

当我们在编写类时总是会有意无意地做这些事情:

封装:将一类属性和方法,“聚拢”到一个 Class 中去

继承:新的 Class 可以通过继承现有的 Class 实现对某一类属性和方法的复用

所以类组件在继承 React.component 后其内部的功能是特别多的,就好似一艘重装战舰

那么对于众多问题来说,使用类组件来解决小问题,无疑是大炮轰蚊子,可以但没有必要,这不仅会带来高昂的理解成本还有更多的心智负担

深入理解函数组件:呼应 React 设计思想的”轻巧快艇“

首先提出这句话:

函数组件会捕获 render 内部的状态,这是两类组件最大的不同

函数组件更契合 React 的设计理念

Untitled 13.png

作为开发者,我们编写的就是声明式的代码。而 React 的工作就是及时的把声明式的代码转换为命令式的 DOM 操作。把数据层面的描述映射到用户可见的 UI 变化中去。

这就意味着从原则上来讲,React 的数据应该总是紧紧地跟渲染绑定在一起地。而类组件做不到这点

下面来看举例:

class ProfilePage extends React.component {
	showMessage() {
		alert('Follow' + this.props.user);
	}

	handleClick() {
		setTimeout(this.showMessage, 3000);
	}

	render() {
		return <button onClick={this.handleClick}>Follow</button>;
	}
}

这个组件存在的问题就是,在定时器内回调还未执行前如果父组件传入了新的 props,那么定时器内回调执行时 this.props 就是新的 props 而非之前的 props

由此可见数据并没有跟组件绑定在一起

如果同样的情况换做函数组件,因为函数闭包的特性,即使父组件传入新的 props,也不过是再次调用了函数组件,定时器中的 showMessage 依然访问的是之前的作用域中的 props

function ProfilePage({props}) {
	showMessage() {
		alert('Follow' + props.user);
	}

	handleClick() {
		setTimeout(showMessage, 3000);
	}

	return (<button onClick={handleClick}>Follow</button>)
} 

到这里你应该就可以理解为什么说”函数组件可以捕获 render 内部的状态“了

函数组件是一个更加匹配其设计理念、也更有利于逻辑拆分与重用的组件表达形式

Hooks 的本质:一套能够使函数组件更强大、更灵活的”钩子“

通过上面的学习,可以看出函数组件比起类组件“少”了很多东西给函数组件的使用带来了非常多的局限性

如果说函数组件是一台轻巧的快艇,那么 React-Hooks 就是一个内容丰富的零部件箱,允许你自由地选择和使用你需要的那些能力

useState(): 为函数组件引入状态

早期的函数组件相比于类组件,其一大劣势是缺乏定义和维护 state 的能力,useState 正是这样一个能够为函数组件引入状态的API

// 使用函数组件和 useState 钩子函数
function Counter() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={handleClick}>Increment</button>
    </div>
  );
}

// 使用类组件和 this.state
class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }
  render() {
    return (
      <div>
        <p>Count: {this.state.count}</p>
        <button onClick={this.handleClick}>Increment</button>
      </div>
    );
  }
}

从上面的例子可以看出,使用 useState钩子函数的函数组件相较于使用类组件的复杂度更低,因为它们不需要编写构造函数和手动维护 this.state,而且处理事件的代码也更为简洁。这使得函数组件在某些情况下更易于编写和维护,尤其是对于一些简单的组件和小型应用程序来说。

调用 React.useState 的时候实际上是给这个组件关联了一个状态

这是相比较类组件而言的·,因为类组件是将所有需要设置为状态的数据作为 state 对象的属性的

useEffect(): 允许函数组件执行副作用操作

useEffect 则在一定程度上弥补了生命周期的缺席

🍐 useEffect能够为函数组件引入副作用 过去我门习惯放在componentDidMount、componentDidUpdate 和componentWillUnmount.三个生命周期里来做的事 现在可以放在useEffect里来做

Why React-Hooks: Hooks 是如何帮助我们升级工作模式的

这也是在思考 ”为什么需要 React-Hooks“这道面试题

  1. 告别难以理解的 Class

    对于下面的类组件而言,事件回调被调用时,因为是严格模式下,所以 this 为 undefined 所以会报错(onClick 只是接受了一个没有指定 this 函数定义)

    class Counter extends React.Component {
      constructor(props) {
        super(props);
        this.state = { count: 0 };
        //this.handleClick = this.handleClick.bind(this);
      }
      handleClick() {
    		// 报错
        this.setState({ count: this.state.count + 1 });
      }
      render() {
        return (
          <div>
            <p>Count: {this.state.count}</p>
            <button onClick={this.handleClick}>Increment</button>
          </div>
        );
      }
    }
    

    而函数组件无需考虑 this

  2. 解决业务逻辑难以拆分的问题

  3. 使状态逻辑复用变得简单可行

  4. 函数组件从设计思想上来看更加契合 React 的设计理念

React-Hooks 依然存在不足

尽管 React Hooks 已经成为 React 中非常受欢迎的特性,但它们仍然存在一些不足。下面是一些 React Hooks 的不足之处:

  1. 学习曲线:React Hooks 的概念和用法与类组件有很大的不同,因此对于一些开发者来说,学习曲线可能会比较陡峭。
  2. 限制性:尽管 React Hooks 提供了很多钩子函数(如 useState()useEffect()useContext() 等),但它们仍然不能完全替代类组件的所有功能。例如,它们并不能完全替代类组件的生命周期方法,也不能继承或扩展其他组件。
  3. 实现细节:React Hooks 在底层实现上使用了一些 JavaScript 的特性(如闭包和数组解构),这可能会导致一些性能问题和内存泄漏问题。
  4. 命名规范:React Hooks 的命名规范比较严格,必须以 use 开头,并且只能在函数组件或其他钩子函数中使用。这可能会增加代码的复杂度和阅读难度。
  5. 开发工具支持:一些开发工具可能还没有完全支持 React Hooks,例如某些编辑器、调试工具和代码检查工具可能需要升级或配置才能支持 React Hooks。

总之,尽管 React Hooks 提供了很多方便的特性,但它们仍然存在一些不足之处。为了更好地利用 React Hooks,开发者需要深入了解它们的实现原理和使用方法,并且根据实际情况进行选择和折衷。