React Virtual DOM 原理
Virtual DOM 是什么
- 是一种编程理念;
- 将UI节点抽象成JS对象
- 因为提供了对HTML DOM的抽象,所以在web开发中,通常不需要去调用DOM API。也是因为抽象,所以React也可以开发Native(React Native)。
- Virtual DOM 构建UI
- 构建UI:React是通过render方法渲染Virtual DOM(这里不考虑优化),从而绘制出真实DOM,意味着,每次修改了state的值就会执行render方法。
- 和原生DOM更新差异
- 原生DOM更新
- DOM API调用更新UI
- Virtual DOM更新
- 每次render都会产生一份新的'react dom'
- Virtual DOM要对新旧'react dom'进行比较,从而确定在旧'dom'的基础上进行多少变更
- 确定最优的变更策略之后调用DOM API更新UI
- Virtual DOM如何提高性能
- 我们将render产生的virtual DOM 简称'Vdom'
- 通常调用setState方法触发Vdom更新,setState大多数时候是异步的
- 通过对比新旧'Vdom',确定最优实现新'Vdom'所需的操作
- 原生DOM更新
Virtual DOM Diff
- 层次
- 组件级别比较
- 元素级别比较
React Reconciler
- React协调的过程
- 利用Virtual DOM将内存中抽象的UI转换为真实UI节点的过程
- 协调方式
- Stack Reconciler:栈协调
- 弊端:
- 事务性的,不可阻断,响应时间长,大部分时间处理渲染和更新;
- 没有对任务的优先级进行区分
- 弊端:
- Fiber Reconciler
- 需要解决的问题
- 可阻断的渲染过程
- 适时重启渲染
- 父子附件中来回切换布局更新
- 更清晰的错误处理
- React Fiber的数据结构
- React Fiber将之前的DOM节点树用链表的结构来描述,与React 15.x之前的版本不一样
- React 15.x中描述DOM节点树的VDom是一个对象(嵌套)。而在React 16.x中是用Fiber节点来描述的。Fiber节点的数据结构就是一个链表
- 阶段
- render阶段
- 执行组件的render方法(函数组件对应return),确定哪些需要更新。此过程是可以被打断的。
- commit阶段
- 更新阶段,在确定更新内容后,提交更新并调用对应渲染模块(react-dom)进行渲染。为了防止页面抖动,此过程是同步且不能被打断。
- render阶段
- 时间分片
- React在挂载或者更新过程中会做很多事情,比如调用组件的渲染函数、对比前后树差异,而且commit阶段是同步的,所以在Stack Reconciler中会导致卡顿等问题。
- 双缓冲
- Fiber Reconciler过程中,内存中保持着两棵树:current Tree & WorkInProcess Tree。当workInprocess Tree中执行完毕就转为current tree。如果执行过程中被打断或者响应更高优先级的任务,也能在workInprocess tree中继续开始。
- 需要解决的问题
- Stack Reconciler:栈协调
React New Component Lifecycle
-
React组件新生命周期详解
- 挂载阶段的函数
- constructor 构造函数,初始化state,以及为事件处理函数绑定实例。
- getDerivedStateFromProps 新增的静态方法,返回一个新的state,或者是null(后续不更新)
- render 渲染函数
- componentDidMount 挂载成功后立即调用的函数。
- 更新阶段的函数
- getDerivedStateFromProps props变化或者state方法触发
- shouldComponentUpdate 判断是否进行更新
- render 渲染函数
- getSnapshotBeforeUpdate render方法之后调用,返回一个dom更改之前的快照,将配合后续的componentDidUpdate方法是用
- componentDidUpdate 更新后会被立即调用
- 卸载阶段
- componentWillUnmount 卸载函数,组件卸载及销毁之前直接调用。主要用于清除一些在组件生命周期订阅,真实DOM事件以及setTimeout/setInterval的返回值。
- 异常捕获的函数
- componentDidCatch 生命周期方法在后代组件抛出错误后被调用。方法接收两个参数(error,info),分别是错误信息和错误组件的栈信息。
- getDerivedStateFromError 在后代组件抛出错误后调用,接收一个参数(error)表示具体的错误信息。
- 挂载阶段的函数
-
新版组件升级
- componentWillMount
- render方法之前调用,在此调用setState并不会触发再次渲染
- 通常会在这个方法中进行页面标题的一些修改以及其他与再次render不相关的操作
- UNSAFE_componentWillMount
- 与state相关的操作挪到constructor方法中执行
- 异步操作挪到componentDidMount中执行
- componentWillUpdate
- 在组件收到新的props或state时,会在渲染之前调用
- 方法内不能调用setState,触发循环,内存泄漏
- UNSAFE_componentWillUpdate
- 应该在shouldComponentUpdate中判断是否更新
- componentWillReceiveProps
- 接收父级组件传递过来最新的props,转化为组件内的state
- 判断是否进行更新或者执行异步请求数据
- UNSAFE_componentWillReceiveProps
- 与渲染相关的props直接渲染,不需要处理为组件内state
- 异步数据请求在componentDidUpdate中处理
- getDerivedStateFromProps方法替换,需要考虑生命周期的执行顺序
- componentWillMount
-
升级组件版本问题
- 老工程代码量很多,改动特别麻烦。需要回归的点非常多!怎么解决?
- npx react-codemod rename-unsafe-lifecycles
- 老工程代码量很多,改动特别麻烦。需要回归的点非常多!怎么解决?
React Hooks
Hooks使命
- 逻辑组件复用
- 逻辑与UI分离 React官方推荐在开发中将逻辑部分与视图部分解耦,便于定位问题和职责清晰。
- 函数组件拥有state 在函数组件中如果要是实现类似拥有state的状态,必须要将组件转成class组件
- 逻辑组件复用 社区一直致力逻辑层面的复用,像render props/HOC ,不过他们都有对应的问题。Hooks是目前为止相对完美的解决方案。
- Hooks解决什么问题
- render props通过嵌套组件实现,在真实的业务中,会出现嵌套多层,以及梳理props不清晰的问题
- HOC 通过对现有组件进行扩展、增强的方法来实现复用,通常采用包裹方法来实现。高阶组件的实现会额外地增加元素层级,使得页面元素的数量更加臃肿。
- React 16.8 引入的Hooks,使得实现相同功能而代码量更少成为现实。
- 通过使用Hooks,不仅在硬编码层面减少代码的数量,同样在编译之后的代码也会更少。
Hooks原理
- No magic,just arrays
- setState是如何是如何实现的,useState源码解析
- mountState源码解析
- mountState方法内:
-
- 先返回当前执行中的Hook对象,Hook 对象内包含memoizedState/baseState/queue等属性;
-
- 接着会将下次要渲染的state值和修改state方法返回;
-
- 最后将这两个值以数组的形式返回;
-
- mountState方法内:
- 原理图
React New Feature
- React Fragments/Portals/Strict Mode
- 新版React提供了Fragments组件,并不会生成多余元素
- Portals
- React提供了一个能让改变挂载节点的API。通常的开发中,组件会挂载其最近的父节点上。在某些特定的需求上,需要挂载在特定的节点上。
-
- 之前的做法是封装成方法,而不是在render中引入
-
- 既想在render中引入,又能自定义挂载节点就需要Portals API
- Strict Mode
- Strict Mode提供一个可以显示潜在问题的组件
-
- 检测是否使用string ref和findDOMNode以及老版的context api
-
- 检测是否多次调用不可预测的副作用
-
- 检测是否使用即将废弃的生命周期函数
- React Concurrent Mode
- 一个还在试验阶段的特性
- 目的:让react应用能够更好的响应交互并且还能根据用户设备的硬件性能和网络条件进行对应的调节
- 如果说Fiber是让应用更好的更新,那concurrent就是让应用在体验上再上一个台阶
- blocking rendering: 当前react在更新时,包括创建一个新的DOM节点或者是现有DOM节点的移动,这些过程是不能被阻断的,一旦开始执行,就是执行到完成为止
- interruptible rendering:在concurrent模式下,渲染更新时可以被中断的,特别是当渲染的过程特别耗时的时候,中断渲染来响应用户的行为,会让整个应用的体验得到提升
- 开启concurrent模式
- createRoot
- createBlockingRoot
Redux
- Redux基础
- Redux 动机
- 为什么需要Redux?
- Redux 适合于大型复杂的单页面应用
- 为什么需要Redux?
- Redux 核心概念
- state 应用全局数据的来源,数据驱动视图的核心
- action 数据发生改变动作的描述
- reducer 结合state和action,并返回一个新的state
- Redux 的三个原则
- 单一数据源 整个应用的state被存储在一棵object tree中,并且整个object tree只存在于唯一一个store中
- state是只读的 唯一改变state的方法就是触发action,action是一个用于描述已发生事件的普通对象
- 使用纯函数来执行修改state 可存函数意味着同样的输入会有同样的输出
- Redux 动机
- 优化使用Redux,让应用更高效
-
- Redux 异步
- Redux 的插件机制,使得Redux默认的同步Action扩展支持异步Action
- applyMiddleware接收一系列插件。每个插件(middleware)都会以dispatch和getstate作为参数,并返回一个函数。改函数会被传入下一个插件中,直到调用结束。
-
- Reselect & Immutable Data
- Reselect
- 针对mapStateToProps中state在同一数据源中需要筛选的场景
- mapStateToProps中的state如果带有筛选函数,会导致每次都返回新对象
- Immutable Data
- 避免副作用
- 状态可追溯
- React中比较是shallowCompare
- Immutable方案
- immutable
- 提供完整的api,与普通的JS对象不同,两者不能直接使用
- 对redux的应用程序来说,整个state tree应该是immutable.js对象,根本不需要使用普通Javascript对象
- immer
- js原生数据结构实现的immutable也提供了一套对应的api,相比immutable更推荐使用
- immutable
-
- redux-actions & @rematch/core & dva
- Redux范式繁琐
- 完成一次页面渲染,需要在Action层分别定义type,Action方法,Reducer中响应Action方法。完成一次流程需要在多个文件夹中来回切换
- 基础功能匮乏
- 默认只有同步方法,异步数据请求需要安装插件redux-thunk,复杂功能交由第三方插件完成,有一定的接入成本。
-
Mobox
响应式状态管理工具
Context API 前世今生
Context: React的上下文,贯穿了整个React,不需要层层传递
前世
- Context VS Props
- Context
- 父级(根节点)与应用节点都需要强制类型声明,关键字不一样
- 全局上下文,贯穿了整个应用
- Props
- 应用节点需要类型声明,非强制
- 只能进行逐级传递,一旦中间断掉,就会传递失败
- Context
- Context缺陷
- 父级组件的shouldComponentUpdate返回false,就会引起更新失败。导致子组件接收到的context还是老的,破坏了传递流程
- PureComponent或者自定义的优化可能收不到Context的更新