C React
设计理念
- 构建 快速响应 的大型 Web 应用程,解决CPU、IO的瓶颈
- CPU瓶颈,在浏览器每一帧的时间中,预留5ms(实际> 5ms && < 6ms)给JS线程, React 利用这部分时间更新组件,当预留的时间不够用时, React 将线程控制权交还给浏览器使其有时间渲染UI, React 则等待下一帧时间到来继续被中断的工作,称为 时间切片 (time slice)。而 时间切片 的关键是:将 同步的更新 变为 可中断的异步更新 。
- IO瓶颈, 网络延迟 是前端开发者无法解决的,点击切换页面后,先在当前页面停留了一小段时间,这一小段时间被用来请求数据,当“这一小段时间”足够短时,用户是无感知的。如果请求时间超过一个范围,再显示 loading 的效果。为此, React 实现了 Suspense (opens new window) 功能及配套的 hook —— useDeferredValue (opens new window) 。(需要可中断的异步更新)
架构(Scheduler、Reconciler、Renderer) #技术分享
- Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入 Reconciler
- 当浏览器有剩余时间时通知我们,触发回调
- 提供了多种调度优先级供任务设置
- requestIdleCallback 浏览器兼容性,触发频率不稳定,受许多因素影响。列如当我们的浏览器切换tab后,之前tab注册的 requestIdleCallback 触发的频率会变得很低。使用postMessage宏任务实现
- Reconciler(协调器)—— 负责找出变化的组件
- Fiber架构(链表)把更新工作从递归 同步更新 变成了 异步可中断更新 (浏览器时间分片用尽或有更高优任务插队)。每次循环都会调用 shouldYield 判断当前是否有剩余时间,当可以继续执行时恢复之前执行的中间状态。
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
- Reconciler 与 Renderer 不再是交替工作;整个 Scheduler 与 Reconciler 的工作都在内存中进行。当 Scheduler 将任务交给 Reconciler 后, Reconciler 会为变化的虚拟DOM打上代表增/删/更新的标记,只有当所有组件都完成 Reconciler 的工作,才会统一交给 Renderer 。
- Renderer(渲染器)—— 负责将变化的组件渲染到页面上
- Renderer 根据 Reconciler 为虚拟DOM打的标记,同步执行对应的DOM操作。
- Scheduler 和 Reconciler 的工作都在内存中进行,即使反复中断,用户也不会看见更新不完全的DOM,中断缘由( 有其他更高优任务需要先更新,当前帧没有剩余时间 )
Reconciler与Renderer
- Reconciler
- render阶段 开始于 performSyncWorkOnRoot 或 performConcurrentWorkOnRoot 方法的调用。
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
function workLoopConcurrent() { while (workInProgress !== null && !shouldYield()) { performUnitOfWork(workInProgress); } }
- performUnitOfWork 的工作可以分为两部分:“递”和“归”,通过 current === null ? 来区分组件是处于 mount 还是 update 。
- “递”阶段,第一从 rootFiber 开始向下深度优先遍历。为遍历到的每个 Fiber节点 调用 beginWork方法 ,该方法会根据传入的 Fiber节点 创建 子Fiber节点 ,并将这两个 Fiber节点 连接起来,当遍历到叶子节点(即没有子组件的组件)时就会进入“归”阶段。
- mount时,会根据 fiber.tag 不同,创建不同类型的 子Fiber节点 。如( FunctionComponent / ClassComponent / HostComponent ),最终会进入reconcileChildren方法创建新的 子Fiber节点 。
- update 时:如果 current 存在, (oldProps === newProps && workInProgress.type === current.type,或者 !includesSomeLane(renderLanes, updateLanes) ,即当前 Fiber节点 优先级不够) ,复用 current 节点,进入reconcileChildren比较 Diff 算法,将比较 workInProgress.child 和 current.child 的结果生成新 Fiber节点 。
- 在reconcileChildren, mountChildFibers (mount)与 reconcileChildFibers (update)的逻辑基本一致,会生成新的子 Fiber节点 并赋值给 workInProgress.child ,作为本次 beginWork 返回值 (opens new window) ,并作为下次 performUnitOfWork 执行时 workInProgress
- reconcileChildFibers(update)会为生成的 Fiber节点 带上 effectTag 属性,要执行 DOM 操作的具体类型就保存在 fiber.effectTag 中。 当一个 FunctionComponent 含有 useEffect 或 useLayoutEffect ,他对应的 Fiber节点 也会被赋值 effectTag 。对于 HostComponent 、 ClassComponent 如果包含 ref 操作,那么也会赋值相应的 effectTag 。 (在 mount 时只有 rootFiber 会赋值 Placement effectTag ,防止整棵 Fiber树 都会执行一次插入操作)
- “归”阶段,会调用 completeWork (opens new window) 处理 Fiber节点 ;当某个 Fiber节点 执行完 completeWork ,如果其存在 兄弟Fiber节点 (即 fiber.sibling !== null ),会进入其 兄弟Fiber 的“递”阶段。如果不存在 兄弟Fiber ,会进入 父级Fiber 的“归”阶段。
- completeWork 也是针对不同 fiber.tag 调用不同的处理逻辑,如下处理HostComponent
- mount 时,为 Fiber节点 生成对应的 DOM节点 ,由于 completeWork 属于“归”阶段调用的函数,每次调用 appendAllChildren 时都会将已生成的子孙 DOM节点 插入当前生成的 DOM节点 下。那么当“归”到 rootFiber 时,我们已经有一个构建好的离屏 DOM树 ;与 update 逻辑中的 updateHostComponent 类似的处理 props 的过程
- update时, onClick 、 onChange 等回调函数的注册;主要是处理 props ,被处理完的( 变化了的 ) props 会以数组形式[key, value]被赋值给 workInProgress.updateQueue ,并最终会在 commit阶段 被渲染在页面上
- 在“归”阶段,所有有 effectTag 的 Fiber节点 都会被追加在 effectList 中,最终形成一条以 rootFiber.firstEffect 为起点的单向链表,在 commit阶段 只需要遍历 effectList 就能执行所有 effect 了。
- render阶段 全部工作完成。在 performSyncWorkOnRoot 函数中 fiberRootNode 被传递给 commitRoot 方法,开启 commit阶段 工作流程。
- rootFiber beginWork
- App Fiber beginWork
- div Fiber beginWork
- “i am” Fiber beginWork
- “i am” Fiber completeWork
- span Fiber beginWork
- span Fiber completeWork
- div Fiber completeWork
- App Fiber completeWork
- rootFiber completeWork
- Renderer
- 在 rootFiber.firstEffect 上保存了一条需要执行 副作用 的 Fiber节点 的单向链表 effectList ,这些 Fiber节点 的 updateQueue 中保存了变化的 props 。
- before mutation阶段(执行 DOM 操作前)
- 遍历 effectList 处理 DOM节点 渲染/删除后的 autoFocus 、 blur 逻辑。
- 调用 getSnapshotBeforeUpdate 生命周期钩子,在commit阶段同步执行一次,取代componentWillXXX在render阶段任务可能中断/重新开始多次执行。
- before mutation阶段 在 scheduleCallback 中以某个优先级异步调度 flushPassiveEffects (防止同步执行时阻塞浏览器渲染)
- layout阶段 之后将 effectList 赋值全局变量给 rootWithPendingPassiveEffects
- scheduleCallback 触发 flushPassiveEffects , flushPassiveEffects 内部遍历 rootWithPendingPassiveEffects (即 effectList )执行 useEffect 回调函数。
- mutation阶段(执行 DOM 操作)
- 遍历 effectList 根据 effectTag 分别处理,其中 effectTag 包括( Placement | Update | Deletion | Hydrating )
- Placement effect,获取父级 DOM节点 根据 DOM 兄弟节点是否存在决定调用 parentNode.insertBefore 或 parentNode.appendChild 执行 DOM 插入stateNode
- Update effect, Fiber节点 需要更新,会遍历 effectList ,执行所有 useLayoutEffect hook 的销毁函数。
- Deletion effect,解绑 ref ,调度 useEffect 的销毁函数;1. 递归调用 Fiber节点 及其子孙 Fiber节点 中 fiber.tag 为 ClassComponent 的 componentWillUnmount (opens new window) 生命周期钩子,从页面移除 Fiber节点 对应 DOM节点
- layout阶段(执行 DOM 操作渲染完成后)
- commitLayoutEffectOnFiber(调用 生命周期钩子 和 hook 相关操作)
- 对于 ClassComponent ,会通过 current === null? 区分是 mount 还是 update ,调用componentDidMount或componentDidUpdate;触发 状态更新 的 this.setState 如果赋值了第二个参数 回调函数 ,也会在此时调用。
- 对于 FunctionComponent 及相关类型,他会调用 useLayoutEffect hook 的 回调函数 (销毁到回调同步执行),调度 useEffect 所有的 销毁 与 回调 函数
- useLayoutEffect hook 从上一次更新的 销毁函数 调用到本次更新的 回调函数 调用是同步执行的。(layout渲染绘制屏幕之前触发,不会闪烁);而 useEffect 则需要先调度,在( Layout阶段 完成后)再异步执行。
- useLayoutEffect 内部的代码和所有计划的状态更新阻塞了浏览器重新绘制屏幕。如果过度使用,这会使你的应用程序变慢。如果可能的话,尽量选择 useEffect
- commitAttachRef(赋值 ref)获取 DOM 实例stateNode,更新 ref 的current。
- 在 mutation阶段 结束后, layout阶段 开始前,current指向新的workInProgress Fiber树
Fiber架构
Fiber 是 React 内部实现的一套状态更新机制。支持任务不同 优先级 ,可中断与恢复,并且恢复后可以复用之前的 中间状态 。
每个 Fiber 节点 对应一个 React element ,保存了该组件的类型(函数组件/类组件/原生组件…)、对应的 DOM 节点等信息,本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新…)
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// 作为静态数据结构的属性
// Fiber对应组件的类型 Function/Class/Host...
this.tag = tag
this.key = key
this.elementType = null
// 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
this.type = null
// Fiber对应的真实DOM节点
this.stateNode = null
// 用于连接其他 Fiber 节点形成 Fiber 树 this.return = null this.child = null this.sibling = null this.index = 0
this.ref = null
// 作为动态的工作单元的属性,保存本次更新造成的状态改变相关信息 this.pendingProps = pendingProps this.memoizedProps = null this.updateQueue = null this.memoizedState = null this.dependencies = null
this.mode = mode // 保存本次更新会造成的 DOM 操作 this.effectTag = NoEffect this.nextEffect = null
this.firstEffect = null this.lastEffect = null
// 调度优先级相关 this.lanes = NoLanes this.childLanes = NoLanes
// 指向该 fiber 在另一次更新时对应的 fiber this.alternate = null }
- React 使用“双缓存”( 在内存中构建并直接替 )来完成 Fiber树 的构建与替换——对应着 DOM树 的创建与更新。
- 在 React 中最多会同时存在两棵 Fiber树 。当前屏幕上显示内容对应的 Fiber树 称为 current Fiber树 ,正在内存中构建的 Fiber树 称为 workInProgress Fiber树 。current fiber节点与 workInProgress fiber 节点,他们通过 alternate 属性连接。
currentFiber.alternate === workInProgressFiber
workInProgressFiber.alternate === currentFiber
- mount时, 首次执行 ReactDOM.render 会创建 fiberRootNode (源码中叫 fiberRoot )和 rootFiber 。其中 fiberRootNode 是整个应用的根节点, rootFiber 是 <App/> 所在组件树的根节点。
- 2.接下来进入 render阶段 ,根据组件返回的 JSX 在内存中依次创建 Fiber节点 并连接在一起构建 Fiber树 ,被称为 workInProgress Fiber树 。在构建 workInProgress Fiber树 时会尝试复用 current Fiber树 中已有的 Fiber节点 内的属性,在 首屏渲染 时只有 rootFiber 存在对应的 current fiber (即 rootFiber.alternate )。
- 3.已构建完的 workInProgress Fiber树 在 commit阶段 渲染到页面, fiberRootNode 的 current 指针指向 workInProgress Fiber树 使其变为 current Fiber 树 。
- update时, 触发状态改变,这会开启一次新的 render阶段 并构建一棵新的 workInProgress Fiber 树
JSX简介
JSX 在编译时会被 Babel 编译为 React.createElement 方法。React.createElement 最终会调用 ReactElement 方法返回一个包含组件数据的对象,该对象有个参数 $$typeof: REACT_ELEMENT_TYPE 标记了该对象是个 React Element ,还有 type、key、props、ref 等属性。
FunctionComponent 对应的 Element 的 type 字段为 AppFunc 自身,ClassComponent 对应的 Element 的 type 字段为 AppClass 自身;React 通过 ClassComponent 实例原型上的 isReactComponent 变量判断是否是 ClassComponent 。
JSX 与 Fiber 节点区别,JSX 是一种描述当前组件内容的数据结构,不包含组件在更新中的 优先级 ,state,用于 Renderer 的 标记 等信息。
Diff算法
- Diff算法 的本质是对比current Fiber和JSX对象,生成workInProgress Fiber。
- reconcileChildFibers只对同级元素进行 Diff ;两个不同类型的元素会产生出不同的树,会销毁其及其子孙节; 开发者可以通过 key prop 来暗示哪些子元素在不同的渲染下能保持稳定
- 单节点 Diff (newChild为object)
- 当 key一样 且 type不同 时,所以都需要标记删除。
- 当 key不同 时只代表遍历到的该 fiber 不能被 p 复用,后面还有兄弟 fiber 还没有遍历到。所以仅仅标记该 fiber 删除。
- 多节点 Diff (newChild为array)
- 第一轮遍历:处理 更新 的节点。
- 遍历 newChildren ,将 newChildren[i] 与 oldFiber 比较,判断 DOM节点 是否可复用。
- 如果可复用, i++ ,继续比较 newChildren[i] 与 oldFiber.sibling ,可以复用则继续遍历。
- key 一样 type 不同导致不可复用,会将 oldFiber 标记为 DELETION ,并继续遍历; key 不同导致不可复用,立即跳出整个遍历, 第一轮遍历结束。
- 如果 newChildren 或者 oldFiber 遍历完, 第一轮遍历结束。
- newChildren 与 oldFiber 同时遍历完,此时 Diff 结束。
- newChildren 没遍历完, oldFiber 遍历完,本次更新有新节点插入,我们只需要遍历剩下的 newChildren 为生成的 workInProgress fiber 依次标记 Placement 。
- newChildren 遍历完, oldFiber 没遍历完,有节点被删除了。所以需要遍历剩下的 oldFiber ,依次标记 Deletion 。
- newChildren 与 oldFiber 都没遍历完,有节点在这次更新中改变了位置。
- 第二轮遍历:处理剩下的不属于 更新 的节点。
- 将所有还未处理的 oldFiber 存入以 key 为key, oldFiber 为value的 Map 中。遍历剩余的 newChildren ,通过 newChildren[i].key 就能在 existingChildren map中找到 key 一样的 oldFiber 。
- 第一轮遍历中最后一个可复用的节点在 oldFiber 中的索引 lastPlacedIndex
- 遍历 newChildren 的一项从map中匹配到节点在 oldFiber 的中的索引 oldIndex
- oldIndex >= lastPlacedIndex 该可复用节点不需要移动,重置lastPlacedIndex的值为oldIndex,继续遍历newChildren
- oldIndex < lastPlacedIndex 该节点需要向右移动。
- 思考性能,我们要尽量减少将节点从后面移动到前面的操作
class组件状态更新update原理
- ClassComponent 与 HostRoot (即 rootFiber.tag 对应类型)共用同一种 Update结构 。
const update: Update<*> = {
eventTime,
lane, // 优先级相关字段,同`Update`优先级可能是不同的。
suspenseConfig,
tag: UpdateState,
// 更新挂载的数据,对于`ClassComponent`,`payload`为`this.setState`的第一个传参;
// 对于`HostRoot`,`payload`为`ReactDOM.render`的第一个传参
payload: null,
// 更新的回调函数。`this.setState`的第二个传参;layout阶执行
callback: null,
next: null, // 与其他`Update`连接形成单向环状链表。};
- 方法内部调用了两次 this.setState 。这会在该 fiber 中产生两个 Update ,个 Update 会组成链表并被包含在 fiber.updateQueue 中。( React Hooks 有 batchedUpdates 批量更新)
- Fiber节点 最多同时存在两个 updateQueue ,current updateQueue和workInProgress updateQueue,中断重新开始时,会基于 current updateQueue 克隆出 workInProgress updateQueue 。由于 current updateQueue.lastBaseUpdate 已经保存了上一次的 Update ,所以不会丢失。
- updateQueue 有三种类型,其中针对 HostComponent 类型我们在completeWork中数组形式[key, value]存变化的props; ClassComponent 与 HostRoot 使用的 UpdateQueue 结构如下:
const queue: UpdateQueue<State> = {
// 本次更新前该`Fiber节点`的`state`,`Update`基于该`state`计算更新后的`state`
baseState: fiber.memoizedState,
// `firstBaseUpdate`与`lastBaseUpdate`:本次更新前该`Fiber节点`已保存的`Update`。
// 以链表形式存在,链表头为`firstBaseUpdate`,链表尾为`lastBaseUpdate`。
// 之所以在更新产生前该`Fiber节点`内就存在`Update`,
// 是由于某些`Update`优先级较低所以在上次`render阶段`由`Update`计算`state`时被跳过。
firstBaseUpdate: null,
lastBaseUpdate: null,
// 触发更新时,产生的`Update`会保存在`shared.pending`中形成单向环状链表。
// `shared.pending` 会保证始终指向最后一个插入的`update`
// 当由`Update`计算`state`时这个环会被剪开并连接在`lastBaseUpdate`后面
// 单向环状链表使shared.pending.next指向第一个加入update
shared: {
pending: null,
},
// 数组。保存`update.callback !== null`的`Update`
effects: null,
};
- render阶段beginWork时 , shared.pending 的环被剪开并连接在 updateQueue.lastBaseUpdate 后面,以 fiber.updateQueue.baseState 为 初始state ,依次与遍历到的每个 Update 计算并产生新的 state (该操作类比 Array.prototype.reduce )
- 遍历时如果有优先级低的 Update 会被跳过。在其之前的B2由于优先级不够被跳过。 update 之间可能有依赖关系, 所以被跳过的 update 及其后面所有 update 会成为下次更新的 baseUpdate 。 (即 B2 –> C1 –> D2 )。在 commit 阶段结尾会再调度一次更新,该次更新中的 baseState 基于被跳过的前一个update计算出的值;C1执行了两次, componentWillXXX 也会触发两次。
- state 的变化在 render阶段 产生与上次更新不同的 JSX 对象,通过 Diff算法 产生 effectTag
shared.pending: A1 --> B2 --> C1 --> D2
第一次render阶段使用的Update: [A1, C1],
commit完后 baseState: 'A',baseUpdate: B2 --> C1 --> D2,memoizedState: 'AC'
第二次render阶段使用的Update: [B2, C1, D2],baseState: 'A'计算
commit完后memoizedState: 'ABCD'
react18相对react16.8新增的api和功能
- 并发渲染引擎。ReactDOM.createRoot(container) 取代 ReactDOM.render,开启并发能力。让更新可中断可恢复、可优先级调度,提升大页面的流畅度。
- 自动批处理(Automatic Batching)。跨事件、setTimeout、Promise 等的多个 setState 会自动合并一次渲染。 若需立即同步更新,可用 flushSync(fn) 强制同步刷新。
- useTransition & startTransition: 区分紧急更新(如用户输入)和过渡更新(如搜索结果渲染),标记非紧急更新为可中断任务,提升交互流畅性。
- useDeferredValue: 延迟渲染非关键值(如大型列表),类似防抖但更智能,避免阻塞用户操作。
并发渲染(Concurrent Rendering)Scheduler 原理
- Fiber 链表结构,渲染过程中可中断,中断后通过指针定位恢复位置(如 workInProgress 指针),天然支持可中断遍历。
- 双缓存机制 维护两棵 Fiber 树: Current Tree:当前渲染完成的 UI。 Work-in-Progress Tree:正在构建的更新树。 通过指针切换实现原子性更新,避免直接修改当前树。
- 时间分片,每次执行后shouldYield检查5ms是否有剩余时间。超时5ms或有优先级任务则中断当前任务。Render 阶段:可中断,处理 Fiber 链表的构建和差异计算(调用 shouldYield 判断是否中断)。 Commit 阶段:同步,应用 DOM 更新(无中断)
- 启动调度 调用 requestHostCallback 触发异步循环: 浏览器环境:基于 MessageChannel(0~1ms 延迟)。 降级方案:setTimeout(0)。
react触发更新机制api
- class组件ReactDOM.render,props,this.setState,this.forceUpdate
- 备注,this.forceUpdate创建的update对象 tag 为 ForceUpdate ,那么当前 ClassComponent 不会受其他 性能优化手段 ( shouldComponentUpdate | PureComponent )影响,必定会更新。
- 函数式组件调用useState、useReducer的dispatch方法更改状态,useContext订阅时,该context.provide的value值改变,useSelector(state => state.reducerA.val)返回redux的新值。
- React Context 的更新机制会绕过 shouldComponentUpdate(类组件)和 React.memo(函数组件)的性能优化逻辑,导致被优化的组件强制重渲染。Context 更新属于全局状态变更,通过 Fiber 树的特殊标记(updateQueue)强制触发渲染2。
- 批量更新失效;
- 异步回调中更新状态 场景:在 setTimeout、Promise.then、fetch 回调或 useEffect 的异步操作中调用 setState。
- 原生事件处理函数 场景:直接绑定浏览器原生事件(如 document.addEventListener)而非 React 合成事件。
- React 18 之前的遗留模式 场景:使用 ReactDOM.render(非 createRoot)挂载应用。
- 解决: React 18 默认在 createRoot 模式下,所有环境(包括异步回调)的更新均自动批处理;若需强制同步更新,可使用 flushSync API。使用 unstable_batchedUpdates: 在异步回调中包裹状态更新,强制批处理;
react context
- 使用
- React.createContext(defaultValue):创建 Context 对象,defaultValue 仅在无 Provider 包裹时生效。
- <Provider value={}> :包裹组件树,提供数据,当 Provider 的 value 变化时,所有消费组件会 强制重渲染 。内层Provider值会覆盖外层值。
- 消费数据的组件: 类组件:static contextType = MyContext 或 <Consumer>{value => (<><>)}</Consumer> ; 函数组件:useContext(MyContext);
- 机制
- 当 Provider 的 value 变化时,React 会主动遍历子树,直接触发所有使用 useContext 或 Consumer 的订阅的组件强制更新。此过程不受 shouldComponentUpdate 或 React.memo 阻断,即使组件的 props 未变化。
- shouldComponentUpdate 和 React.memo 仅拦截 props/state 变化,但 Context 更新属于全局状态变更,通过 Fiber 树的特殊标记(updateQueue)强制触发渲染。若 Context value 包含 { theme: 'dark', user: 'Alice' },即使组件仅使用 user 字段,theme 的变更仍会触发重渲染。
- 优化
- 按业务拆分为多个细粒度 Context,组件仅订阅所需 Context,减少无关渲染。
- 通过 useMemo,useCallback 冻结对象类型value引用,稳定 Context 的value引用。
- 再包一层或者往下拆一层,将 Context 值作为 props 传递给子组件,子组件用 React.memo 包裹,并通过 useMemo 过滤无关字段:
ErrorBoundary
React 的错误边界(Error Boundary)是一种类组件,用于捕获其子组件树在渲染过程中抛出的错误,并展示降级 UI。仅处理 渲染阶段同步错误(如 render、生命周期方法、构造函数) 。
- 事件处理函数,异步代码错误,try/catch + async/await;
- Promise使用.catch 回调;全局监听 unhandledrejection 事件捕获未处理的 Promise 错误;
react ref
react 性能优化
- 拆分组件优化渲染范围,仅受影响的子组件重渲染。按业务拆分状态数据,最少状态数据更新。
- 类组件用 PureComponent(自动浅比较),shouldComponentUpdate(手动比较),函数组件用 React.memo。
- immer.js会当修改深层次对象时,从改变的子节点到根节点的整一条路径对象地址都会改变。其他分支的对象地址不会改变。 juejin.cn/post/692609…
- useCallback/useMemo 缓存函数和对象。
- 懒加载(React.lazy)和代码分割(webpack的import的/ webpackChunkName /注释语法)。 import 执行会返回一个 Promise 值为module.default作为异步加载的手段
- 路由懒加载,结合react-router-dom里的lazy实现路由组件按需加载,首屏 JS 体积减少 30%~70%,加载时间缩短。
- 组件级别懒加载,对非首屏组件(如弹窗、富文本编辑器)按需加载。
- 循环正确使用key,避免用随机数或index和用index拼接其他的字段做key;绑定事件尽量不要使用箭头函数,使用useCallback。
- 时间分片,初始化加载大量数据到内存中。优先级控制,useTransition()标记为非紧急更新;
- 虚拟列表;分为渲染区,缓冲区,冲区的作用就是防止快速下滑或者上滑过程中,会有空白的现象。
- 计算出容器高度scorllBoxHeight和item的高度itemHeight,Math.ceil(scorllBoxHeight / itemHeight) + bufferCount一屏个数加上缓冲区个数endNum渲染出撑出滚动条
- 监听滚动容器的 onScroll 事件,根据 scrollTop 来计算渲染区域向上偏移量,使用translate3d开启cpu加速移动容器,并计算出新的startNum和endNum
- 双缓存,先在内存中处理操作DOM,并在GPU加速opacity:0,will-change:opacity的隐藏的DOM中预渲染大数据量DOM结构,最后再直接替换当前的DOM。
shouldComponentUpdate与React.memo
类组件用 PureComponent(自动浅比较),函数组件用 React.memo。
useSelector 与 useDispatch
- useSelector 与 useDispatch联合使用,实现状态读取与更新,替代传统的 connect 高阶组件
- useSelector的选择器函数的第一个参数是 Redux Store 的根状态(state),返回值即为组件中使用的数据。若需获取多个状态,可多次调用 useSelector。
useDispatch 执行异步操作,异步操作完成后,useSelector 会自动更新数据。
react hook原理
juejin.cn/post/694486…
- fiber.memoizedState : FunctionComponent 对应 fiber 保存的 Hooks 链表。
- 在 FunctionComponent render 前,根据(current === null || current.memoizedState === null)区分 mount 与 update ,可见 mount 时调用的 hook 和 update 时调用的 hook 实则是两个不同的函数。
- 一旦在条件语句中声明 hooks ,在下一次函数组件更新, hooks 链表结构,将会被破坏, current 树的 memoizedState 缓存 hooks 信息,和当前 workInProgress 不一致,如果涉及到读取 state 等操作,就会发生异常。
- hooks 通过什么来证明唯一性的,答案 ,通过 hooks 链表顺序。
ReactCurrentDispatcher.current = current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate
- Hook的数据结构
const hook: Hook = {
// `fiber.memoizedState`:`FunctionComponent`对应`fiber`保存的`Hooks`链表。
// `hook.memoizedState`:`Hooks`链表中保存的单一`hook`对应的数据。
memoizedState: null,
baseState: null, baseQueue: null, queue: null,
next: null, };
- 不同类型 hook 的 memoizedState 保存不同类型数据,具体如下:
- useState:对于 const [state, updateState] = useState(initialState) , memoizedState 保存 state 的值
- useReducer:对于 const [state, dispatch] = useReducer(reducer, {}); , memoizedState 保存 state 的值
- useEffect: memoizedState 保存包含 useEffect回调函数 、 依赖项 等的链表数据结构 effect ,你可以在 这里 (opens new window) 看到 effect 的创建过程。 effect 链表同时会保存在 fiber.updateQueue 中
- useRef:对于 useRef(1) , memoizedState 保存 {current: 1}
- useMemo:对于 useMemo(callback, [depA]) , memoizedState 保存 [callback(), depA]
- useCallback:对于 useCallback(callback, [depA]) , memoizedState 保存 [callback, depA] 。与 useMemo 的区别是, useCallback 保存的是 callback 函数本身,而 useMemo 保存的是 callback 函数的执行结果
- 有些 hook 是没有 memoizedState 的,列如:useContext
- useState 与 useReducer
- 本质来说, useState 只是预置了 reducer 的 useReducer , useReducer 的 lastRenderedReducer 为传入的 reducer 参数
- 声明阶段
function mountReducer<S>(
initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
const hook = mountWorkInProgressHook();
const queue = (hook.queue = { pending: null, dispatch: null, lastRenderedReducer: reducer, lastRenderedState: (initialState: any), });
return [hook.memoizedState, dispatch.bind(this)(fiber,hook.queue,]; }
function updateReducer<S, I, A>( reducer: (S, A) => S, initialArg: I, init?: I => S, ): [S, Dispatch<A>] { const hook = updateWorkInProgressHook(); const queue = hook.queue;
queue.lastRenderedReducer = reducer;
if (hook.queue.pending) { }
const dispatch: Dispatch<A> = (queue.dispatch: any); return [hook.memoizedState, dispatch.bind(this)(fiber,hook.queue,)]; }
- React 用一个标记变量 didScheduleRenderPhaseUpdate 判断是否是 render阶段 触发的更新,防止这次 更新 会开启一次新的 render阶段 ,最终会无限循环更新。
- 调用阶段,执行 dispatchAction (opens new window) ,此时该 FunctionComponent 对应的 fiber 以及 hook.queue 已经通过调用 bind 方法预先作为参数传入。
function dispatchAction(fiber, queue, action) {
var update = { eventTime: eventTime, lane: lane, suspenseConfig: suspenseConfig, action: action, eagerReducer: null, eagerState: null, next: null };
var alternate = fiber.alternate; if (fiber === currentlyRenderingFiber$1 || alternate !== null && alternate === currentlyRenderingFiber$1) { didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true; } else { if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) { } scheduleUpdateOnFiber(fiber, lane, eventTime); } }
- 无论是类组件调用 setState ,还是函数组件的 dispatchAction ,都会产生一个 update 对象,里面记录了此次更新的信息,然后将此 update 放入待更新的 pending 队列中, dispatchAction 第二步就是判断当前函数组件的 fiber 对象是否处于渲染阶段,如果处于渲染阶段,那么不需要我们在更新当前函数组件,只需要更新一下当前 update 的 expirationTime 即可。
- 如果当前 fiber 没有处于更新阶段。那么通过调用 lastRenderedReducer 获取最新的 state ,和上一次的 currentState ,进行浅比较,如果相等,那么就退出,这就证实了为什么 useState ,两次值相等的时候,组件不渲染的缘由了,这个机制和 Component 模式下的 setState 有必定的区别。
- 如果两次 state 不相等,那么调用 scheduleUpdateOnFiber 调度渲染当前 fiber , scheduleUpdateOnFiber 是 react 渲染更新的主要函数。
- useEffect
- 在 flushPassiveEffects 方法内部会在commit阶段的layout后从全局变量 rootWithPendingPassiveEffects 获取 effectList 。
- useEffect 和 useLayoutEffect 中也有同样的问题,所以他们都遵循“全部销毁”再“全部执行”的顺序。
- 向 pendingPassiveHookEffectsUnmount 数组内 push 数据的操作发生在 layout阶段 commitLayoutEffectOnFiber 方法内部的 schedulePassiveEffects 方法中。
function mountEffect(
create,
deps,
) {
const hook = mountWorkInProgressHook()
const nextDeps = deps === undefined ? null : deps
hook.memoizedState = pushEffect(
HookHasEffect | hookEffectTag,
create, // useEffect 第一次参数,就是副作用函数
undefined,
nextDeps, // useEffect 第二次参数,deps
)
}
- pushEffect 第一创建一个 effect ,判断组件如果第一次渲染,那么创建 componentUpdateQueue ,就是 workInProgress 的 updateQueue 。然后将 effect 放入 updateQueue 中。
- React 采用深度优先搜索算法,在 render 阶段遍历 fiber 树时,把每一个有副作用的 fiber 筛选出来,最后构建生成一个只带副作用的 effect list 链表。 在 commit 阶段, React 拿到 effect list 数据后,通过遍历 effect list ,并根据每一个 effect 节点的 effectTag 类型,执行每个 effect ,从而对相应的 DOM 树执行更改。
- 更新阶段updateEffect , useEffect 做的事很简单,判断两次 deps 相等,如果相等说明此次更新不需要执行,则直接调用 pushEffect ,这里注意 effect 的标签, hookEffectTag ,如果不相等,那么更新 effect ,并且赋值给 hook.memoizedState ,这里标签是 HookHasEffect | hookEffectTag ,然后在 commit 阶段, react 会通过标签来判断,是否执行当前的 effect 函数。
- useRef
- seRef 仅仅是返回一个包含 current 属性的对象,任何需要被”引用”的数据都可以保存在 ref 中
- 函数组件更新useRef做的事情更简单,就是返回了缓存下来的值,也就是无论函数组件怎么执行,执行多少次, hook.memoizedState 内存中都指向了一个对象,所以解释了 useEffect , useMemo 中,为什么 useRef 不需要依赖注入,就能访问到最新的改变值。
function mountRef<T>(initialValue: T): {|current: T|} {
const hook = mountWorkInProgressHook();
const ref = {current: initialValue};
hook.memoizedState = ref;
return ref;
}
function updateRef<T>(initialValue: T): {|current: T|} { const hook = updateWorkInProgressHook(); return hook.memoizedState; }
- 在 render阶段 的 beginWork 中,给 HostComponent 、 ClassComponent 如果包含 ref 操作,那么也会赋值相应的 Ref effectTag 。
- 对于 mount , workInProgress.ref !== null ,即存在 ref 属性
- 对于 update , current.ref !== workInProgress.ref ,即 ref 属性改变
- 在 commit阶段 的 mutation阶段 中,对于 ref 属性改变的情况,需要先移除之前的 ref ,在 layout 阶段重新赋值。
- useMemo 与 useCallback
- 执行 useMemo 函数,做的事情实际很简单,就是判断两次 deps 是否相等,如果不想等,证明依赖项发生改变,那么执行 useMemo 的第一个函数,得到新的值,然后重新赋值给 hook.memoizedState ,如果相等 证明没有依赖项改变,那么直接获取缓存的值。
function mountCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = mountWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
hook.memoizedState = [callback, nextDeps];
return callback;
}
function updateCallback<T>(callback: T, deps: Array<mixed> | void | null): T {
const hook = updateWorkInProgressHook();
const nextDeps = deps === undefined ? null : deps;
const prevState = hook.memoizedState;
if (prevState !== null) { if (nextDeps !== null) { const prevDeps: Array<mixed> | null = prevState[1]; if (areHookInputsEqual(nextDeps, prevDeps)) { return prevState[0]; } } }
hook.memoizedState = [callback, nextDeps]; return callback; }
7.Scheduler
juejin.cn/post/684490…
- 时间切片与任务调度
- Message Channel其特点是其两个端口属性支持双向通信和异步订阅发布事件( port.postMessage(…) )。
- Fiber是一个的节点对象,使用链表的形式将所有Fiber节点连接,形成链表树,链表能缓存上次中断遍历的位置
- 在 React 的 render 阶段执行调度工作循环和计算工作循环时,执行每一个工作中Fiber,都会通过 Scheduler 提供的 shouldYield 方法(检查5毫秒是否到期的条件)判断是否需要中断遍历。
- 运行一次 异步的 MessageChannel 的 port.postMessage(…) 方法,检查是否存在事件响应、更高优先级任务或其他代码需要执行 ,如果有则执行;如果没有则判断执行完一个任务中的回调函数后,检测其是否返回函数(5毫秒时间切片过期后root.callbackNode === originalCallbackNode就会返回其自身)。若返回,则将其作为任务新的回调函数,继续进行工作循环;若未返回,则执行下一个任务的回调函数。在执行调度任务过程中,会执行 requestHostCallback(…) , 从而调用 port.postMessage(…) ,执行剩下的工作中Fiber。
- 在执行完所有工作中fiber后,React进入提交步骤,更新DOM。2. 任务的回调函数返回空值,调度工作循环因此(运行任务步骤中第二点:若任务的回调函数执行后返回为空,则执行下一个任务)完成此任务,并将此任务从任务队列中删除。
- 高优先级插队
- 触发点击事件后,React会运行内部的合成事件相关代码,然后执行一个执行优先级的方法(runWithPriority(UserBlockingPriority, …)),优先级参数为“用户交互UserBlockingPriority”,接着进行 setState 操作。
- 在确保root被安排任务的方法(ensureRootIsScheduled),由于目前的优先级更高,调度中心撤销对之前低优先级任务的安排,并将之前低优先级任务的回调置空,确保它之后不会被执行。
- 执行高优先级任务,当执行到开始计算工作中类Fiber( class ConcurrentSchedulingExample ),执行更新队列方法时,React将循环遍历工作中类fiber的update环状链表。低优先级的update被跳过,其和其后面update被保存的在baseUpdate中,此处是之后恢复低优先级的关键所在。
- 在完成优先级任务过程的提交渲染DOM步骤中,渲染DOM后,会将root的callbackNode(其名字容易误导其功能,实则就是调度任务,用callbackTask或许更合适)设为空值。
- 执行确保root被安排任务的方法中,由于baseUpdate中不为空且root的callbackNode为空值,所以创建新的任务,即重新创建一个新的低优先级任务。并将任务放入任务列表中。
- 重新执行低优先级任务,所以恢复低优先级任务必定是重新完整执行一遍。
C redux,react-redux,中间件设计
juejin.cn/post/684490…
1.redux
- redux就为我们提供了一种响应式管理公共状态的方案
import { reducer } from './reducer'
export const createStore = (reducer,applyMiddleware) => {
if (heightener) {
return heightener(createStore)(reducer)
}
let currentState = {} let observers = [] function getState() { return currentState } function dispatch(action) { currentState = reducer(currentState, action) observers.forEach(fn => fn()) } function subscribe(fn) { observers.push(fn) } dispatch({ type: '@@REDUX_INIT' }) return { getState, subscribe, dispatch } }
const store = createStore(reducer) store.subscribe(() => { console.log('组件1收到 store 的通知') }) store.subscribe(() => { console.log('组件2收到 store 的通知') }) store.dispatch({ type: 'plus' })
2.react-redux
- Provider实现,Provider是一个组件,接收store并放进全局的 context 对象,我们就能在组件中通过this.context.store这样的形式取到store,不需要再单独import store。
import React from 'react'
import PropTypes from 'prop-types'
export class Provider extends React.Component {
static childContextTypes = {
store: PropTypes.object
}
getChildContext() { return { store: this.store } }
constructor(props, context) { super(props, context) this.store = props.store }
render() { return this.props.children } }
- connect这种设计,是 装饰器模式 的实现,所谓装饰器模式,简单地说就是对类的一个包装,动态地拓展类的功能。connect以及React中的高阶组件(HoC)都是这一模式的实现。除此之外,也有更直接的缘由:这种设计能够兼容ES7的 装饰器(Decorator) ,使得我们可以用@connect这样的方式来简化代码,有关@connect的使用可以看这篇
export function connect(mapStateToProps, mapDispatchToProps) {
return function(Component) {
class Connect extends React.Component {
componentDidMount() {
this.context.store.subscribe(this.handleStoreChange.bind(this));
}
handleStoreChange() {
this.forceUpdate()
}
render() {
return (
<Component
{ ...this.props }
{ ...mapStateToProps(this.context.store.getState()) }
{ ...mapDispatchToProps(this.context.store.dispatch) }
/>
) } } Connect.contextTypes = { store: PropTypes.object } return Connect } }
3.中间件设计
- 中间件进一步柯里化,让next通过参数传入
const logger = store => next => action => {
console.log('进入log1')
let result = next(action)
console.log('离开log1')
return result
}
const thunk = store => next =>action => { console.log('thunk') const { dispatch, getState } = store return typeof action === 'function' ? action(store.dispatch) : next(action) }
const logger3 = store => next => action => { console.log('进入 log3') let result = next(action) console.log('离开 log3') return result }
- 洋葱圈模型 ,进入log1 -> 执行next(mid2) -> 进入log2 -> 执行next(mid3) -> 进入log3 -> 执行next -> next执行完毕(dispatch) -> 离开log3 -> 回到上一层中间件,执行上层中间件next之后的语句 -> 离开log2 -> 回到中间件log1, 执行log1的next之后的语句 -> 离开log1
- applyMiddleware实现
const applyMiddleware = (...middlewares) => createStore => reducer => {
const store = createStore(reducer)
let { getState, dispatch } = store
const params = {
getState,
dispatch: (action) => dispatch(action)
}
const middlewareArr = middlewares.map(middleware => middleware(params)) dispatch = compose(...middlewareArr)(dispatch) return { ...store, dispatch } }
function compose(...fns) { if (fns.length === 0) return arg => arg if (fns.length === 1) return fns[0] return fns.reduce((res, cur) =>(...args) => res(cur(...args))) }
- compose 内部使用reduce巧妙地组合了中间件函数,使传入的中间件函数变成 (…arg) => mid1(mid2(mid3(…arg))) 这种形式,从右往左执行,mid3作为next传给mid2,mid2作为next传给mid1。执行mid1的next相当于执行mid2…
C React事件系统工作原理
juejin.cn/post/695563…
- 合成事件,在 react 中,我们绑定的事件 onClick 等,并不是原生事件,而是由原生事件合成的 React 事件,列如 click 事件合成为 onClick 事件。列如 blur , change , input , keydown , keyup 等 , 合成为 onChange 。 react 并不是一开始,把所有的事件都绑定在 document 上,而是采取了一种按需绑定,列如发现了 onClick 事件,再去绑定 document click 事件。真实的 dom 上的 click 事件被单独处理,已经被 react 底层替换成空函数。
- React 想实现一个全浏览器的框架, 为了实现这种目标就需要提供全浏览器一致性的事件系统,以此抹平不同浏览器的差异。
- 将事件绑定在 document 统一管理,防止许多事件直接绑定在原生的 dom 元素上。从而免去了去操作 removeEventListener 或者同步 eventlistenermap 的操作,所以其执行效率将会大大提高,相当于全局给我们做了一次事件委托
1.事件合成,插件机制
- 在 React 中,处理 props 中事件的时候,会根据不同的事件名称,找到对应的事件插件,然后统一绑定在 document 上
const SimpleEventPlugin = {
eventTypes:{
'click':{
phasedRegistrationNames:{
bubbled: 'onClick', // 对应的事件冒泡 - onClick
captured:'onClickCapture' //对应事件捕获阶段 - onClickCapture
},
dependencies: ['click'], //事件依赖
...
},
'blur':{ },
...
}
extractEvents:function(topLevelType,targetInst,){ }
}
第一事件插件是一个对象,有两个属性,第一个 extractEvents 作为事件统一处理函数,第二个 eventTypes 是一个对象,对象保存了原生事件名和对应的配置项 dispatchConfig 的映射关系。由于 v16React 的事件是统一绑定在 document 上的,React 用独特的事件名称列如 onClick 和 onClickCapture ,来说明我们给绑定的函数到底是在冒泡事件阶段,还是捕获事件阶段执行。
1.事件绑定
- ① 在React,调用diffProperties,diffDOM元素类型的fiber的props对象上的 memoizedProps 和 pendingProps 的时候,如果发现是React合成事件,列如 onClick ,会就会调用 legacyListenToEvent 函数。在 legacyListenToEvent 函数中,先找到 React 合成事件对应的原生事件集合,列如 onClick -> ['click'] , onChange -> [ blur , change , input , keydown , keyup ],然后遍历依赖项的数组,绑定事件。
- ② 根据React合成事件类型,找到对应的原生事件的类型,然后调用判断原生事件类型,大部分事件都按照冒泡逻辑处理,少数事件会按照捕获逻辑处理(列如 scroll 事件)。
- ③ 调用 addTrappedEventListener 进行真正的事件绑定,添加事件监听器 addEventListener 绑定在 document 上,绑定我们的事件统一处理函数 dispatchEvent 的几个默认参数。
function addTrappedEventListener(targetContainer,topLevelType,eventSystemFlags,capture){
const listener = dispatchEvent.bind(null,topLevelType,eventSystemFlags,targetContainer)
if(capture){
}else{
targetContainer.addEventListener(topLevelType,listener,false)
}
}
- ④ 有一点值得注意: 只有上述那几个特殊事件列如 scorll , focus , blur 等是在事件捕获阶段发生的,其他的都是在事件冒泡阶段发生的,无论是 onClick 还是 onClickCapture 都是发生在冒泡阶段 ,至于 React 本身怎么处理捕获逻辑的。我们接下来会讲到。
2.事件触发
- ①第一通过统一的事件处理函数 dispatchEvent 。由于 dispatchEvent 前三个参数已经被bind了进去,所以真正的事件源对象 event ,被默认绑定成第四个参数,根据事件源对象,找到 e.target 真实的 dom 元素。然后根据 dom 元素,找到与它对应的 fiber 对象。然后正式进去 legacy 模式的事件处理系统,进行批量更新batchedEventUpdates。
export function batchedEventUpdates(fn,a){
isBatchingEventUpdates = true;
try{
fn(a)
}finally{
isBatchingEventUpdates = false
}
}
- handleTopLevel 最后的处理逻辑就是执行我们说的事件处理插件(SimpleEventPlugin)中的处理函数 extractEvents 。
- 第一形成 React 事件独有的合成事件源对象event,这个对象,保存了整个事件的信息,里面单独封装了列如 stopPropagation 和 preventDefault 等方法。将作为参数传递给真正的事件处理函数(handerClick)。
- 然后声明事件执行队列 ,按照 冒泡 和 捕获 逻辑,从事件源开始逐渐向上,查找dom元素类型HostComponent对应的fiber ,收集上面的 React 合成事件,例如 onClick / onClickCapture ,对于冒泡阶段的事件( onClick ),将 push 到执行队列后面 , 对于捕获阶段的事件( onClickCapture ),将 unShift 到执行队列的前面。最终形成一个事件执行队列,React就是用这个队列,来模拟事件捕获->事件源->事件冒泡这一过程。
- 将事件执行队列event._dispatchListeners,保存到React事件源对象上。等待执行。
- ③最后通过 runEventsInBatch 执行事件队列, 从上往下先执行捕获阶段再往上执行冒泡阶段 。如果发现阻止冒泡,那么break跳出循环,最后重置事件源,放回到事件池中,完成整个流程。 dispatchListeners[i](event) 就是执行我们的事件处理函数列如 handerClick ,从这里我们知道, 我们在事件处理函数中,返回 false ,并不会阻止浏览器默认行为 。
function runEventsInBatch(){
const dispatchListeners = event._dispatchListeners
const dispatchInstances = event._dispatchInstances
if (Array.isArray(dispatchListeners)) {
for (let i = 0
if (event.isPropagationStopped()) { /* 判断是否已经阻止事件冒泡 */
break
}
dispatchListeners[i](event)
}
}
/* 执行完函数,置空两字段 */
event._dispatchListeners = null
event._dispatchInstances = null
}
handerClick = () => console.log(1)
handerClick1 = () => console.log(2)
handerClick2 = () => console.log(3)
handerClick3= () => console.log(4)
render(){
return <div onClick={ this.handerClick2 } onClickCapture={this.handerClick3} >
<button onClick={ this.handerClick } onClickCapture={ this.handerClick1 } className="button" >点击</button>
</div>
}
打印 // 4 2 1 3
- React会在一个原生事件里触发所有相关节点(只对原生组件)的 onClick 事件, 在执行这些 onClick 之前 React 会打开批量渲染开关,这个开关会将所有的 setState 变成异步函数(多次setState只会触发一次render)。
- setTimeout(() => {}),document.addEventListener('click',() => {}),Promise.then(() => {})等是是同步的,(多次setState只会触发多次render)。由于此时 batchedEventUpdates中已经执行完 isBatchingEventUpdates = false ,所以批量更新被打破。
- React v17事件
- React v17事件统一绑定container上,ReactDOM.render(app, container);而不是document上,这样好处是有利于微前端的,微前端一个前端系统中可能有多个应用,如果继续采取全部绑定在 document 上,那么可能多应用下会出现问题。
- React 17 中终于支持了原生捕获事件的支持, 对齐了浏览器原生标准。同时 onScroll 事件不再进行事件冒泡。 onFocus 和 onBlur 使用原生 focusin , focusout 合成。
- 撤销事件池 React 17 撤销事件池复用,也就解决了上述在 setTimeout 打印,找不到 e.target 的问题。
- js原生事件 addEventListener(type, listener, useCapture = false);
- useCapture 一个布尔值,表明在 DOM 树中注册了 listener 的元素,是否要先于它下面的 EventTarget 调用该 listener 。当 useCapture(设为 true)时,沿着 DOM 树向上冒泡的事件不会触发 listener。当一个元素嵌套了另一个元素,并且两个元素都对同一事件注册了一个处理函数时,所发生的事件冒泡和事件捕获是两种不同的事件传播方式。事件传播模式决定了元素以哪个顺序接收事件。 useCapture 默认为 false 。
- 对于事件目标上的事件监听器来说,事件会处于“目标阶段”,而不是冒泡阶段或者捕获阶段。捕获阶段的事件监听器会在任何非捕获阶段的事件监听器之前被调用。
- DOM 事件流( event flow )存在三个阶段:事件捕获阶段、处于目标阶段、事件冒泡阶段。
- 事件捕获(event capturing) : 当鼠标点击或者触发 dom 事件时(被触发 dom 事件的这个元素被叫作事件源),浏览器会从根节点 =>事件源(由外到内)进行事件传播。注册冒泡阶段的不执行
- 事件冒泡(dubbed bubbling) :当一个元素接收到事件的时候,会把他接收到的事件传给自己的父级,一直到 window 即事件源 =>根节点(由内到外)进行事件传播。注册捕获阶段的不执行。
C React-Router原理
V5:juejin.cn/post/695024…
V6:juejin.cn/post/706955…
前端路由
- hash模式 使用hashchange事件,点击a标签hash改变不会发送到后端。在不刷新页面的前提下修改url,监听和匹配url的变化,并根据路由匹配渲染页面内容
- History模式 使用H5引入的popstate事件(点击浏览器后退、前进、a标签点击或者history.back()、history.forward()、history.go(),才会触发,pushState 和 replaceState不会触发),重写 a标签 的点击事件,阻止了默认的页面跳转行为,并通过(pushState增加一个 和 replaceState替换当前)无刷新地改变 url,最后渲染对应路由的内容。
- Link在最后渲染的时候实则是创建了a标签,同时添加了一个onClick的监听事件,判定是否应该阻止a标签的默认跳转,如果阻止的话,则根据replace的props值决定执行history.push还是执行history.replace。如果不阻止的话,则实则与直接a标签的写法类似了,当点击操作触发url的hash值改变。
- Link只负责触发url变更,Route只负责根据url渲染组件,history的作用是监听url变更,并同时通知Route重新渲染。
- 前置知识 react context 。
- 创建一个 Context 对象。当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身 最近 的那个匹配的 Provider 中读取到当前的 context 值。
- Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。
- 当 Provider 的 value 值发生变化时(使用 Object.is 来做比较),它内部的所有消费组件都会重新渲染。从 Provider 到其内部 consumer 组件(包括 .contextType 和 useContext )的传播不受制于 shouldComponentUpdate ,consumer 组件在其祖先组件跳过更新的情况下也能更新。使用 memo 来跳过重新渲染并不妨碍子级接收到新的 context 值。
- useContext(SomeContext) 用组件返回 context 的值。它被确定为传递给树中调用组件上方最近的 SomeContext.Provider 的 value 。如果没有这样的 provider,那么返回值将会是为创建该 context 传递给 createContext 的 defaultValue 。返回的值始终是最新的。如果 context 发生变化,React 会自动重新渲染读取 context 的组件。useContext 的机制是使用这个 hook 的组件在 context 发生变化时都会重新渲染。
const MyContext = React.createContext(defaultValue)
<MyContext.Provider value={/* 某个值 */}>
- 新版本的 router 没有 Switch 组件,取而代之的是 Routes,route 必须配合 Routes 使用,
- 引入 Outlet 占位功能,Outlet是真正的路由组件要挂载的地方,而且不受到组件层级的影响;使嵌套路由结构会更加清晰,不像v5一样配置二级路由,需要在业务组件中配置。
- 状态获取 (useLocation); 路由跳转 (useNavigate); 获取 url 上的动态路由信息 (useParams); 获取,设置url 参数 (useSearchParams); 路由的动态配置 (useRoutes);
- 传递 history 的 NavigationContext 对象,传递 location 的 LocationContext 对象,传递视图的 OutletContext 对象,
1.BrowserRouter与Router(v6)
- 通过 createBrowserHistory 创建 history 对象,并通过 useRef 保存 history 对象。
- 当 history 发生变化(浏览器人为输入,获取 a 标签跳转,api 跳转等 )。派发更新,渲染整个 router 树。 这是和老版本的区别,老版本里面,监听路由变化更新组件是在 Router 中进行的。
- 在老版本中,有一个 history 对象的概念,新版本中把它叫做 navigator 。
export function BrowserRouter({
basename,
children,
window
}: BrowserRouterProps) {
/* 通过 useRef 保存 history 对象 */
let historyRef = React.useRef<BrowserHistory>()
if (historyRef.current == null) {
historyRef.current = createBrowserHistory({ window })
}
let history = historyRef.current let [state, setState] = React.useState({ action: history.action, location: history.location }) /* history 监听 histosy 变化。*/ React.useLayoutEffect(() => history.listen(setState), [history])
return ( <Router basename={basename} children={children} location={state.location} navigationType={state.action} navigator={history} />
) }
- 通过 React context 来传递负责跳转路由等功能的 navigator 对象和路由信息的 location 对象。
- 当路由变化时候,在 BrowserRouter 中通过 useState 改变 location ,那么当 location 变化的时候, LocationContext 发生变化,消费 LocationContext 会更新。
function Router({basename,children,location:locationProp,navigator}){
let navigationContext = React.useMemo(
() => ({ basename, navigator, static: staticProp }),
[basename, navigator, staticProp]
);
const { pathname, search, hash, state, key } = locationProp
let location = React.useMemo(() => {
return { pathname, search, hash, state, key }
},[basename, pathname, search, hash, state, key])
return (
<NavigationContext.Provider value={navigationContext}>
<LocationContext.Provider children={children} value={{ location, navigationType }} />
</NavigationContext.Provider>
) }
2.useRoutes(Routes)和Outlet(v6)
- Route组件不是常规的组件,可以理解成一个空函数。Routes会过 createRoutesFromChildren 处理把 Route 组件给结构化。
- useRoutes ,可以直接把 route 配置结构变成 element 结构,并且负责展示路由匹配的路由组件,那么 useRoutes 就是整个路由体系核心
- 使用 <Routes /> 的时候,本质上是通过 useRoutes 返回的 react element 对象。
export function Routes({children,location }) {
return useRoutes(createRoutesFromChildren(children), location);
}
function createRoutesFromChildren(children) { let routes = []; Children.forEach(children, element => { let route = { caseSensitive: element.props.caseSensitive, element: element.props.element, index: element.props.index, path: element.props.path }; if (element.props.children) { route.children = createRoutesFromChildren(element.props.children); } routes.push(route); }); return routes; }
- useRoutes 内部用了 useLocation 。 当 location 对象变化的时候,useRoutes 会重新执行渲染。
- 第一阶段 ,生成对应的 pathname :还是以上面的 demo 为例子,列如切换路由 /children/child1 ,那么 pathname 就是 /children/child1 。
- 通过 matchRoutes ,找到匹配的路由分支。 如 /children/child1 ,扁平化后匹配的路由结构matches,{pathname: '/children', route: {element:}, pathname: '/children/child1', route: {element:},}
function useRoutes(routes, locationArg) {
let locationFromContext = useLocation();
let matches = matchRoutes(routes, { pathname: remainingPathname }); console.log('----match-----',matches)
return _renderMatches(matches && matches.map(match => Object.assign({}, match, { params: Object.assign({}, parentParams, match.params), pathname: joinPaths([parentPathnameBase, match.pathname]), pathnameBase: match.pathnameBase === "/" ? parentPathnameBase : joinPaths([parentPathnameBase, match.pathnameBase]) })), parentMatches); }
function _renderMatches(matches, parentMatches) { if (parentMatches === void 0) { parentMatches = []; } if (matches == null) return null; return matches.reduceRight((outlet, match, index) => { return createElement(RouteContext.Provider, { children: match.route.element !== undefined ? match.route.element : createElement(Outlet, null), value: { outlet, matches: parentMatches.concat(matches.slice(0, index + 1)) } }); }, null); }
- reduceRight 是从右向左开始遍历,match 结构是 root -> children -> child1, reduceRight 把前一项返回的内容作为后一项的 outlet(通过 provider 方式逐层传递 Outlet)
- 第一通过 provider 包裹 child1,那么 child1 真正需要渲染的内容 Child1 组件 ,将被当作 provider 的 children,最后把当前 provider 返回,child1 没有子路由,所以第一层 outlet 为 null。
- 接下来第一层返回的 provider,讲作为第二层的 outlet ,通过第二层的 provider 的 value 里面 outlet 属性传递下去。然后把 Layout 组件作为 children 返回。
- 接下来渲染的是第一层的 Provider ,所以 Layout 会被渲染,那么 Child1 并没有直接渲染,而是作为 provider 的属性传递下去。
- child1 是在 container 中用 Outlet 占位组件的形式渲染的,用 useContext 把第一层 provider 的 outlet 获取到然后渲染就可以渲染 child1 的 provider 了,而 child1 为 children 也就会被渲染了
- 就是获取上一级的 Provider 上面的 outlet ,(在上面 demo 里就是包裹 Child1 组件的 Provider ),然后渲染 outlet ,所以二级子路由就可以正常渲染了。
export function Outlet(props: OutletProps): React.ReactElement | null {
return useOutlet(props.context);
}
export function useOutlet(context?: unknown): React.ReactElement | null { let outlet = React.useContext(RouteContext).outlet; if (outlet) { return ( <OutletContext.Provider value={context}>{outlet}</OutletContext.Provider>
); } return outlet; }
3.BrowserRouter 和 HashRouter(v5)
- 引入history库用不同的createHistory创建了一个 history对象,然后将其和子组件一起透传给了Router。
- history 使您可以在任何运行 JavaScript 的地方轻松管理会话历史记录。一个 history 对象可以抽象出各种环境中的差异,并提供一个最小的API,使您可以管理历史记录堆栈,导航和在会话之间保持状态
const App = () => {
return (
<BrowserRouter>
<Route path="/" component={Home}></Route>
</BrowserRouter>
); }
import React from "react"; import { Router } from "react-router"; import { createBrowserHistory as createHistory } from "history";
class BrowserRouter extends React.Component { history = createHistory(this.props);
render() { return <Router history={this.history} children={this.props.children} />; } }
export default BrowserRouter;
import React from "react"; import { Router } from "react-router"; import { createHashHistory as createHistory } from "history";
class HashRouter extends React.Component { history = createHistory(this.props);
render() { return <Router history={this.history} children={this.props.children} />; } }
export default HashRouter;
4.Router监听location变化,派发更新(v5)
- 绑定了路由监听事件,使每次路由的改变都触发setState更新location对象(触发子组件重新渲染),给子组件包了一层context,让路由信息( history 和 location 对象)能传递给其下所有子孙组件,子孙组件(switch,route组件)在拿到当前路由信息后,取出对应的组件并传入路由信息渲染出来,通过props能获取路由信息。
- 一个项目应该有一个根 Router , 来产生切换路由组件之前的更新作用。如果存在多个 Router 会造成,会造成切换路由,页面不更新的情况。
import RouterContext from "./RouterContext";
import React from 'react';
class Router extends React.Component { static computeRootMatch(pathname) { return { path: "/", url: "/", params: {}, isExact: pathname === "/" }; }
constructor(props) { super(props);
this.state = { location: props.history.location }; this._isMounted = false; this._pendingLocation = null;
if (!props.staticContext) { this.unlisten = props.history.listen(location => { if (this._isMounted) { this.setState({ location }); } else { this._pendingLocation = location; } }); } }
componentDidMount() { this._isMounted = true;
if (this._pendingLocation) { this.setState({ location: this._pendingLocation }); } }
componentWillUnmount() { if (this.unlisten) this.unlisten(); }
render() { return ( <RouterContext.Provider children={this.props.children || null} value={{ history: this.props.history, location: this.state.location, match: Router.computeRootMatch(this.state.location.pathname), staticContext: this.props.staticContext }} />
); } } export default Router
5.Route组件页面承载容器(v5)
- 引入了 path-to-regexp 来拼接路径正则以实现不同模式的匹配(生成match对象),路由组件 作为一个高阶组件包裹业务组件, 通过比较当前路由信息match对象和传入的path,以不同的优先级来渲染对应组件
- 组件的渲染逻辑,子组件> component属性传入的组件 > children是函数 这样的优先级渲染
import React from "react";
import RouterContext from "./RouterContext";
import matchPath from "../utils/matchPath.js";
function isEmptyChildren(children) { return React.Children.count(children) === 0; }
class Route extends React.Component { render() { return ( {} <RouterContext.Consumer>
{} {context => { const location = this.props.location || context.location; const match = this.props.computedMatch ? this.props.computedMatch : this.props.path ? matchPath(location.pathname, this.props) : context.match;
const props = { ...context, location, match }; let { children, component, render } = this.props; if (Array.isArray(children) && isEmptyChildren(children)) { children = null; }
return ( <RouterContext.Provider value={props}>
{props.match ? children ? typeof children === "function" ? children(props) : children : component ? React.createElement(component, props) : render ? render(props) : null : typeof children === "function" ? children(props) : null} </RouterContext.Provider>
); }} </RouterContext.Consumer>
); } }
export default Route;



















暂无评论内容