React
react 与 react-dom库
react package contains only the functionality necessary to define React components. It is typically used together with a React renderer like react-dom for the web.
react-dom package serves as the entry point to the DOM and server renderers for React.
import ReactDOM from 'react-dom';
ReactDOM.render(<App />, document.getElementById('root'));
react-router 与 react-router-dom库
react-router是路由核心库。react-router-dom基于react-router,加入了一些在浏览器运行的功能,与 dom 相关。一般二者要一起使用。
React router
基于 history 来实现
- link 标签与 a 标签。 Link的跳转只触发对应路由页面更新,不会触发整个页面更新。
问题汇总
React 特点
JSX 语法
单向数据流
虚拟 DOM
组件化
jsx: js 的语法糖。
如何将jsx 转换为真实节点
React.createElement 具体做了什么?
将 传入的 element 及 key ref props children 数据组合,返回 React.element 对象(虚拟dom)。React 会在稍后的渲染过程中使用虚拟dom构建真实dom和进行更新。
虚拟 DOM
虚拟 DOM 是用来描述 DOM 结构的 js 对象,不是真实 DOM,与真实的 DOM 一一对应。使用的目的是为了更好地比较 DOM 的变化,不直接更新 DOM。
真实 DOM 更新缓慢,会直接更新 HTML,操作代价高,引起页面回流和重绘,消耗内存多。
虚拟 DOM:
- react 中数据发生改变,虚拟 DOM 重新渲染。
- 计算虚拟 DOM 与之前虚拟 DOM 的差异。
- 将 DOM 的变化用于更新真实 DOM。
为什么需要:
-
框架帮你实现一定的优化。
-
只需要专注业务代码。
-
跨平台复用。
虚拟 DOM 一定更快吗?
不是的。虚拟 DOM 会有大量的计算,不一定比直接操作 DOM 更快。虚拟 DOM 提高了代码的性能下限,优化了大量操作 DOM 的性能损耗。所以在简单场景,或初次渲染等情况 不如原生DOM。
Class Comp 与 Function Comp
-
类组件相对复杂,并且含有 this,比较难以理解。在一个生命周期函数中,可能有很多不想管的逻辑,而函数组件可以按照代码的用途拆分,做到关注点分离。
-
类组件相对较大,从长远角度难以维护,而 Hooks 组件相对较短,更容易拆分,可读性高也更方便维护。
-
函数组件复用成本低,更容易抽象出单独的组件或单独的逻辑。
-
函数组件更加灵活,更适合做到逻辑和 UI 的解耦。但类组件的逻辑也更清晰:构造函数以及生命周期的角度。
-
思考模式不同。
Class 组件的模式是先做什么再做什么,按照时间维度划分,比如 this.setState 的第二个参数,比如生命周期的使用。
函数组件的思考模式是依赖,副作用。围绕着 state,props 的变化而做什么事情。
函数式组件捕获了渲染所使用的值。hooks 追求的是渲染一致性,当执行一些方法时,读到的 state,props 是当时的状态快照,是不可变的。而类组件有 this,this.state/this.props 上的值都是变化的。---- 函数式组件与类组件有何不同? — Overreacted
使用区别
class 组件
- 需要继承。
- 有组件实例。
- 有生命周期函数。
- 有状态。
Function 组件
- 无需继承。
- 不是 new 出来的,没有实例。React 内部有表示该组件的对象。
- 没有生命周期函数。
- 以前没有状态,现在 useState 算是它的状态。
React 元素与组件
react 组件可以用 jsx 创建,(jsx 是React.createElement的简写),是 react 中的最小单位,并不是真实的 dom,实际是一个 js 对象。
组件由元素构成,元素的数据结构是对象,而组件的数据结构是类或函数。
PureComponent
自动调用 shouldComponentUpdate,比较 props 和 state,如果没有变化,不会调用 render。
使用 Object.is 进行浅比较。
生命周期

挂载阶段
- constructor 初始化 state,进行方法绑定
- static getDerivedStateFormProps 在初始挂载及后续更新都会被调用,返回一个对象来更新 state,state 的值在任何时候都取决于 props, 如果返回 null 则不更新任何内容。
- render 渲染组件
- componentDidMount 在组件第一次挂载完成(渲染到 DOM)后调用,只会被调用一次
更新阶段
-
static getDerivedStateFromProps
-
shouldComponentUpdate(nextProps,nextState) 根据返回值决定是否更新,返回 true 时才会调用 render,返回 false 时不会调用 render。默认行为是返回 true ,每次 props 或 state 发生变化都会调用。
-
render()
-
getSnapshotBeforeUpdate(prevProps, prevState) 在组件被更新之前调用,返回值会作为 componentDidUpdate 的第三个参数。
在最近一次渲染输出(提交到 DOM 节点)之前调用,这个函数运行完真实 DOM 会被渲染。它使得组件能在发生更改之前从 DOM 中捕获一些信息(例如,滚动位置)
-
componentDidUpdate(prevProps, prevState, snapshot)
卸载阶段
- componentWillUnmount() 在组件卸载及销毁之前直接调用。在此方法中执行必要的清理操作
被废弃的生命周期
componentWillMountcomponentWillReceivePropscomponentWillUpdate
原因:
react 架构或渲染机制改变带来的问题。有副作用,在 render 之前执行,不能保证副作用被 清除。
错误处理
捕获并打印在子组件的 js 错误,并渲染备用 UI
在 class 组件中定义 static getDerivedStateFromError或 ComponentDidCatch,然后直接当作常规组件使用,此组件就会形成错误墙(Error Boundary),错误组件中子组件的错误会被捕获。
类似在组件层面实现 try catch 机制,一般只需要声明一次,并在整个应用使用。
错误边界无法捕捉事件处理、异步、服务端渲染、自身错误等非子组件的错误。
事件处理器不会在渲染阶段触发,可以在事件处理内部使用 try catch 语句。
todo: 实现一个hooks 进行错误处理
setState 同步异步问题
我们一直说的同步异步并不是指setState本身, setState本身一直一个同步函数, 我们指的是调用完setState后react会同步的去执行后续的步骤还是会异步的去执行后续的步骤。
本身不是异步的,但在表现上有时同步,有时异步。因为 setState 都会触发更新,将多个状态合并更新,减少渲染次数。
React 早期(使用 Fiber 架构之前)实现更新批处理导致的,更新优先级的问题,是否处于 batchUpdate。
在 React 的事件处理函数或 React 生命周期中,state 的更新是异步的,浏览器事件处理结束后批量合并后批量更新。
在 setTimeout 或原生 dom 事件里没有处于batchUpdate中是同步更新的。
class App extends React.Component { constructor(props) { super(props); this.state = { count: 0, }; } componentDidMount() { this.setState({ count: this.state.count + 1 }); console.log('1', this.state.count); this.setState({ count: this.state.count + 1 }); console.log('2', this.state.count); setTimeout(() => { this.setState({ count: this.state.count + 1 }); console.log('3', this.state.count); }); setTimeout(() => { this.setState({ count: this.state.count + 1 }); console.log('4', this.state.count); }); } render() { return <></>; } }
React18 的 setState
React 18 要看是 React.createRoot 创建的话,无论是不是在setTimeout中都走批量处理,都表现为异步更新。否则还是之前的更新逻辑。
如果需要同步更新可以使用React.flushSync()
受控组件
受控组件:通过组件控制用户输入的值,包括 value,onChange 事件,数据由 react 组件维护。组件对子组件的变更有控制权。
非受控组件:可以设置 defaultValue,但不能控制值的变更,可以通过 ref 获取当前组件的值。
状态提升
几个组件共用状态数据,将这部分状态提升到他们最近的父组件进行管理,并通过 props 传给子组件使用。
Refs
用于父元素访问子元素的方法。也可以用于缓存一个值。
Context
跨层级组件数据传递。
通过组件树传递数据的方法,实现在组件间共享数据,可以避免一层层地手动 props 传递数据。
组件传值的方式
父传子
props
子传父
调用父组件传过来的函数
class: Refs 拿到子组件的实例,调用其方法
hooks:forwardRef+useImperativeHandle
兄弟组件
状态提升到公共祖先,借公共组件传递
其他
context
redux
自定义事件
Portals 传送门
将子节点渲染到存在于父组件以外的 DOM 节点,常用于对话框、悬浮卡以及提示框。
ReactDOM.createPortal(child, container) child 是 React 子元素,container 是一个 DOM 元素,child 会渲染到容器里,而不是父类;使用在组件的 render 函数里。
除了在 DOM 树中的位置,其他行为和普通的 react 子节点行为一致。 react 内的事件处理逻辑等还是在原来的 react 组件中(比如冒泡)
为什么必须在组件的顶层使用 hook
只能在顶层,且不能在循环/条件语句中使用。
React 内部使用链表,保存每个 hook 调用的顺序,完全依赖 hook 的调用顺序。比如使用多个 useState,需要把每个 state 和对应的 setState 关联。