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具备更强的可用性图解工作流:
-
Context API 的一些核心概念:
- Context 对象:Context API 中的核心概念,它是一个 JavaScript 对象,用于在组件之间传递数据。Context 对象需要在父组件中创建,并且可以包含任何类型的数据。
- Provider 组件:用于在父组件中将 context 对象的值传递给子组件。Provider 组件需要包裹在组件树中的某个位置,它可以接收一个
value属性,用于传递 context 对象的值。 - Consumer 组件:用于在子组件中访问 context 对象的值。Consumer 组件需要包裹在需要访问 context 对象的子组件中,并且可以接收一个函数作为子组件,这个函数的参数就是 context 对象的值。
- useContext hook:用于在函数式组件中访问 context 对象的值。通过
useContexthook,你可以在函数式组件中直接访问 context 对象的值,避免了创建额外的组件来传递 context 对象的问题。
-
过时的 Context API 存在以下几个问题:
- 性能问题:在过时的 Context API 中,当一个 context 值改变时,所有依赖于该 context 值的组件都会重新渲染,即使它们并不真正需要使用这个新的 context 值。这会导致不必要的性能开销,降低应用程序的性能。
- 复杂性问题:在过时的 Context API 中,需要手动编写大量的代码来创建和管理 context,并且需要深入了解 React 的内部实现细节。这增加了代码的复杂性,使得开发过程更加困难和容易出错。
- 不稳定性问题:由于过时的 Context API 是 React 16.3 版本之前的 API,因此它已经被标记为过时的 API,并且不再受到官方支持。这意味着在将来的版本中,可能会删除这个API,导致应用程序出现不兼容的问题。
旧 Context API 的工作流图解:
第三方数据流框架的”课代表“:初探 Redux
🍐 Redux 是 JavaScript 的状态容器,它提供可预测的状态管理数据在组件与 Redux 之间的关系表现,使 Redux 成为像仓库一般的存在
- Redux 是如何帮助 React 管理数据的
-
store 是一个单一的数据源,而且是只读的
-
action 是对变化的描述
// 一个 action 对象大致长这样 const action = { type = "ADD_ITEM", payload = '<li>text</li>' } -
reducer 负责对变化进行分发和处理
-
(当面试过程中被提问到 Redux 相关问题时,首先抛出这句)
Redux 的单一数据流:
- actions 会在用户交互如点击时被 dispatch
- store 通过执行 reducer 方法计算出一个新的 state
- UI 读取最新的 state 来展示最新的值

这里是我关于 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)来控制组件的渲染过程。
以下是类组件的一些特点:
- 继承 React.Component 类:类组件通过继承 React.Component 类来创建,它需要实现
render()方法来返回要渲染的 JSX 元素。 - 带有状态的组件:类组件可以通过
state属性来存储组件的状态,当状态发生变化时,React 会自动重新渲染组件。 - 支持组件属性:类组件可以通过
props属性来接收父组件传递的数据,这使得组件之间可以方便地进行数据传递和交互。 - 生命周期方法:类组件提供了一些生命周期方法,例如
componentDidMount、componentDidUpdate、componentWillUnmount等,用于控制组件的渲染过程和处理组件相关的操作。
- 继承 React.Component 类:类组件通过继承 React.Component 类来创建,它需要实现
-
代码示例
下面是一个简单的类组件的代码例子,它接收一个名字和年龄属性,并在页面上显示一个 "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 编写的元素。
以下是函数组件的一些特点:
- 使用函数定义组件:函数组件是通过定义一个普通的 JavaScript 函数来创建的,它接收一个 props 对象作为参数,并返回一个用 JSX 编写的元素。
- 无状态组件:函数组件不需要维护自己的状态,因此它是一种无状态(stateless)组件。
- 纯函数组件:函数组件应该是纯函数(pure function),即对于相同的输入,始终返回相同的输出。这使得函数组件更加易于测试和理解。
- 结构简洁:函数组件不需要继承 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 的设计理念
作为开发者,我们编写的就是声明式的代码。而 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“这道面试题
-
告别难以理解的 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
-
解决业务逻辑难以拆分的问题
-
使状态逻辑复用变得简单可行
-
函数组件从设计思想上来看更加契合 React 的设计理念
React-Hooks 依然存在不足
尽管 React Hooks 已经成为 React 中非常受欢迎的特性,但它们仍然存在一些不足。下面是一些 React Hooks 的不足之处:
- 学习曲线:React Hooks 的概念和用法与类组件有很大的不同,因此对于一些开发者来说,学习曲线可能会比较陡峭。
- 限制性:尽管 React Hooks 提供了很多钩子函数(如
useState()、useEffect()、useContext()等),但它们仍然不能完全替代类组件的所有功能。例如,它们并不能完全替代类组件的生命周期方法,也不能继承或扩展其他组件。 - 实现细节:React Hooks 在底层实现上使用了一些 JavaScript 的特性(如闭包和数组解构),这可能会导致一些性能问题和内存泄漏问题。
- 命名规范:React Hooks 的命名规范比较严格,必须以
use开头,并且只能在函数组件或其他钩子函数中使用。这可能会增加代码的复杂度和阅读难度。 - 开发工具支持:一些开发工具可能还没有完全支持 React Hooks,例如某些编辑器、调试工具和代码检查工具可能需要升级或配置才能支持 React Hooks。
总之,尽管 React Hooks 提供了很多方便的特性,但它们仍然存在一些不足之处。为了更好地利用 React Hooks,开发者需要深入了解它们的实现原理和使用方法,并且根据实际情况进行选择和折衷。