一文了解 React 重点:Fiber 架构、diff 算法等

您所在的位置:网站首页 diff算法react 一文了解 React 重点:Fiber 架构、diff 算法等

一文了解 React 重点:Fiber 架构、diff 算法等

2024-07-04 23:07:34| 来源: 网络整理| 查看: 265

文章主要概括了 React 的构成,初始化流程,更新流程,优化方向,Fiber 架构,事件机制,diff 算法,React-Redux 和 react-router 的原理。主要用于对 React 原理有个基本的认识。

‘React 构成

基础模块, react 基础 API 及组件类,组件内定义 render 、setState 方法和生命周期相关的回调方法。

渲染模块,针对不同宿主环境采用不同的渲染方法实现,如 react-dom, react-webgl, react-native, react-art, 依赖 react-reconciler, 注入相应的渲染方法到 reconciler 中。

核心模块,负责调度算法及 Fiber tree diff, 连接 react 及 renderer 模块,注入 setState 方法到 component 实例中,在 diff 阶段执行 react 组件中 render 方法,在 patch 阶段执行 react 组件中生命周期回调并调用 renderer 中注入的相应的方法渲染真实视图结构。

React 初始化流程

JSX 会经过 babel 编译成 React.createElement 递归调用的表达式,React.createElement 会在 render 函数被调用的时候执行,换句话说,当 render 函数被调用的时候,会返回一个 element(组成虚拟 DOM 树的节点)。

element 类型为对象时分为原生 DOM(调用 ReactDOMComponent)和自定义类(ReactCompositeComponentWrapper),不是对象分为文本节点(ReactDOMTextComponent)和空节点(ReactDOMEmptyComponent)。

上述四种类常见的方法 mountComponent 用于创建组件,而 updateComponent 用于用户更新组件。而我们自定义组件的生命周期函数以及 render 函数都是在这些私有类的方法里被调用的。

其中 ReactDOMComponent 的 mountComponent 方法会自己操作浏览器 DOM 元素。而 ReactCompositeComponentWrapper 的则是实例化自定义组件,最后是通过递归调用到 ReactDOMComponent 的 mountComponent 方法来得到真实 DOM。

ReactCompositeComponentWrapper mountComponent 的过程:

得到实例化 App 对象 instancerenderedElement = instance.render();初始化 renderedElement 得到 childchild.mountComponent(container)

在第一步得到 instance 对象之后,就会去看 instance.componentWillMount 是否有被定义,有的话调用,而在整个渲染过程结束之后调用 componentDidMount。

setState 流程newState 存入 pendingState 队列根据一个变量 isBatchingUpdates 判断是直接更新 state 还是放到队列中。也就是决定是将组件放到 dirtyComponents 中,还是遍历 dirtyComponents,调用 updateComponent,去更新 state 或者 props。isBatchingUpdates 默认是 false,React 在调用事件处理函数之前就会调用 batchedUpdates 改变值,以此让 state 不会立即更新。

batchedUpdates 是通过事务的方式去保证一次更新的完整

更新过程中 ReactCompositeComponentWrapper 的 updateComponent 流程:

计算出 nextStaterender() 得到 nextRenderElement与 preRenderElement 进行 diff 比较,更新节点。

相关生命周期:

shouldComponentUpdate 在第一步调用得到 nextState 之后调用当 shouldComponentUpdate 返回 true 的时候,会先调用 componentWillUpdate,在整个更新过程结束之后调用 componentDidUpdate。

ReactDOMComponent 的 updateComponent 流程就是直接更新浏览器 DOM 元素。

React 优化

优化的方向有两个,一个减少 render 次数,也就是减少 diff 计算。还有一个是减少计算的量,主要是减少重复计算,对于函数式组件来说,每次 render 都会重新从头开始执行函数调用。在类组件中主要使用 shouldComponentUpdate 生命周期和 PureComponent 组件去减少 render 次数,函数式组件主要使用:

React.memo:等同于 PureComponent,用它包裹子组件,当父组件需要重新 render 的时候,如果传给自己的 props 不变,就不会触发重新 render。memo 可以添加第二个参数,是个函数,参数为前后 props,返回 true 不需要重新 render。useCallback:应用场景是父组件向子组件传递方法,当父组件重新渲染时,代码都会重新执行。所以就算子组件包裹了 React.memo,也会重新渲染。可以通过 useCallback 进行记忆传递的方法,并将记忆的方法传递给子组件。useMemo:如果在组件有个变量的值需要大量的计算才可以得出,因为函数组件重新渲染就会重新执行代码,所以该变量的值也会重新计算,就可以 useMemo 做计算结果缓存。Fiber

其作用是会在浏览器空闲时期依次调用函数, 这就可以在主事件循环中执行后台或低优先级的任务,而且不会对像动画和用户交互这样延迟触发而且关键的事件产生影响。函数一般会按先进先调用的顺序执行,除非函数在浏览器调用它之前就到了它的超时时间。

fiber 借助单链表数据结构将 diff 算法的递归遍历变为循环遍历。

当执行 setState() 或首次 render() 时,进入工作循环,循环体中处理的单元为 Fiber Node, 即是拆分任务的最小单位,从根节点开始,自顶向下逐节点构造 workInProgress tree(构建中的新 Fiber Tree)。

beginWork() 主要做的事情是从顶向下生成所有的 Fiber Node,并标记 Diff。

completeUnitOfWork() 当没有子节点,开始遍历兄弟节点作为下一个处理单元,处理完兄弟节点开始向上回溯,真到再次回去根节点为止,将收集向上回溯过程中的所有 diff,拿到 diff 后开始进入 commit 阶段。

构建 workInProgress tree 的过程就是 diff 的过程,通过 requestIdleCallback 来调度执行一组任务,每完成一个任务后回来看看有没有插队的(更紧急的),把时间控制权交还给主线程,直到下一次 requestIdleCallback 回调再继续构建 workInProgress tree。

requestIdleCallback 的兼容方案

需要原因:兼容性不好,目前只能一秒调用回调 20 次。

因为 diff 的过程需要多次间隔调用,由此可以借助 requestAnimationFrame,它回调方法会在每次重绘前执行,另外它还存在一个瑕疵:页面处于后台时该回调函数不会执行,所以需要 setTimeout 进行补救。两个定时器内部互相取消对方。

在一帧当中,浏览器可能会响应用户的交互事件、执行 JS、进行渲染的一系列计算绘制,diff 就是在执行 JS 这个过程中。如果在一帧范围内没有执行完毕就会出现掉帧,影响用户体验,所以就需要在当下存在空闲时间我们才去执行任务。否则就等到下一帧的空闲时间继续执行。

是否有时间继续 diff 是通过计算剩余时间来判断,简单来说就是假设当前时间为 5000,浏览器支持 60 帧,那么 1 帧近似 16 毫秒,那么就会计算出下一帧时间为 5016。得出下一帧时间以后,我们只需对比当前时间是否小于下一帧时间即可,这样就能清楚地知道是否还有空闲时间去执行任务。

在事件循环中,渲染以后只有宏任务是最先会被执行的,所以选择优先级高的 MessageChannel。

调度的时候先判断任务是否过期,没有过期先计算下一帧时间(通过 requestAnimationFrame ),再调用 port.postMessage(undefined)(过期直接调用) ,这样渲染之后 channel.port1.onmessage 就会执行(任务没过期就要对比当前时间和下一帧时间,还有时间就执行任务,没有就看下一帧是否能执行任务,过期则直接执行)。

事件机制

React 事件并没有绑定在真实的 Dom 节点上,而是通过事件代理,在最外层的 document 上对事件进行统一分发,原生事件在目标阶段执行,React 在冒泡阶段执行。

组件挂载更新时,给 document 注册原生事件回调为 dispatchEvent (统一的事件分发机制)。

事件初始化,添加到 listenerBank,结构是: listenerBank[registrationName][key]

触发事件时:

触发 document 注册原生事件的回调 dispatchEvent获取到触发这个事件最深一级的元素遍历这个元素的所有父元素,依次对每一级元素进行处理。构造合成事件。将每一级的合成事件存储在 eventQueue 事件队列中。遍历 eventQueue。通过 isPropagationStopped 判断当前事件是否执行了阻止冒泡方法。如果阻止了冒泡,停止遍历,否则通过 executeDispatch 执行合成事件。释放处理完成的事件。diffReact 15

基于两个假设

两个相同的组件产生类似的 DOM 结构,不同组件产生不同 DOM 结构对于同一层次的一组子节点,它们可以通过唯一的 id 区分

tree diff

主要的规则就是跨层级的节点移动不会考虑复用,直接新建再删除。

注意:在开发组件时,保持稳定的 DOM 结构会有助于性能的提升。例如,可以通过 CSS 隐藏或显示节点,而不是真的移除或添加 DOM 节点。

对于不同的节点类型,react 会基于第一条假设,直接删去旧的节点,新建一个新的节点。

component diff

如果是同一类型的组件,根据新节点的 props 去更新原来根节点的组件实例,触发一个更新的过程,按照原策略继续比较 virtual DOM tree。如果不是,则将该组件判断为 dirty component,从而替换整个组件下的所有子节点。

element diff

当节点处于同一层级时,React diff 提供了三种节点操作,分别为:INSERT_MARKUP(插入)、MOVE_EXISTING(移动)和 REMOVE_NODE(删除)。

在没有 key 的情况下对比节点的时候,是一个一个按着顺序对比的。比如以下这种情况在没有 key 的情况下会删除 Test3,新建 Test2,新建 Test3。

对于有 key 数组的 diff

先看只考虑移动的情况

旧集合:A | B | C | D

新集合:B | A | D | C

首先对新集合的节点进行循环遍历,for (name in nextChildren),通过唯一 key 可以判断新老集合中是否存在相同的节点,if (prevChild === nextChild),如果存在相同节点,则进行移动操作,但在移动前需要将当前节点在老集合中的位置与 lastIndex 进行比较,if (child._mountIndex < lastIndex),则进行节点移动操作,否则不执行该操作。这是一种顺序优化手段,lastIndex 一直在更新,表示访问过的节点在老集合中最右的位置(即最大的位置),如果新集合中当前访问的节点比 lastIndex 大,说明当前访问节点在老集合中就比上一个节点位置靠后,则该节点不会影响其他节点的位置,因此不用添加到差异队列中,即不执行移动操作,只有当访问的节点比 lastIndex 小时,才需要进行移动操作。

在考虑移动的情况上,如果新集合中有新节点,则创建新节点(标记其在新集合中的位置),nextIndex++ 进入下一个节点的判断。

当完成新集合中所有节点 diff 时,最后还需要对老集合进行循环遍历,判断是否存在新集合中没有但老集合中仍存在的节点。存在就标记为删除。

由上面可以看出移动的操作都是向后的,出现以下这种情况是要移动三次的。

旧集合:A | B | C | D

新集合:D | A | B | C

React 16

对于数组的 diff

第一轮遍历的核心逻辑是复用和当前节点索引一致的旧节点,一旦出现不能复用的情况就跳出遍历。当第一轮遍历结束后,会出现两种情况:newChild 已经遍历完,只需要把所有剩余的旧节点都删除即可。旧节点已经遍历完了,开始第二轮遍历,只需要把剩余新的节点全部创建完毕即可。找出可以复用的子节点,否则创建。原理是将旧节点放置在 map 结构中,循环新节点看是否有匹配的项,匹配则在 map 中删除,并移动位置,循环结束后将 map 中还存在的旧节点删除。

注:删除新建移动的操作都只是先做标记。

react-router 原理

hash 路由:核心是监听了 load 和 onHashChange 事件,在页面刷新或者 URL hash 改变时渲染不同页面组件。 history API 路由:核心是通过 replaceState 和 pushState 去改变页面 URL,通过 popState 事件监听 history 对象改变的时候改变页面。

react-redux

Redux 是 JavaScript 状态容器,能提供可预测化的状态管理。需要它的原因是因为前端有大量无规律的交互和异步操作,而且随着代码量越来越大,我们要维护的状态也越来越多。它能提供的就是让每个 State 变化可预测,动作与状态统一管理。

connect 原理

connect 方法是一个高阶组件,主要的两个参数都是函数,命名为 mapStateToProps 和 mapDispatchToProps,内部原理是获取 store 添加订阅后,将 state 和 dispatch 分别传入上面两个方法,返回需要的 state 和 改变 state 的方法添加到 UI 组件的 props 上。

前提是在应用顶层已经使用 provider 组件,并应用初始化时创建 store。

中间件原理

中间件可以说是 dispatch 的增强或者替换。

applyMiddleware([middleware]) 返回的是一个函数,createStore 内部会使用这个函数的调用结果(参数为 createStore)创建 store。

// 一个简单的中间件规范,next 可以理解成 dispatch。 const middleware = store => next => action => { // 一些操作 next(action); // 一些操作 }; const applyMiddleware = function(...middlewares) { return function rewriteCreateStoreFunc(oldCreateStore) { return function newCreateStore(reducer, initState) { const store = oldCreateStore(reducer, initState); const chain = middlewares.map(middleware => middleware(store)); let dispatch = store.dispatch; chain.reverse().map(middleware => { dispatch = middleware(dispatch); }); // 多个中间件的情况相当于 dispatch = middleware1(middleware2(middleware3(dispatch))) // middleware1 内执行包含 middleware2 操作的 dispatch,middleware2 内执行包含 middleware3 操作的 dispatch // 所以中间件的顺序是从左到右的 store.dispatch = dispatch; return store; }; }; }; const store = createStore(reducer, initState, applyMiddleware(middleware));

redux 是通过 compose 函数也做到 [A, B, C] 转换成 A(B(C(next)))

// 结合上面代码 dispatch = compose(...chain)(store.dispatch); export default function compose(...funcs) { if (funcs.length === 1) { return funcs[0]; } return funcs.reduce((a, b) => (...args) => a(b(...args))); }



【本文地址】

公司简介

联系我们

今日新闻


点击排行

实验室常用的仪器、试剂和
说到实验室常用到的东西,主要就分为仪器、试剂和耗
不用再找了,全球10大实验
01、赛默飞世尔科技(热电)Thermo Fisher Scientif
三代水柜的量产巅峰T-72坦
作者:寞寒最近,西边闹腾挺大,本来小寞以为忙完这
通风柜跟实验室通风系统有
说到通风柜跟实验室通风,不少人都纠结二者到底是不
集消毒杀菌、烘干收纳为一
厨房是家里细菌较多的地方,潮湿的环境、没有完全密
实验室设备之全钢实验台如
全钢实验台是实验室家具中较为重要的家具之一,很多

推荐新闻


图片新闻

实验室药品柜的特性有哪些
实验室药品柜是实验室家具的重要组成部分之一,主要
小学科学实验中有哪些教学
计算机 计算器 一般 打孔器 打气筒 仪器车 显微镜
实验室各种仪器原理动图讲
1.紫外分光光谱UV分析原理:吸收紫外光能量,引起分
高中化学常见仪器及实验装
1、可加热仪器:2、计量仪器:(1)仪器A的名称:量
微生物操作主要设备和器具
今天盘点一下微生物操作主要设备和器具,别嫌我啰嗦
浅谈通风柜使用基本常识
 众所周知,通风柜功能中最主要的就是排气功能。在

专题文章

    CopyRight 2018-2019 实验室设备网 版权所有 win10的实时保护怎么永久关闭